@hegemonart/get-design-done 1.58.0 → 1.59.1
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/.claude-plugin/marketplace.json +4 -4
- package/.claude-plugin/plugin.json +17 -3
- package/CHANGELOG.md +70 -5
- package/README.md +1 -1
- package/SKILL.md +1 -0
- package/bin/gdd-mcp +12 -1
- package/bin/gdd-state-mcp +12 -1
- package/connections/gdd-state.md +8 -8
- package/package.json +2 -4
- package/reference/codex-tools.md +1 -1
- package/reference/gemini-tools.md +1 -1
- package/reference/known-failure-modes.md +2 -2
- package/reference/registry.json +1 -1
- package/reference/schemas/generated.d.ts +240 -4
- package/reference/schemas/mcp-gdd-state-tools.schema.json +1 -1
- package/reference/schemas/mcp-gdd-tools.schema.json +1 -1
- package/reference/skill-graph.md +2 -1
- package/scripts/install.cjs +21 -14
- package/scripts/lib/install/mcp-register.cjs +131 -50
- package/scripts/lib/manifest/skills.json +7 -0
- package/sdk/cli/commands/audit.ts +66 -6
- package/sdk/cli/index.js +33 -3
- package/skills/bandit-reset/SKILL.md +91 -0
|
@@ -160,6 +160,108 @@ export interface DesignConfigJson {
|
|
|
160
160
|
|
|
161
161
|
export type ConfigSchema = DesignConfigJson;
|
|
162
162
|
|
|
163
|
+
// ---- design-context.schema.json ----
|
|
164
|
+
/**
|
|
165
|
+
* The canonical typed knowledge graph of a design system, persisted at .design/context-graph.json. Nodes are design entities (tokens, components, screens, patterns); edges are typed relationships between them (uses-token, composes, transitions-to). Built by a two-phase mapper: a deterministic extract pass emits node/edge skeletons, then an LLM summary pass fills each node summary. Validated structurally by scripts/validate-design-context.cjs and queried by scripts/lib/design-context-query.cjs.
|
|
166
|
+
*/
|
|
167
|
+
export interface DesignContextGraph {
|
|
168
|
+
/**
|
|
169
|
+
* Schema version of this graph document (e.g. "52.0").
|
|
170
|
+
*/
|
|
171
|
+
schema_version: string;
|
|
172
|
+
/**
|
|
173
|
+
* ISO-8601 timestamp the graph was last assembled (optional).
|
|
174
|
+
*/
|
|
175
|
+
generated_at?: string;
|
|
176
|
+
/**
|
|
177
|
+
* All design entities in the graph. Node ids must be unique.
|
|
178
|
+
*/
|
|
179
|
+
nodes: Node[];
|
|
180
|
+
/**
|
|
181
|
+
* All typed relationships. Every source/target must resolve to a node id.
|
|
182
|
+
*/
|
|
183
|
+
edges: Edge[];
|
|
184
|
+
[k: string]: unknown;
|
|
185
|
+
}
|
|
186
|
+
export interface Node {
|
|
187
|
+
/**
|
|
188
|
+
* Stable unique identifier for the node (referenced by edge source/target).
|
|
189
|
+
*/
|
|
190
|
+
id: string;
|
|
191
|
+
/**
|
|
192
|
+
* The kind of design entity this node represents.
|
|
193
|
+
*/
|
|
194
|
+
type:
|
|
195
|
+
| 'token'
|
|
196
|
+
| 'component'
|
|
197
|
+
| 'variant'
|
|
198
|
+
| 'state'
|
|
199
|
+
| 'motion-fragment'
|
|
200
|
+
| 'a11y-pattern'
|
|
201
|
+
| 'screen'
|
|
202
|
+
| 'layer'
|
|
203
|
+
| 'pattern'
|
|
204
|
+
| 'anti-pattern';
|
|
205
|
+
/**
|
|
206
|
+
* Human-readable name of the entity.
|
|
207
|
+
*/
|
|
208
|
+
name: string;
|
|
209
|
+
/**
|
|
210
|
+
* One-line LLM-authored description of what the entity is and does. A stub summary (empty or identical to name) is flagged by the validator as a soft warning.
|
|
211
|
+
*/
|
|
212
|
+
summary: string;
|
|
213
|
+
/**
|
|
214
|
+
* Controlled-vocabulary tags grouping the node by concern (see reference/design-context-tag-vocab.md). Unknown tags are a soft warning, not a hard error.
|
|
215
|
+
*/
|
|
216
|
+
tags?: string[];
|
|
217
|
+
/**
|
|
218
|
+
* Coarse complexity bucket for the entity.
|
|
219
|
+
*/
|
|
220
|
+
complexity: 'simple' | 'moderate' | 'complex';
|
|
221
|
+
/**
|
|
222
|
+
* Optional finer classification. For token nodes one of color/spacing/typography/radius/shadow; for layer nodes one of Atomic/Molecular/Organism/Template. Free-form for other node types.
|
|
223
|
+
*/
|
|
224
|
+
subtype?: string;
|
|
225
|
+
[k: string]: unknown;
|
|
226
|
+
}
|
|
227
|
+
export interface Edge {
|
|
228
|
+
/**
|
|
229
|
+
* Node id the edge originates from.
|
|
230
|
+
*/
|
|
231
|
+
source: string;
|
|
232
|
+
/**
|
|
233
|
+
* Node id the edge points to.
|
|
234
|
+
*/
|
|
235
|
+
target: string;
|
|
236
|
+
/**
|
|
237
|
+
* The kind of relationship between source and target.
|
|
238
|
+
*/
|
|
239
|
+
type:
|
|
240
|
+
| 'uses-token'
|
|
241
|
+
| 'composes'
|
|
242
|
+
| 'extends'
|
|
243
|
+
| 'transitions-to'
|
|
244
|
+
| 'depends-on'
|
|
245
|
+
| 'mirrors'
|
|
246
|
+
| 'conflicts-with'
|
|
247
|
+
| 'referenced-by'
|
|
248
|
+
| 'tested-by'
|
|
249
|
+
| 'documented-by'
|
|
250
|
+
| 'consumes-context'
|
|
251
|
+
| 'provides-context';
|
|
252
|
+
/**
|
|
253
|
+
* Whether the relationship reads source-to-target (forward), target-to-source (backward), or both ways (bidirectional).
|
|
254
|
+
*/
|
|
255
|
+
direction: 'forward' | 'backward' | 'bidirectional';
|
|
256
|
+
/**
|
|
257
|
+
* Relationship strength in the inclusive range 0..1.
|
|
258
|
+
*/
|
|
259
|
+
weight: number;
|
|
260
|
+
[k: string]: unknown;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export type DesignContextSchema = DesignContextGraph;
|
|
264
|
+
|
|
163
265
|
// ---- events.schema.json ----
|
|
164
266
|
/**
|
|
165
267
|
* One line of .design/telemetry/events.jsonl — the append-only telemetry stream produced by Plan 20-06. Each event is a single JSON object followed by a newline. See .planning/phases/20-gdd-sdk-foundation/20-06-PLAN.md.
|
|
@@ -168,7 +270,7 @@ export type Event = {
|
|
|
168
270
|
[k: string]: unknown;
|
|
169
271
|
} & {
|
|
170
272
|
/**
|
|
171
|
-
* Free-form event type identifier. Pre-registered seeds: state.mutation, state.transition, stage.entered, stage.exited, hook.fired, error, capability_gap, kfm-candidate, router_pick, verify_outcome, rollout_started, rollout_advanced, rollout_stuck, budget_forecast, project_cap_warning, project_cap_halt.
|
|
273
|
+
* Free-form event type identifier. Pre-registered seeds: state.mutation, state.transition, stage.entered, stage.exited, hook.fired, error, capability_gap, kfm-candidate, router_pick, verify_outcome, rollout_started, rollout_advanced, rollout_stuck, budget_forecast, project_cap_warning, project_cap_halt, live_session_start, live_pick, live_generate, live_accept, live_discard, live_session_end, instinct_emitted, instinct_promoted, instinct_decayed, risk_assessment.
|
|
172
274
|
*/
|
|
173
275
|
type: string;
|
|
174
276
|
/**
|
|
@@ -266,6 +368,73 @@ export interface AgentInsightLine {
|
|
|
266
368
|
|
|
267
369
|
export type InsightLineSchema = AgentInsightLine;
|
|
268
370
|
|
|
371
|
+
// ---- instinct.schema.json ----
|
|
372
|
+
/**
|
|
373
|
+
* An atomic, confidence-weighted design instinct learned across cycles. Validates the YAML frontmatter object of an instinct unit (see reference/instinct-format.md). Project-scoped units live at <root>/instincts/instincts.json; promoted global units live at ~/.claude/gdd/global-instincts.json. Persisted + queried by scripts/lib/instinct-store.cjs.
|
|
374
|
+
*/
|
|
375
|
+
export type InstinctUnit = {
|
|
376
|
+
[k: string]: unknown;
|
|
377
|
+
} & {
|
|
378
|
+
/**
|
|
379
|
+
* Kebab-case stable identifier, e.g. "prefer-token-over-hex". Lowercase letters, digits, single hyphens.
|
|
380
|
+
*/
|
|
381
|
+
id: string;
|
|
382
|
+
/**
|
|
383
|
+
* One sentence naming the situation that fires the instinct, e.g. "When a color literal appears in a component, reach for a design token first."
|
|
384
|
+
*/
|
|
385
|
+
trigger: string;
|
|
386
|
+
/**
|
|
387
|
+
* Posterior trust in the instinct. Floor 0.3 (a fresh instinct is advisory, never directive); ceiling 0.9 (no instinct is ever certain). TTL decay multiplies this by 0.9 when the instinct goes unsurfaced.
|
|
388
|
+
*/
|
|
389
|
+
confidence: number;
|
|
390
|
+
/**
|
|
391
|
+
* Lifecycle stage the instinct applies to, aligned to the Phase 50 lifecycle stages.
|
|
392
|
+
*/
|
|
393
|
+
domain: 'intake' | 'explore' | 'decide' | 'build' | 'verify' | 'operate' | 'utility';
|
|
394
|
+
/**
|
|
395
|
+
* project = learned from one repository; global = promoted after the K/M gate across distinct projects.
|
|
396
|
+
*/
|
|
397
|
+
scope: 'project' | 'global';
|
|
398
|
+
/**
|
|
399
|
+
* 8-char hex sha of the normalized git origin the instinct was first learned from. Required for project scope; optional for global (a promoted instinct is no longer tied to one origin).
|
|
400
|
+
*/
|
|
401
|
+
project_id?: string;
|
|
402
|
+
/**
|
|
403
|
+
* Which producer minted the instinct: a reflection pass, the extract-learnings step, or a direct user assertion.
|
|
404
|
+
*/
|
|
405
|
+
source: 'reflection' | 'extract-learnings' | 'user';
|
|
406
|
+
/**
|
|
407
|
+
* How many distinct design cycles have surfaced this instinct. Feeds the K=2 half of the promotion gate.
|
|
408
|
+
*/
|
|
409
|
+
cycles_seen?: number;
|
|
410
|
+
/**
|
|
411
|
+
* Set of distinct project ids that have surfaced this instinct. Its length feeds the M=2 half of the promotion gate.
|
|
412
|
+
*/
|
|
413
|
+
project_ids?: string[];
|
|
414
|
+
/**
|
|
415
|
+
* ISO date (YYYY-MM-DD) the instinct was first recorded.
|
|
416
|
+
*/
|
|
417
|
+
first_seen?: string;
|
|
418
|
+
/**
|
|
419
|
+
* ISO date (YYYY-MM-DD) the instinct was last surfaced. Resets the TTL decay window.
|
|
420
|
+
*/
|
|
421
|
+
last_seen?: string;
|
|
422
|
+
/**
|
|
423
|
+
* Beta posterior success weight. Seeded from the Beta(2,8) prior on promotion.
|
|
424
|
+
*/
|
|
425
|
+
alpha?: number;
|
|
426
|
+
/**
|
|
427
|
+
* Beta posterior failure weight. Seeded from the Beta(2,8) prior on promotion.
|
|
428
|
+
*/
|
|
429
|
+
beta?: number;
|
|
430
|
+
/**
|
|
431
|
+
* Tag recording which prior class seeded the posterior, e.g. "instinct".
|
|
432
|
+
*/
|
|
433
|
+
prior_class?: string;
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
export type InstinctSchema = InstinctUnit;
|
|
437
|
+
|
|
269
438
|
// ---- intel.schema.json ----
|
|
270
439
|
/**
|
|
271
440
|
* Shape of intel-store slice files per reference/intel-schema.md. Each slice has a generated timestamp and one array-valued payload key matching the slice name.
|
|
@@ -394,6 +563,57 @@ export interface IterationBudget {
|
|
|
394
563
|
|
|
395
564
|
export type IterationBudgetSchema = IterationBudget;
|
|
396
565
|
|
|
566
|
+
// ---- live-session.schema.json ----
|
|
567
|
+
/**
|
|
568
|
+
* A single `/gdd:live` session record persisted at .design/live-sessions/<session-id>.json. Captures the pick -> generate -> accept/discard loop as an append-only event log so a session survives a crash or --resume. Written atomically by scripts/lib/live/session-store.cjs.
|
|
569
|
+
*/
|
|
570
|
+
export interface LiveSession {
|
|
571
|
+
/**
|
|
572
|
+
* Schema version of this session record (e.g. "47.0").
|
|
573
|
+
*/
|
|
574
|
+
schema_version: string;
|
|
575
|
+
/**
|
|
576
|
+
* Stable id; also the basename of the on-disk file (no path separators).
|
|
577
|
+
*/
|
|
578
|
+
session_id: string;
|
|
579
|
+
/**
|
|
580
|
+
* Lifecycle status. Only in_progress sessions are resumable.
|
|
581
|
+
*/
|
|
582
|
+
status: 'in_progress' | 'completed' | 'abandoned';
|
|
583
|
+
/**
|
|
584
|
+
* ISO-8601 timestamp the session was created.
|
|
585
|
+
*/
|
|
586
|
+
started_at: string;
|
|
587
|
+
/**
|
|
588
|
+
* ISO-8601 timestamp the session was closed; null while in_progress.
|
|
589
|
+
*/
|
|
590
|
+
ended_at: string | null;
|
|
591
|
+
/**
|
|
592
|
+
* The page the element was picked from (optional).
|
|
593
|
+
*/
|
|
594
|
+
url?: string;
|
|
595
|
+
/**
|
|
596
|
+
* Dev-server descriptor (url/port/command, or a plain string). Free-form by design.
|
|
597
|
+
*/
|
|
598
|
+
dev_server?: string | {} | null;
|
|
599
|
+
/**
|
|
600
|
+
* Append-only log of session events, oldest first.
|
|
601
|
+
*/
|
|
602
|
+
events: {
|
|
603
|
+
/**
|
|
604
|
+
* Event kind.
|
|
605
|
+
*/
|
|
606
|
+
kind: 'pick' | 'generate' | 'accept' | 'discard';
|
|
607
|
+
/**
|
|
608
|
+
* ISO-8601 timestamp the event was recorded.
|
|
609
|
+
*/
|
|
610
|
+
at: string;
|
|
611
|
+
[k: string]: unknown;
|
|
612
|
+
}[];
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export type LiveSessionSchema = LiveSession;
|
|
616
|
+
|
|
397
617
|
// ---- marketplace.schema.json ----
|
|
398
618
|
/**
|
|
399
619
|
* Shape of .claude-plugin/marketplace.json — the plugin marketplace descriptor.
|
|
@@ -448,7 +668,7 @@ export type McpBudgetSchema = MCPBudget;
|
|
|
448
668
|
|
|
449
669
|
// ---- mcp-gdd-state-tools.schema.json ----
|
|
450
670
|
/**
|
|
451
|
-
* Combined manifest of all 11 gdd-state MCP tool input+output schemas (Plan 20-05). Individual tool schemas live under
|
|
671
|
+
* Combined manifest of all 11 gdd-state MCP tool input+output schemas (Plan 20-05). Individual tool schemas live under sdk/mcp/gdd-state/schemas/ and the tool handlers reference them; this combined schema exists so downstream validators and codegen can compile a single surface.
|
|
452
672
|
*/
|
|
453
673
|
export interface McpGddStateTools {
|
|
454
674
|
tools: {
|
|
@@ -482,11 +702,11 @@ export type McpGddStateToolsSchema = McpGddStateTools;
|
|
|
482
702
|
|
|
483
703
|
// ---- mcp-gdd-tools.schema.json ----
|
|
484
704
|
/**
|
|
485
|
-
* Combined manifest of all gdd-mcp tool input+output schemas (Plan 27.7-02). Individual tool schemas live under
|
|
705
|
+
* Combined manifest of all gdd-mcp tool input+output schemas (Plan 27.7-02). Individual tool schemas live under sdk/mcp/gdd-mcp/schemas/ and the tool handlers reference them; this combined schema exists so downstream validators and codegen can compile a single surface (D-11).
|
|
486
706
|
*/
|
|
487
707
|
export interface McpGddTools {
|
|
488
708
|
/**
|
|
489
|
-
* Per-tool input/output schemas keyed by tool name. Exactly
|
|
709
|
+
* Per-tool input/output schemas keyed by tool name. Exactly 13 entries (D-03 cap, raised 12 -> 13 in Phase 52 for gdd_context_query).
|
|
490
710
|
*/
|
|
491
711
|
tools?: {
|
|
492
712
|
gdd_status?: {
|
|
@@ -499,6 +719,22 @@ export interface McpGddTools {
|
|
|
499
719
|
blocker_count: number;
|
|
500
720
|
};
|
|
501
721
|
};
|
|
722
|
+
gdd_context_query?: {
|
|
723
|
+
input: {
|
|
724
|
+
op: 'nodes' | 'edges' | 'path' | 'consumers-of' | 'unreachable' | 'cycles' | 'coverage';
|
|
725
|
+
type?: string;
|
|
726
|
+
tag?: string;
|
|
727
|
+
from?: string;
|
|
728
|
+
to?: string;
|
|
729
|
+
id?: string;
|
|
730
|
+
};
|
|
731
|
+
output: {
|
|
732
|
+
op: string;
|
|
733
|
+
graph_present: boolean;
|
|
734
|
+
path?: string;
|
|
735
|
+
result: unknown[] | {} | null;
|
|
736
|
+
};
|
|
737
|
+
};
|
|
502
738
|
gdd_phase_current?: {
|
|
503
739
|
input: {};
|
|
504
740
|
output: {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
3
|
"$id": "https://raw.githubusercontent.com/hegemonart/get-design-done/main/reference/schemas/mcp-gdd-state-tools.schema.json",
|
|
4
4
|
"title": "McpGddStateTools",
|
|
5
|
-
"description": "Combined manifest of all 11 gdd-state MCP tool input+output schemas (Plan 20-05). Individual tool schemas live under
|
|
5
|
+
"description": "Combined manifest of all 11 gdd-state MCP tool input+output schemas (Plan 20-05). Individual tool schemas live under sdk/mcp/gdd-state/schemas/ and the tool handlers reference them; this combined schema exists so downstream validators and codegen can compile a single surface.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"required": ["tools"],
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
3
|
"$id": "https://raw.githubusercontent.com/hegemonart/get-design-done/main/reference/schemas/mcp-gdd-tools.schema.json",
|
|
4
4
|
"title": "McpGddTools",
|
|
5
|
-
"description": "Combined manifest of all gdd-mcp tool input+output schemas (Plan 27.7-02). Individual tool schemas live under
|
|
5
|
+
"description": "Combined manifest of all gdd-mcp tool input+output schemas (Plan 27.7-02). Individual tool schemas live under sdk/mcp/gdd-mcp/schemas/ and the tool handlers reference them; this combined schema exists so downstream validators and codegen can compile a single surface (D-11).",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"properties": {
|
|
8
8
|
"tools": {
|
package/reference/skill-graph.md
CHANGED
|
@@ -9,7 +9,7 @@ is a `composes_with` edge (the source calls the target as sub-orchestration); a
|
|
|
9
9
|
a `next_skills` edge (a pipeline hint for what runs next). Stage grouping is best-effort and
|
|
10
10
|
inferred from the skill name; skills with no stage keyword fall under Utility.
|
|
11
11
|
|
|
12
|
-
Skills:
|
|
12
|
+
Skills: 95. Composition edges: 19 composes_with, 6 next_skills.
|
|
13
13
|
|
|
14
14
|
```mermaid
|
|
15
15
|
flowchart TD
|
|
@@ -65,6 +65,7 @@ flowchart TD
|
|
|
65
65
|
n_add_backlog["add-backlog"]
|
|
66
66
|
n_analyze_dependencies["analyze-dependencies"]
|
|
67
67
|
n_apply_reflections["apply-reflections"]
|
|
68
|
+
n_bandit_reset["bandit-reset"]
|
|
68
69
|
n_bandit_status["bandit-status"]
|
|
69
70
|
n_budget["budget"]
|
|
70
71
|
n_cache_manager["cache-manager"]
|
package/scripts/install.cjs
CHANGED
|
@@ -74,7 +74,7 @@ function helpText() {
|
|
|
74
74
|
' --dry-run Print the diff without writing',
|
|
75
75
|
' --config-dir D Override the config directory',
|
|
76
76
|
' --no-peer-prompt Suppress the post-install peer-CLI detection nudge',
|
|
77
|
-
' --register-mcp Register gdd-mcp with detected harnesses (Claude Code, Codex). Opt-in.',
|
|
77
|
+
' --register-mcp Register gdd-mcp + gdd-state with detected harnesses (Claude Code, Codex). Opt-in.',
|
|
78
78
|
' --no-register-mcp Skip MCP registration (default behavior; included for symmetry).',
|
|
79
79
|
' --doctor Print Tier-2 distribution-channel status (read-only; no install)',
|
|
80
80
|
' --help, -h Show this message',
|
|
@@ -287,6 +287,7 @@ async function main() {
|
|
|
287
287
|
}
|
|
288
288
|
|
|
289
289
|
// Phase 27.7 / Plan 27.7-04 — opt-in MCP registration (D-07).
|
|
290
|
+
// Phase 59.1 — registers BOTH gdd MCP servers (gdd-mcp + gdd-state).
|
|
290
291
|
// Fires only on real install (not uninstall, not dry-run) when the user
|
|
291
292
|
// passes --register-mcp explicitly. Default OFF; --no-register-mcp is a
|
|
292
293
|
// no-op today (reserved for symmetry / when default flips). Idempotent
|
|
@@ -298,23 +299,29 @@ async function main() {
|
|
|
298
299
|
try {
|
|
299
300
|
const result = registerMcp({ harness });
|
|
300
301
|
if (!result.detected) {
|
|
302
|
+
// CLI absent — single notice covers all servers for this harness.
|
|
301
303
|
process.stderr.write('[install] ' + result.notice + '\n');
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
// Report each registered server individually.
|
|
307
|
+
for (const s of result.servers || []) {
|
|
308
|
+
if (s.idempotent_skip) {
|
|
309
|
+
process.stdout.write(
|
|
310
|
+
'[install] ' + s.server + ' already registered with ' + harness + ' — skipping.\n',
|
|
311
|
+
);
|
|
312
|
+
} else if (s.applied) {
|
|
313
|
+
process.stdout.write(
|
|
314
|
+
'[install] ' + s.server + ' registered with ' + harness + '.\n',
|
|
315
|
+
);
|
|
316
|
+
} else {
|
|
317
|
+
process.stderr.write(
|
|
318
|
+
'[install] ' + s.server + ' registration with ' + harness + ' failed: exit ' + s.exit_code + '\n',
|
|
319
|
+
);
|
|
320
|
+
}
|
|
314
321
|
}
|
|
315
322
|
} catch (err) {
|
|
316
323
|
process.stderr.write(
|
|
317
|
-
'[install]
|
|
324
|
+
'[install] MCP registration error (' + harness + '): ' + (err && err.message ? err.message : err) + '\n',
|
|
318
325
|
);
|
|
319
326
|
}
|
|
320
327
|
}
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
// scripts/lib/install/mcp-register.cjs
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
|
-
// Plan 27.7-04 — registers
|
|
5
|
-
// (Claude Code, Codex) and detects existing registration. Idempotent;
|
|
4
|
+
// Plan 27.7-04 — registers gdd's MCP servers with the two harnesses that
|
|
5
|
+
// matter (Claude Code, Codex) and detects existing registration. Idempotent;
|
|
6
6
|
// graceful absent-CLI fallback (D-07).
|
|
7
7
|
//
|
|
8
|
+
// Phase 59.1 — MCP parity: gdd ships TWO MCP servers, both registered here:
|
|
9
|
+
// - gdd-mcp (read-only project tools; launch command `gdd-mcp`)
|
|
10
|
+
// - gdd-state (typed STATE mutators; launch command `gdd-state-mcp`)
|
|
11
|
+
// Each server is described in MCP_SERVERS as {name, launchCommand}. The
|
|
12
|
+
// per-harness add-args are built per server so the registration name and the
|
|
13
|
+
// launch command can differ (gdd-state registers under `gdd-state` but is
|
|
14
|
+
// launched via the `gdd-state-mcp` bin).
|
|
15
|
+
//
|
|
8
16
|
// Pure library — no side effects on require. Invoked by:
|
|
9
17
|
// - scripts/install.cjs --register-mcp (opt-in; default off per D-07)
|
|
10
18
|
// - skills/health/SKILL.md check-mcp-registration step (read-only detect)
|
|
@@ -13,39 +21,75 @@
|
|
|
13
21
|
// touching real CLIs in CI.
|
|
14
22
|
//
|
|
15
23
|
// Threat model: scripts/install.cjs --register-mcp writes to harness user-
|
|
16
|
-
// level config. Command args are hardcoded in HARNESSES (no
|
|
17
|
-
// injection surface); `--` separator before
|
|
18
|
-
// injection (T-27.7-04-06).
|
|
24
|
+
// level config. Command args are hardcoded in HARNESSES / MCP_SERVERS (no
|
|
25
|
+
// command-injection surface); the `--` separator before the launch command
|
|
26
|
+
// prevents flag injection (T-27.7-04-06).
|
|
19
27
|
|
|
20
28
|
const { spawnSync } = require('node:child_process');
|
|
21
29
|
|
|
22
|
-
|
|
30
|
+
// The set of MCP servers gdd registers. `name` is the registration name (and
|
|
31
|
+
// what appears in `<binary> mcp list`); `launchCommand` is the bin on PATH the
|
|
32
|
+
// harness spawns. For gdd-mcp the two coincide; for gdd-state they differ.
|
|
33
|
+
const MCP_SERVERS = Object.freeze([
|
|
34
|
+
Object.freeze({ name: 'gdd-mcp', launchCommand: 'gdd-mcp' }),
|
|
35
|
+
Object.freeze({ name: 'gdd-state', launchCommand: 'gdd-state-mcp' }),
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
// Back-compat: the original single-server name. Retained for existing
|
|
39
|
+
// importers (skills/health detection, type decls, tests).
|
|
40
|
+
const MCP_NAME = MCP_SERVERS[0].name;
|
|
41
|
+
|
|
42
|
+
// Build the `mcp add` argv for a given harness + server. Mirrors the original
|
|
43
|
+
// per-harness shape exactly: claude pins user scope (`-s user`), codex does
|
|
44
|
+
// not. The registration name precedes `--`; the launch command follows it.
|
|
45
|
+
function claudeAddArgs(server) {
|
|
46
|
+
return ['mcp', 'add', server.name, '-s', 'user', '--', server.launchCommand];
|
|
47
|
+
}
|
|
48
|
+
function codexAddArgs(server) {
|
|
49
|
+
return ['mcp', 'add', server.name, '--', server.launchCommand];
|
|
50
|
+
}
|
|
23
51
|
|
|
24
52
|
const HARNESSES = Object.freeze({
|
|
25
53
|
claude: Object.freeze({
|
|
26
54
|
binary: 'claude',
|
|
27
|
-
|
|
55
|
+
addArgsFor: claudeAddArgs,
|
|
56
|
+
// Back-compat: addArgs for the primary (gdd-mcp) server.
|
|
57
|
+
addArgs: Object.freeze(claudeAddArgs(MCP_SERVERS[0])),
|
|
28
58
|
listArgs: Object.freeze(['mcp', 'list']),
|
|
29
59
|
listMatchPattern: /\bgdd-mcp\b/,
|
|
30
60
|
}),
|
|
31
61
|
codex: Object.freeze({
|
|
32
62
|
binary: 'codex',
|
|
33
|
-
|
|
63
|
+
addArgsFor: codexAddArgs,
|
|
64
|
+
addArgs: Object.freeze(codexAddArgs(MCP_SERVERS[0])),
|
|
34
65
|
listArgs: Object.freeze(['mcp', 'list']),
|
|
35
66
|
listMatchPattern: /\bgdd-mcp\b/,
|
|
36
67
|
}),
|
|
37
68
|
});
|
|
38
69
|
|
|
70
|
+
// Whether a server name appears in the harness's `mcp list` stdout. Built per
|
|
71
|
+
// call so each server is matched on its own word-boundary-delimited name.
|
|
72
|
+
function makeListMatchPattern(serverName) {
|
|
73
|
+
// Escape regex metacharacters in the server name (defensive; names are
|
|
74
|
+
// hardcoded today but this keeps the matcher injection-safe).
|
|
75
|
+
const escaped = serverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
76
|
+
return new RegExp('(^|[^\\w-])' + escaped + '([^\\w-]|$)');
|
|
77
|
+
}
|
|
78
|
+
|
|
39
79
|
/**
|
|
40
|
-
* Build the command tuple for a given harness + mode.
|
|
80
|
+
* Build the command tuple for a given harness + mode (+ optional server).
|
|
41
81
|
* Currently only 'register' (add) is supported in command-build; 'detect'
|
|
42
82
|
* uses listArgs internally, 'unregister' is reserved for future work.
|
|
83
|
+
*
|
|
84
|
+
* @param {'claude'|'codex'} harness
|
|
85
|
+
* @param {'register'|'detect'} [mode='register']
|
|
86
|
+
* @param {{name:string,launchCommand:string}} [server=MCP_SERVERS[0]]
|
|
43
87
|
*/
|
|
44
|
-
function buildHarnessCommand(harness, mode = 'register') {
|
|
88
|
+
function buildHarnessCommand(harness, mode = 'register', server = MCP_SERVERS[0]) {
|
|
45
89
|
const h = HARNESSES[harness];
|
|
46
90
|
if (!h) throw new Error('Unknown harness: ' + harness);
|
|
47
91
|
if (mode === 'register') {
|
|
48
|
-
return { binary: h.binary, args:
|
|
92
|
+
return { binary: h.binary, args: h.addArgsFor(server) };
|
|
49
93
|
}
|
|
50
94
|
if (mode === 'detect') {
|
|
51
95
|
return { binary: h.binary, args: Array.from(h.listArgs) };
|
|
@@ -75,10 +119,16 @@ function detectHarnessPresent(harness, spawnFn = spawnSync) {
|
|
|
75
119
|
}
|
|
76
120
|
|
|
77
121
|
/**
|
|
78
|
-
* Detect whether
|
|
79
|
-
* Runs `<binary> mcp list` and matches against
|
|
122
|
+
* Detect whether a given MCP server is already registered with the harness.
|
|
123
|
+
* Runs `<binary> mcp list` and matches against the server's name. When
|
|
124
|
+
* `serverName` is omitted, falls back to the harness's primary (gdd-mcp)
|
|
125
|
+
* pattern for back-compat with the original single-server signature.
|
|
126
|
+
*
|
|
127
|
+
* @param {'claude'|'codex'} harness
|
|
128
|
+
* @param {Function} [spawnFn]
|
|
129
|
+
* @param {string} [serverName] server registration name to match
|
|
80
130
|
*/
|
|
81
|
-
function isAlreadyRegistered(harness, spawnFn = spawnSync) {
|
|
131
|
+
function isAlreadyRegistered(harness, spawnFn = spawnSync, serverName) {
|
|
82
132
|
const h = HARNESSES[harness];
|
|
83
133
|
if (!h) throw new Error('Unknown harness: ' + harness);
|
|
84
134
|
let result;
|
|
@@ -92,45 +142,20 @@ function isAlreadyRegistered(harness, spawnFn = spawnSync) {
|
|
|
92
142
|
}
|
|
93
143
|
if (!result || result.status !== 0) return false;
|
|
94
144
|
const stdout = (result.stdout || '').toString();
|
|
95
|
-
|
|
145
|
+
const pattern = serverName ? makeListMatchPattern(serverName) : h.listMatchPattern;
|
|
146
|
+
return pattern.test(stdout);
|
|
96
147
|
}
|
|
97
148
|
|
|
98
149
|
/**
|
|
99
|
-
* Register
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* @param {'claude'|'codex'} opts.harness
|
|
103
|
-
* @param {'register'|'unregister'|'detect'} [opts.mode='register']
|
|
104
|
-
* @param {boolean} [opts.dryRun=false]
|
|
105
|
-
* @param {Function} [opts.spawnFn] child_process.spawnSync substitute
|
|
106
|
-
* @returns {object} {harness, action, detected, command, applied,
|
|
107
|
-
* idempotent_skip, notice?, stdout?, stderr?,
|
|
108
|
-
* exit_code?, dry_run?}
|
|
150
|
+
* Register a single MCP server with the given harness. Assumes the harness CLI
|
|
151
|
+
* is already known to be present (caller does the PATH check once for all
|
|
152
|
+
* servers). Returns the same per-server shape the original registerMcp did.
|
|
109
153
|
*/
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
if (mode !== 'register' && mode !== 'detect' && mode !== 'unregister') {
|
|
115
|
-
throw new Error('Unsupported mode: ' + mode);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Step 1 — detect harness CLI on PATH
|
|
119
|
-
if (!detectHarnessPresent(harness, spawnFn)) {
|
|
120
|
-
return {
|
|
121
|
-
harness,
|
|
122
|
-
action: mode,
|
|
123
|
-
detected: false,
|
|
124
|
-
command: null,
|
|
125
|
-
applied: false,
|
|
126
|
-
idempotent_skip: false,
|
|
127
|
-
notice: harness + ' CLI not on PATH — skipping ' + MCP_NAME + ' registration',
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Step 2 — idempotency check: already registered?
|
|
132
|
-
if (isAlreadyRegistered(harness, spawnFn)) {
|
|
154
|
+
function registerOneServer(harness, server, { mode, dryRun, spawnFn }) {
|
|
155
|
+
// Idempotency check: this specific server already registered?
|
|
156
|
+
if (isAlreadyRegistered(harness, spawnFn, server.name)) {
|
|
133
157
|
return {
|
|
158
|
+
server: server.name,
|
|
134
159
|
harness,
|
|
135
160
|
action: mode,
|
|
136
161
|
detected: true,
|
|
@@ -140,12 +165,13 @@ function registerMcp({ harness, mode = 'register', dryRun = false, spawnFn = spa
|
|
|
140
165
|
};
|
|
141
166
|
}
|
|
142
167
|
|
|
143
|
-
//
|
|
144
|
-
const { binary, args } = buildHarnessCommand(harness, 'register');
|
|
168
|
+
// Build + dispatch the per-server add command.
|
|
169
|
+
const { binary, args } = buildHarnessCommand(harness, 'register', server);
|
|
145
170
|
const commandStr = binary + ' ' + args.join(' ');
|
|
146
171
|
|
|
147
172
|
if (dryRun) {
|
|
148
173
|
return {
|
|
174
|
+
server: server.name,
|
|
149
175
|
harness,
|
|
150
176
|
action: mode,
|
|
151
177
|
detected: true,
|
|
@@ -161,6 +187,7 @@ function registerMcp({ harness, mode = 'register', dryRun = false, spawnFn = spa
|
|
|
161
187
|
result = spawnFn(binary, args, { stdio: 'pipe', encoding: 'utf8' });
|
|
162
188
|
} catch (e) {
|
|
163
189
|
return {
|
|
190
|
+
server: server.name,
|
|
164
191
|
harness,
|
|
165
192
|
action: mode,
|
|
166
193
|
detected: true,
|
|
@@ -175,6 +202,7 @@ function registerMcp({ harness, mode = 'register', dryRun = false, spawnFn = spa
|
|
|
175
202
|
const stderr = (result && result.stderr) || '';
|
|
176
203
|
const exit_code = result ? result.status : null;
|
|
177
204
|
return {
|
|
205
|
+
server: server.name,
|
|
178
206
|
harness,
|
|
179
207
|
action: mode,
|
|
180
208
|
detected: true,
|
|
@@ -187,6 +215,58 @@ function registerMcp({ harness, mode = 'register', dryRun = false, spawnFn = spa
|
|
|
187
215
|
};
|
|
188
216
|
}
|
|
189
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Register all gdd MCP servers (MCP_SERVERS) with the given harness.
|
|
220
|
+
*
|
|
221
|
+
* The harness CLI presence is checked ONCE; if absent, no servers are
|
|
222
|
+
* registered. Otherwise each server in MCP_SERVERS is registered (idempotent
|
|
223
|
+
* per server). The return value keeps the original single-server fields at the
|
|
224
|
+
* top level (mirroring the primary gdd-mcp server) for back-compat, and adds a
|
|
225
|
+
* `servers` array carrying the per-server results.
|
|
226
|
+
*
|
|
227
|
+
* @param {object} opts
|
|
228
|
+
* @param {'claude'|'codex'} opts.harness
|
|
229
|
+
* @param {'register'|'unregister'|'detect'} [opts.mode='register']
|
|
230
|
+
* @param {boolean} [opts.dryRun=false]
|
|
231
|
+
* @param {Function} [opts.spawnFn] child_process.spawnSync substitute
|
|
232
|
+
* @returns {object} {harness, action, detected, command, applied,
|
|
233
|
+
* idempotent_skip, notice?, stdout?, stderr?,
|
|
234
|
+
* exit_code?, dry_run?, servers}
|
|
235
|
+
*/
|
|
236
|
+
function registerMcp({ harness, mode = 'register', dryRun = false, spawnFn = spawnSync } = {}) {
|
|
237
|
+
if (!HARNESSES[harness]) {
|
|
238
|
+
throw new Error('Unknown harness: ' + harness + ' (expected one of: ' + Object.keys(HARNESSES).join(', ') + ')');
|
|
239
|
+
}
|
|
240
|
+
if (mode !== 'register' && mode !== 'detect' && mode !== 'unregister') {
|
|
241
|
+
throw new Error('Unsupported mode: ' + mode);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Step 1 — detect harness CLI on PATH (once, for all servers).
|
|
245
|
+
if (!detectHarnessPresent(harness, spawnFn)) {
|
|
246
|
+
const names = MCP_SERVERS.map((s) => s.name).join(' + ');
|
|
247
|
+
return {
|
|
248
|
+
harness,
|
|
249
|
+
action: mode,
|
|
250
|
+
detected: false,
|
|
251
|
+
command: null,
|
|
252
|
+
applied: false,
|
|
253
|
+
idempotent_skip: false,
|
|
254
|
+
notice: harness + ' CLI not on PATH — skipping ' + names + ' registration',
|
|
255
|
+
servers: [],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Step 2 — register each server (idempotent per server).
|
|
260
|
+
const servers = MCP_SERVERS.map((server) =>
|
|
261
|
+
registerOneServer(harness, server, { mode, dryRun, spawnFn }),
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// Top-level fields mirror the primary (first) server for back-compat with
|
|
265
|
+
// the original single-server callers; `servers` carries the full detail.
|
|
266
|
+
const primary = servers[0];
|
|
267
|
+
return Object.assign({}, primary, { servers });
|
|
268
|
+
}
|
|
269
|
+
|
|
190
270
|
/**
|
|
191
271
|
* Detect overall MCP registration state across all known harnesses.
|
|
192
272
|
*
|
|
@@ -232,4 +312,5 @@ module.exports = {
|
|
|
232
312
|
buildHarnessCommand,
|
|
233
313
|
HARNESSES,
|
|
234
314
|
MCP_NAME,
|
|
315
|
+
MCP_SERVERS,
|
|
235
316
|
};
|