@hegemonart/get-design-done 1.58.1 → 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 +37 -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
|
@@ -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
|
};
|
|
@@ -29,6 +29,13 @@
|
|
|
29
29
|
"argument_hint": "[--retroactive] [--quick] [--no-reflect]",
|
|
30
30
|
"tools": "Read, Write, Task, Glob, Bash"
|
|
31
31
|
},
|
|
32
|
+
{
|
|
33
|
+
"name": "bandit-reset",
|
|
34
|
+
"description": "Confirm-then-reset the per-(agent, bin, delegate) bandit posterior - backs up .design/telemetry/posterior.json to posterior.json.bak, then clears it to a fresh empty envelope. Mutation companion to read-only bandit-status. Use when the posterior is corrupted/unparseable, after a major agent/skill roster change invalidates accumulated arms, or when you deliberately want to rebootstrap adaptive routing from informed priors.",
|
|
35
|
+
"argument_hint": "[--yes to skip the confirmation prompt]",
|
|
36
|
+
"tools": "Read, Write, Bash, AskUserQuestion",
|
|
37
|
+
"disable_model_invocation": true
|
|
38
|
+
},
|
|
32
39
|
{
|
|
33
40
|
"name": "bandit-status",
|
|
34
41
|
"description": "Surface read-only per-(agent, bin, delegate) bandit posterior snapshot - alpha/beta/mean/stddev/count/last-used per arm. Phase 27.5 (v1.27.5) diagnostic. Use when investigating 'why did the bandit pick tier X for agent Y?' or when verifying posterior convergence after enabling adaptive_mode: full.",
|
|
@@ -89,6 +89,11 @@ export interface AuditReport {
|
|
|
89
89
|
readonly connections: readonly ConnectionReport[];
|
|
90
90
|
readonly must_haves: readonly MustHaveReport[];
|
|
91
91
|
readonly baseline?: BaselineReport;
|
|
92
|
+
// `true` when there is no active cycle (.design/STATE.md absent). The audit
|
|
93
|
+
// then runs only the static checks that do not require cycle state and exits
|
|
94
|
+
// 0 with this flag set, rather than failing. Omitted (undefined) on a normal
|
|
95
|
+
// run with an active cycle.
|
|
96
|
+
readonly degraded?: boolean;
|
|
92
97
|
readonly summary: {
|
|
93
98
|
readonly connections_ok: boolean;
|
|
94
99
|
readonly must_haves_ok: boolean;
|
|
@@ -135,14 +140,23 @@ export async function auditCommand(
|
|
|
135
140
|
|
|
136
141
|
const cwd: string =
|
|
137
142
|
typeof flags['cwd'] === 'string' ? (flags['cwd'] as string) : process.cwd();
|
|
138
|
-
const
|
|
139
|
-
typeof flags['state-path'] === 'string' && (flags['state-path'] as string).length > 0
|
|
140
|
-
|
|
141
|
-
|
|
143
|
+
const explicitStatePath: boolean =
|
|
144
|
+
typeof flags['state-path'] === 'string' && (flags['state-path'] as string).length > 0;
|
|
145
|
+
const statePath: string = explicitStatePath
|
|
146
|
+
? resolvePath(cwd, flags['state-path'] as string)
|
|
147
|
+
: resolvePath(cwd, '.design', 'STATE.md');
|
|
142
148
|
|
|
143
149
|
if (!existsSync(statePath)) {
|
|
144
|
-
|
|
145
|
-
|
|
150
|
+
// An explicit --state-path that does not exist is an arg error: the
|
|
151
|
+
// caller pointed us at a specific file that should be present.
|
|
152
|
+
if (explicitStatePath) {
|
|
153
|
+
stderr.write(`gdd-sdk audit: STATE.md not found at ${statePath}\n`);
|
|
154
|
+
return 3;
|
|
155
|
+
}
|
|
156
|
+
// No active cycle (default .design/STATE.md absent). Graceful degrade:
|
|
157
|
+
// emit a clear message + a degraded report covering the static checks
|
|
158
|
+
// that do not require cycle state, then exit 0 — never throw.
|
|
159
|
+
return emitDegraded(flags, stdout, stderr);
|
|
146
160
|
}
|
|
147
161
|
|
|
148
162
|
const readFn = deps.readState ?? read;
|
|
@@ -218,6 +232,46 @@ export async function auditCommand(
|
|
|
218
232
|
return overallOk ? 0 : 1;
|
|
219
233
|
}
|
|
220
234
|
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// Degraded (no active cycle) path.
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Emit a degraded audit report when there is no active cycle (no
|
|
241
|
+
* `.design/STATE.md`). Connections + must-haves are sourced from STATE.md,
|
|
242
|
+
* so without a cycle there is nothing cycle-bound to evaluate; we report
|
|
243
|
+
* empty sets and exit 0. The `degraded` flag signals callers (and the JSON
|
|
244
|
+
* consumers) that this was a no-cycle run, not a clean active-cycle audit.
|
|
245
|
+
*/
|
|
246
|
+
function emitDegraded(
|
|
247
|
+
flags: Record<string, unknown>,
|
|
248
|
+
stdout: NodeJS.WritableStream,
|
|
249
|
+
stderr: NodeJS.WritableStream,
|
|
250
|
+
): number {
|
|
251
|
+
// Human-facing notice on stderr so JSON on stdout stays machine-parseable.
|
|
252
|
+
stderr.write('gdd-sdk audit: no active cycle — run /gdd:start\n');
|
|
253
|
+
|
|
254
|
+
const report: AuditReport = {
|
|
255
|
+
connections: Object.freeze([]),
|
|
256
|
+
must_haves: Object.freeze([]),
|
|
257
|
+
degraded: true,
|
|
258
|
+
summary: {
|
|
259
|
+
connections_ok: true,
|
|
260
|
+
must_haves_ok: true,
|
|
261
|
+
baseline_ok: true,
|
|
262
|
+
overall_ok: true,
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
if (flags['json'] === true) {
|
|
267
|
+
stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
268
|
+
} else {
|
|
269
|
+
stdout.write(renderHuman(report));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return 0;
|
|
273
|
+
}
|
|
274
|
+
|
|
221
275
|
// ---------------------------------------------------------------------------
|
|
222
276
|
// Baseline comparison.
|
|
223
277
|
// ---------------------------------------------------------------------------
|
|
@@ -352,6 +406,12 @@ function parseBaselineStateSync(raw: string): Pick<ParsedState, 'connections' |
|
|
|
352
406
|
|
|
353
407
|
function renderHuman(report: AuditReport): string {
|
|
354
408
|
const lines: string[] = [];
|
|
409
|
+
if (report.degraded === true) {
|
|
410
|
+
lines.push('audit: degraded (no active cycle — run /gdd:start)');
|
|
411
|
+
lines.push('');
|
|
412
|
+
lines.push('No active cycle: skipped connection + must-have checks.');
|
|
413
|
+
return lines.join('\n') + '\n';
|
|
414
|
+
}
|
|
355
415
|
lines.push(`audit: ${report.summary.overall_ok ? 'clean' : 'REGRESSIONS'}`);
|
|
356
416
|
lines.push('');
|
|
357
417
|
lines.push(`connections (${report.summary.connections_ok ? 'ok' : 'degraded'}):`);
|
package/sdk/cli/index.js
CHANGED
|
@@ -8774,11 +8774,15 @@ async function auditCommand(args, deps = {}) {
|
|
|
8774
8774
|
return 3;
|
|
8775
8775
|
}
|
|
8776
8776
|
const cwd = typeof flags["cwd"] === "string" ? flags["cwd"] : process.cwd();
|
|
8777
|
-
const
|
|
8777
|
+
const explicitStatePath = typeof flags["state-path"] === "string" && flags["state-path"].length > 0;
|
|
8778
|
+
const statePath = explicitStatePath ? (0, import_node_path17.resolve)(cwd, flags["state-path"]) : (0, import_node_path17.resolve)(cwd, ".design", "STATE.md");
|
|
8778
8779
|
if (!(0, import_node_fs17.existsSync)(statePath)) {
|
|
8779
|
-
|
|
8780
|
+
if (explicitStatePath) {
|
|
8781
|
+
stderr.write(`gdd-sdk audit: STATE.md not found at ${statePath}
|
|
8780
8782
|
`);
|
|
8781
|
-
|
|
8783
|
+
return 3;
|
|
8784
|
+
}
|
|
8785
|
+
return emitDegraded(flags, stdout, stderr);
|
|
8782
8786
|
}
|
|
8783
8787
|
const readFn = deps.readState ?? read;
|
|
8784
8788
|
let state;
|
|
@@ -8841,6 +8845,26 @@ async function auditCommand(args, deps = {}) {
|
|
|
8841
8845
|
}
|
|
8842
8846
|
return overallOk ? 0 : 1;
|
|
8843
8847
|
}
|
|
8848
|
+
function emitDegraded(flags, stdout, stderr) {
|
|
8849
|
+
stderr.write("gdd-sdk audit: no active cycle \u2014 run /gdd:start\n");
|
|
8850
|
+
const report = {
|
|
8851
|
+
connections: Object.freeze([]),
|
|
8852
|
+
must_haves: Object.freeze([]),
|
|
8853
|
+
degraded: true,
|
|
8854
|
+
summary: {
|
|
8855
|
+
connections_ok: true,
|
|
8856
|
+
must_haves_ok: true,
|
|
8857
|
+
baseline_ok: true,
|
|
8858
|
+
overall_ok: true
|
|
8859
|
+
}
|
|
8860
|
+
};
|
|
8861
|
+
if (flags["json"] === true) {
|
|
8862
|
+
stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
8863
|
+
} else {
|
|
8864
|
+
stdout.write(renderHuman(report));
|
|
8865
|
+
}
|
|
8866
|
+
return 0;
|
|
8867
|
+
}
|
|
8844
8868
|
function computeBaselineDrift(current, baselineDir) {
|
|
8845
8869
|
const baselinePath = (0, import_node_path17.resolve)(baselineDir, "STATE.md");
|
|
8846
8870
|
if (!(0, import_node_fs17.existsSync)(baselinePath)) {
|
|
@@ -8923,6 +8947,12 @@ function parseBaselineStateSync(raw) {
|
|
|
8923
8947
|
}
|
|
8924
8948
|
function renderHuman(report) {
|
|
8925
8949
|
const lines = [];
|
|
8950
|
+
if (report.degraded === true) {
|
|
8951
|
+
lines.push("audit: degraded (no active cycle \u2014 run /gdd:start)");
|
|
8952
|
+
lines.push("");
|
|
8953
|
+
lines.push("No active cycle: skipped connection + must-have checks.");
|
|
8954
|
+
return lines.join("\n") + "\n";
|
|
8955
|
+
}
|
|
8926
8956
|
lines.push(`audit: ${report.summary.overall_ok ? "clean" : "REGRESSIONS"}`);
|
|
8927
8957
|
lines.push("");
|
|
8928
8958
|
lines.push(`connections (${report.summary.connections_ok ? "ok" : "degraded"}):`);
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gdd-bandit-reset
|
|
3
|
+
description: "Confirm-then-reset the per-(agent, bin, delegate) bandit posterior - backs up .design/telemetry/posterior.json to posterior.json.bak, then clears it to a fresh empty envelope. Mutation companion to read-only bandit-status. Use when the posterior is corrupted/unparseable, after a major agent/skill roster change invalidates accumulated arms, or when you deliberately want to rebootstrap adaptive routing from informed priors."
|
|
4
|
+
argument-hint: "[--yes to skip the confirmation prompt]"
|
|
5
|
+
tools: Read, Write, Bash, AskUserQuestion
|
|
6
|
+
disable-model-invocation: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# gdd-bandit-reset
|
|
10
|
+
|
|
11
|
+
## Role
|
|
12
|
+
|
|
13
|
+
You are a deterministic, destructive maintenance skill. You are the ONLY skill that clears the bandit posterior - the mutation companion to read-only `/gdd:bandit-status`. You read the posterior path declared by `scripts/lib/bandit-router.cjs`'s `DEFAULT_POSTERIOR_PATH` (`.design/telemetry/posterior.json`), REQUIRE explicit confirmation, back the file up to `posterior.json.bak`, then overwrite it with a fresh empty envelope so the next bandit pull rebootstraps from informed priors. See `./reference/bandit-integration.md` for setup, interpretation, and convergence guidance.
|
|
14
|
+
|
|
15
|
+
## Invocation Contract
|
|
16
|
+
|
|
17
|
+
- **Input**: optional `--yes` to skip the interactive confirmation (for non-interactive/automated runs).
|
|
18
|
+
- **Output**: a Markdown reset receipt to stdout (backup path + arms cleared + envelope written).
|
|
19
|
+
|
|
20
|
+
## Procedure
|
|
21
|
+
|
|
22
|
+
### 1. Locate the posterior file
|
|
23
|
+
|
|
24
|
+
Read `.design/telemetry/posterior.json` (path declared by `scripts/lib/bandit-router.cjs`'s `DEFAULT_POSTERIOR_PATH` - never hardcode a different path). Missing → nothing to reset; emit and skip to Section 5 (Record):
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
## Bandit Posterior Reset
|
|
28
|
+
|
|
29
|
+
No posterior file found at `.design/telemetry/posterior.json` — nothing to reset.
|
|
30
|
+
|
|
31
|
+
The next bandit pull with `adaptive_mode: full` will bootstrap a fresh posterior from informed priors. See `reference/bandit-integration.md`.
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
If present, count the arms (`arms.length`, treating a missing/non-array `arms` as `0`) so the confirmation and receipt can report what will be cleared. A corrupted/unparseable file is still resettable - report `arms: unknown (file unparseable)` and continue.
|
|
35
|
+
|
|
36
|
+
### 2. Require explicit confirmation
|
|
37
|
+
|
|
38
|
+
This is a DESTRUCTIVE operation. Do NOT proceed without confirmation.
|
|
39
|
+
|
|
40
|
+
- If `--yes` was passed, skip straight to Section 3.
|
|
41
|
+
- Otherwise, prompt via AskUserQuestion: "Reset the bandit posterior at `.design/telemetry/posterior.json`? This clears <N> learned arms. A backup will be written to `posterior.json.bak` first." with options **Reset** and **Cancel**.
|
|
42
|
+
- On **Cancel** (or any non-affirmative answer), abort WITHOUT touching either the posterior or the backup, and emit:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
## Bandit Posterior Reset — Cancelled
|
|
46
|
+
|
|
47
|
+
No changes made. The posterior at `.design/telemetry/posterior.json` is untouched (<N> arms).
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then skip to Section 5 (Record) with `reset: false`.
|
|
51
|
+
|
|
52
|
+
### 3. Back up the current posterior
|
|
53
|
+
|
|
54
|
+
Copy the live posterior to `.design/telemetry/posterior.json.bak` (sibling backup) BEFORE clearing it, so the previous state is always recoverable. Overwrite any existing `.bak` from a prior reset. If the backup write fails, ABORT before clearing; never clear without a successful backup.
|
|
55
|
+
|
|
56
|
+
### 4. Clear to a fresh empty envelope
|
|
57
|
+
|
|
58
|
+
Overwrite `.design/telemetry/posterior.json` with a fresh empty envelope matching `scripts/lib/bandit-router.cjs`'s `loadPosterior()` shape (`SCHEMA_VERSION` = `1.0.0`, current ISO `generated_at`, empty `arms`):
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"schema_version": "1.0.0",
|
|
63
|
+
"generated_at": "<ISO>",
|
|
64
|
+
"arms": []
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Write atomically where possible (`.tmp` + rename, mirroring `savePosterior()`). Then emit the receipt:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
## Bandit Posterior Reset
|
|
72
|
+
|
|
73
|
+
Posterior cleared. The next bandit pull with `adaptive_mode: full` will rebootstrap from informed priors.
|
|
74
|
+
|
|
75
|
+
- Backup: `.design/telemetry/posterior.json.bak`
|
|
76
|
+
- Arms cleared: <N>
|
|
77
|
+
- Fresh envelope: `.design/telemetry/posterior.json` (schema_version 1.0.0, 0 arms)
|
|
78
|
+
|
|
79
|
+
Restore the previous state with: `cp .design/telemetry/posterior.json.bak .design/telemetry/posterior.json`
|
|
80
|
+
Verify the cleared state with `/gdd:bandit-status`. See `reference/bandit-integration.md`.
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 5. Record
|
|
84
|
+
|
|
85
|
+
Append one JSONL line to `.design/skill-records.jsonl`: `{"skill":"gdd-bandit-reset","ts":"<ISO>","reset":<bool>,"arms_cleared":<count>,"backup_written":<bool>}`. The skill mutates ONLY the posterior (+ its `.bak`) and appends to skill-records.jsonl (telemetry); it touches no other state.
|
|
86
|
+
|
|
87
|
+
## Cross-references
|
|
88
|
+
|
|
89
|
+
- `/gdd:bandit-status` - read-only companion; inspect the posterior before/after a reset.
|
|
90
|
+
- `./reference/bandit-integration.md` - operator guide; interpretation patterns and when a reset is warranted.
|
|
91
|
+
- `scripts/lib/bandit-router.cjs` - posterior shape, `DEFAULT_POSTERIOR_PATH`, `SCHEMA_VERSION`, `loadPosterior()`, `savePosterior()`, `reset()`.
|