@hegemonart/get-design-done 1.19.6 → 1.20.0
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 +2 -2
- package/CHANGELOG.md +60 -0
- package/README.md +12 -0
- package/agents/design-reflector.md +13 -0
- package/connections/connections.md +3 -0
- package/connections/figma.md +2 -0
- package/connections/gdd-state.md +186 -0
- package/hooks/budget-enforcer.ts +716 -0
- package/hooks/context-exhaustion.ts +251 -0
- package/hooks/gdd-read-injection-scanner.ts +172 -0
- package/hooks/hooks.json +3 -3
- package/package.json +19 -6
- package/reference/config-schema.md +2 -2
- package/reference/error-recovery.md +58 -0
- package/reference/registry.json +7 -0
- package/reference/schemas/budget.schema.json +42 -0
- package/reference/schemas/events.schema.json +55 -0
- package/reference/schemas/generated.d.ts +419 -0
- package/reference/schemas/iteration-budget.schema.json +36 -0
- package/reference/schemas/mcp-gdd-state-tools.schema.json +89 -0
- package/reference/schemas/rate-limits.schema.json +31 -0
- package/scripts/aggregate-agent-metrics.ts +282 -0
- package/scripts/codegen-schema-types.ts +149 -0
- package/scripts/lib/error-classifier.cjs +232 -0
- package/scripts/lib/error-classifier.d.cts +44 -0
- package/scripts/lib/event-stream/emitter.ts +88 -0
- package/scripts/lib/event-stream/index.ts +154 -0
- package/scripts/lib/event-stream/types.ts +127 -0
- package/scripts/lib/event-stream/writer.ts +154 -0
- package/scripts/lib/gdd-errors/classification.ts +124 -0
- package/scripts/lib/gdd-errors/index.ts +218 -0
- package/scripts/lib/gdd-state/gates.ts +216 -0
- package/scripts/lib/gdd-state/index.ts +167 -0
- package/scripts/lib/gdd-state/lockfile.ts +232 -0
- package/scripts/lib/gdd-state/mutator.ts +574 -0
- package/scripts/lib/gdd-state/parser.ts +523 -0
- package/scripts/lib/gdd-state/types.ts +179 -0
- package/scripts/lib/iteration-budget.cjs +205 -0
- package/scripts/lib/iteration-budget.d.cts +32 -0
- package/scripts/lib/jittered-backoff.cjs +112 -0
- package/scripts/lib/jittered-backoff.d.cts +38 -0
- package/scripts/lib/lockfile.cjs +177 -0
- package/scripts/lib/lockfile.d.cts +21 -0
- package/scripts/lib/prompt-sanitizer/index.ts +435 -0
- package/scripts/lib/prompt-sanitizer/patterns.ts +173 -0
- package/scripts/lib/rate-guard.cjs +365 -0
- package/scripts/lib/rate-guard.d.cts +38 -0
- package/scripts/mcp-servers/gdd-state/schemas/add_blocker.schema.json +67 -0
- package/scripts/mcp-servers/gdd-state/schemas/add_decision.schema.json +68 -0
- package/scripts/mcp-servers/gdd-state/schemas/add_must_have.schema.json +68 -0
- package/scripts/mcp-servers/gdd-state/schemas/checkpoint.schema.json +51 -0
- package/scripts/mcp-servers/gdd-state/schemas/frontmatter_update.schema.json +62 -0
- package/scripts/mcp-servers/gdd-state/schemas/get.schema.json +51 -0
- package/scripts/mcp-servers/gdd-state/schemas/probe_connections.schema.json +75 -0
- package/scripts/mcp-servers/gdd-state/schemas/resolve_blocker.schema.json +66 -0
- package/scripts/mcp-servers/gdd-state/schemas/set_status.schema.json +47 -0
- package/scripts/mcp-servers/gdd-state/schemas/transition_stage.schema.json +70 -0
- package/scripts/mcp-servers/gdd-state/schemas/update_progress.schema.json +58 -0
- package/scripts/mcp-servers/gdd-state/server.ts +288 -0
- package/scripts/mcp-servers/gdd-state/tools/add_blocker.ts +72 -0
- package/scripts/mcp-servers/gdd-state/tools/add_decision.ts +89 -0
- package/scripts/mcp-servers/gdd-state/tools/add_must_have.ts +113 -0
- package/scripts/mcp-servers/gdd-state/tools/checkpoint.ts +60 -0
- package/scripts/mcp-servers/gdd-state/tools/frontmatter_update.ts +91 -0
- package/scripts/mcp-servers/gdd-state/tools/get.ts +51 -0
- package/scripts/mcp-servers/gdd-state/tools/index.ts +51 -0
- package/scripts/mcp-servers/gdd-state/tools/probe_connections.ts +73 -0
- package/scripts/mcp-servers/gdd-state/tools/resolve_blocker.ts +84 -0
- package/scripts/mcp-servers/gdd-state/tools/set_status.ts +54 -0
- package/scripts/mcp-servers/gdd-state/tools/shared.ts +194 -0
- package/scripts/mcp-servers/gdd-state/tools/transition_stage.ts +80 -0
- package/scripts/mcp-servers/gdd-state/tools/update_progress.ts +81 -0
- package/scripts/validate-frontmatter.ts +114 -0
- package/scripts/validate-schemas.ts +401 -0
- package/skills/brief/SKILL.md +15 -6
- package/skills/design/SKILL.md +31 -13
- package/skills/explore/SKILL.md +41 -17
- package/skills/health/SKILL.md +15 -4
- package/skills/optimize/SKILL.md +3 -3
- package/skills/pause/SKILL.md +16 -10
- package/skills/plan/SKILL.md +33 -17
- package/skills/progress/SKILL.md +15 -11
- package/skills/resume/SKILL.md +19 -10
- package/skills/settings/SKILL.md +11 -3
- package/skills/todo/SKILL.md +12 -3
- package/skills/verify/SKILL.md +65 -29
- package/hooks/budget-enforcer.js +0 -329
- package/hooks/context-exhaustion.js +0 -127
- package/hooks/gdd-read-injection-scanner.js +0 -39
- package/scripts/aggregate-agent-metrics.js +0 -173
- package/scripts/validate-frontmatter.cjs +0 -68
- package/scripts/validate-schemas.cjs +0 -242
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// scripts/mcp-servers/gdd-state/tools/frontmatter_update.ts
|
|
2
|
+
//
|
|
3
|
+
// Tool: gdd_state__frontmatter_update
|
|
4
|
+
// Purpose: Patch one or more frontmatter fields. Rejects two forbidden
|
|
5
|
+
// keys:
|
|
6
|
+
// * `pipeline_state_version` — immutable version pin
|
|
7
|
+
// * `stage` — must use transition_stage (which runs
|
|
8
|
+
// gates and emits state.transition)
|
|
9
|
+
// Emits state.mutation on success.
|
|
10
|
+
|
|
11
|
+
import { mutate } from '../../../lib/gdd-state/index.ts';
|
|
12
|
+
import {
|
|
13
|
+
emitStateMutation,
|
|
14
|
+
errorResponse,
|
|
15
|
+
okResponse,
|
|
16
|
+
resolveStatePath,
|
|
17
|
+
throwValidation,
|
|
18
|
+
type ToolResponse,
|
|
19
|
+
} from './shared.ts';
|
|
20
|
+
|
|
21
|
+
export const name = 'gdd_state__frontmatter_update';
|
|
22
|
+
export const schemaPath = '../schemas/frontmatter_update.schema.json';
|
|
23
|
+
|
|
24
|
+
export interface FrontmatterUpdateInput {
|
|
25
|
+
patch: Record<string, string | number | boolean>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Keys that this tool explicitly refuses to modify. */
|
|
29
|
+
export const FORBIDDEN_KEYS: ReadonlySet<string> = new Set([
|
|
30
|
+
'pipeline_state_version',
|
|
31
|
+
'stage',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
/** Accept only scalar values (string / number / boolean). */
|
|
35
|
+
function isScalar(v: unknown): v is string | number | boolean {
|
|
36
|
+
return typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function handle(input: unknown): Promise<ToolResponse> {
|
|
40
|
+
try {
|
|
41
|
+
const typed = (input ?? {}) as FrontmatterUpdateInput;
|
|
42
|
+
if (
|
|
43
|
+
typed.patch === undefined ||
|
|
44
|
+
typed.patch === null ||
|
|
45
|
+
typeof typed.patch !== 'object'
|
|
46
|
+
) {
|
|
47
|
+
throwValidation(
|
|
48
|
+
'MISSING_FIELD',
|
|
49
|
+
'frontmatter_update requires an object patch',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const keys = Object.keys(typed.patch);
|
|
53
|
+
if (keys.length === 0) {
|
|
54
|
+
throwValidation('EMPTY_PATCH', 'patch must contain at least one key');
|
|
55
|
+
}
|
|
56
|
+
for (const k of keys) {
|
|
57
|
+
if (FORBIDDEN_KEYS.has(k)) {
|
|
58
|
+
throwValidation(
|
|
59
|
+
'FORBIDDEN_KEY',
|
|
60
|
+
`patching "${k}" is not allowed via frontmatter_update (stage must go through transition_stage)`,
|
|
61
|
+
{ key: k },
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const value = (typed.patch as Record<string, unknown>)[k];
|
|
65
|
+
if (!isScalar(value)) {
|
|
66
|
+
throwValidation(
|
|
67
|
+
'NON_SCALAR_VALUE',
|
|
68
|
+
`patch value for "${k}" must be a string, number, or boolean`,
|
|
69
|
+
{ key: k },
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const path = resolveStatePath();
|
|
75
|
+
const diff: Record<string, { before: unknown; after: unknown }> = {};
|
|
76
|
+
const after = await mutate(path, (s) => {
|
|
77
|
+
for (const k of keys) {
|
|
78
|
+
const before = s.frontmatter[k];
|
|
79
|
+
const value = (typed.patch as Record<string, unknown>)[k];
|
|
80
|
+
// We restored the scalar invariant above, so this narrow is safe.
|
|
81
|
+
s.frontmatter[k] = value;
|
|
82
|
+
diff[k] = { before, after: value };
|
|
83
|
+
}
|
|
84
|
+
return s;
|
|
85
|
+
});
|
|
86
|
+
emitStateMutation(name, { diff }, after);
|
|
87
|
+
return okResponse({ frontmatter: after.frontmatter, keys });
|
|
88
|
+
} catch (err) {
|
|
89
|
+
return errorResponse(err);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// scripts/mcp-servers/gdd-state/tools/get.ts
|
|
2
|
+
//
|
|
3
|
+
// Tool: gdd_state__get
|
|
4
|
+
// Purpose: Read current STATE.md (parsed). Read-only; does NOT emit an
|
|
5
|
+
// event. Optionally projects a subset of fields when `input.fields` is
|
|
6
|
+
// provided; unknown field names are silently ignored so callers can pass
|
|
7
|
+
// a broad list without pre-flight knowledge of ParsedState shape.
|
|
8
|
+
|
|
9
|
+
import { read } from '../../../lib/gdd-state/index.ts';
|
|
10
|
+
import type { ParsedState } from '../../../lib/gdd-state/types.ts';
|
|
11
|
+
import {
|
|
12
|
+
errorResponse,
|
|
13
|
+
okResponse,
|
|
14
|
+
resolveStatePath,
|
|
15
|
+
type ToolResponse,
|
|
16
|
+
} from './shared.ts';
|
|
17
|
+
|
|
18
|
+
export const name = 'gdd_state__get';
|
|
19
|
+
export const schemaPath = '../schemas/get.schema.json';
|
|
20
|
+
|
|
21
|
+
export interface GetInput {
|
|
22
|
+
fields?: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Project a subset of top-level keys from `state`. Unknown keys are
|
|
27
|
+
* omitted from the output but not treated as an error — the caller may
|
|
28
|
+
* pass a broad list without knowing ParsedState's shape.
|
|
29
|
+
*/
|
|
30
|
+
function project(state: ParsedState, fields: string[]): Record<string, unknown> {
|
|
31
|
+
const allowed = new Set(fields);
|
|
32
|
+
const out: Record<string, unknown> = {};
|
|
33
|
+
for (const [k, v] of Object.entries(state)) {
|
|
34
|
+
if (allowed.has(k)) out[k] = v;
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function handle(input: unknown): Promise<ToolResponse> {
|
|
40
|
+
try {
|
|
41
|
+
const typed = (input ?? {}) as GetInput;
|
|
42
|
+
const path = resolveStatePath();
|
|
43
|
+
const state = await read(path);
|
|
44
|
+
const fields = Array.isArray(typed.fields) ? typed.fields : null;
|
|
45
|
+
const stateOut =
|
|
46
|
+
fields === null || fields.length === 0 ? state : project(state, fields);
|
|
47
|
+
return okResponse({ state: stateOut, path });
|
|
48
|
+
} catch (err) {
|
|
49
|
+
return errorResponse(err);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// scripts/mcp-servers/gdd-state/tools/index.ts
|
|
2
|
+
//
|
|
3
|
+
// Aggregates the 11 tool handlers. `server.ts` imports from here so the
|
|
4
|
+
// registration loop is one call per tool and the tool set is expanded
|
|
5
|
+
// by editing a single line.
|
|
6
|
+
|
|
7
|
+
import * as get from './get.ts';
|
|
8
|
+
import * as update_progress from './update_progress.ts';
|
|
9
|
+
import * as transition_stage from './transition_stage.ts';
|
|
10
|
+
import * as add_blocker from './add_blocker.ts';
|
|
11
|
+
import * as resolve_blocker from './resolve_blocker.ts';
|
|
12
|
+
import * as add_decision from './add_decision.ts';
|
|
13
|
+
import * as add_must_have from './add_must_have.ts';
|
|
14
|
+
import * as set_status from './set_status.ts';
|
|
15
|
+
import * as checkpoint from './checkpoint.ts';
|
|
16
|
+
import * as probe_connections from './probe_connections.ts';
|
|
17
|
+
import * as frontmatter_update from './frontmatter_update.ts';
|
|
18
|
+
|
|
19
|
+
import type { ToolResponse } from './shared.ts';
|
|
20
|
+
|
|
21
|
+
export interface ToolModule {
|
|
22
|
+
/** Public tool name exposed via MCP (e.g. "gdd_state__add_decision"). */
|
|
23
|
+
name: string;
|
|
24
|
+
/** Path to the input/output Draft-07 JSON Schema, relative to the module. */
|
|
25
|
+
schemaPath: string;
|
|
26
|
+
/** Executes the tool. Never throws — always returns a ToolResponse. */
|
|
27
|
+
handle: (input: unknown) => Promise<ToolResponse>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Canonical tool registry for the server. Order is cosmetic — all 11
|
|
32
|
+
* tools are advertised equivalently in `tools/list`. Kept alphabetical
|
|
33
|
+
* after `get` for ease of scanning.
|
|
34
|
+
*/
|
|
35
|
+
export const TOOL_MODULES: readonly ToolModule[] = [
|
|
36
|
+
get,
|
|
37
|
+
add_blocker,
|
|
38
|
+
add_decision,
|
|
39
|
+
add_must_have,
|
|
40
|
+
checkpoint,
|
|
41
|
+
frontmatter_update,
|
|
42
|
+
probe_connections,
|
|
43
|
+
resolve_blocker,
|
|
44
|
+
set_status,
|
|
45
|
+
transition_stage,
|
|
46
|
+
update_progress,
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
49
|
+
/** Canonical count. The plan locks this at 11 — if you add a tool, update
|
|
50
|
+
* the plan, the combined schema, and the connection spec. */
|
|
51
|
+
export const TOOL_COUNT = TOOL_MODULES.length;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// scripts/mcp-servers/gdd-state/tools/probe_connections.ts
|
|
2
|
+
//
|
|
3
|
+
// Tool: gdd_state__probe_connections
|
|
4
|
+
// Purpose: Merge probe results into <connections>. Overwrites keys
|
|
5
|
+
// present in the input; DOES NOT delete keys not in the input (plan
|
|
6
|
+
// contract). Emits state.mutation on success.
|
|
7
|
+
|
|
8
|
+
import { mutate } from '../../../lib/gdd-state/index.ts';
|
|
9
|
+
import {
|
|
10
|
+
isConnectionStatus,
|
|
11
|
+
type ConnectionStatus,
|
|
12
|
+
} from '../../../lib/gdd-state/types.ts';
|
|
13
|
+
import {
|
|
14
|
+
emitStateMutation,
|
|
15
|
+
errorResponse,
|
|
16
|
+
okResponse,
|
|
17
|
+
resolveStatePath,
|
|
18
|
+
throwValidation,
|
|
19
|
+
type ToolResponse,
|
|
20
|
+
} from './shared.ts';
|
|
21
|
+
|
|
22
|
+
export const name = 'gdd_state__probe_connections';
|
|
23
|
+
export const schemaPath = '../schemas/probe_connections.schema.json';
|
|
24
|
+
|
|
25
|
+
export interface ProbeConnectionsInput {
|
|
26
|
+
probe_results: Array<{ name: string; status: ConnectionStatus }>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function handle(input: unknown): Promise<ToolResponse> {
|
|
30
|
+
try {
|
|
31
|
+
const typed = (input ?? {}) as ProbeConnectionsInput;
|
|
32
|
+
if (!Array.isArray(typed.probe_results) || typed.probe_results.length === 0) {
|
|
33
|
+
throwValidation(
|
|
34
|
+
'MISSING_FIELD',
|
|
35
|
+
'probe_connections requires a non-empty probe_results array',
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
for (const p of typed.probe_results) {
|
|
39
|
+
if (!p || typeof p.name !== 'string' || p.name.length === 0) {
|
|
40
|
+
throwValidation(
|
|
41
|
+
'PROBE_RESULT_NAME',
|
|
42
|
+
'each probe_result must have a non-empty name',
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (!isConnectionStatus(p.status)) {
|
|
46
|
+
throwValidation(
|
|
47
|
+
'PROBE_RESULT_STATUS',
|
|
48
|
+
`status "${String(p.status)}" is not one of available/unavailable/not_configured`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const path = resolveStatePath();
|
|
54
|
+
const updated: string[] = [];
|
|
55
|
+
const diff: Record<string, { before: string | null; after: string }> = {};
|
|
56
|
+
const after = await mutate(path, (s) => {
|
|
57
|
+
for (const p of typed.probe_results) {
|
|
58
|
+
const before: string | null =
|
|
59
|
+
Object.prototype.hasOwnProperty.call(s.connections, p.name)
|
|
60
|
+
? (s.connections[p.name] as string)
|
|
61
|
+
: null;
|
|
62
|
+
s.connections[p.name] = p.status;
|
|
63
|
+
updated.push(p.name);
|
|
64
|
+
diff[p.name] = { before, after: p.status };
|
|
65
|
+
}
|
|
66
|
+
return s;
|
|
67
|
+
});
|
|
68
|
+
emitStateMutation(name, { diff }, after);
|
|
69
|
+
return okResponse({ updated, connections: after.connections });
|
|
70
|
+
} catch (err) {
|
|
71
|
+
return errorResponse(err);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// scripts/mcp-servers/gdd-state/tools/resolve_blocker.ts
|
|
2
|
+
//
|
|
3
|
+
// Tool: gdd_state__resolve_blocker
|
|
4
|
+
// Purpose: Remove one entry from <blockers> by 0-based index OR by
|
|
5
|
+
// exact text match (first match). Returns operation_failed when no row
|
|
6
|
+
// matches — input is well-formed, but the operation cannot complete in
|
|
7
|
+
// the current state. Emits state.mutation on successful removal.
|
|
8
|
+
|
|
9
|
+
import { mutate } from '../../../lib/gdd-state/index.ts';
|
|
10
|
+
import type { Blocker } from '../../../lib/gdd-state/types.ts';
|
|
11
|
+
import {
|
|
12
|
+
emitStateMutation,
|
|
13
|
+
errorResponse,
|
|
14
|
+
okResponse,
|
|
15
|
+
operationFailed,
|
|
16
|
+
resolveStatePath,
|
|
17
|
+
throwValidation,
|
|
18
|
+
type ToolResponse,
|
|
19
|
+
} from './shared.ts';
|
|
20
|
+
|
|
21
|
+
export const name = 'gdd_state__resolve_blocker';
|
|
22
|
+
export const schemaPath = '../schemas/resolve_blocker.schema.json';
|
|
23
|
+
|
|
24
|
+
export interface ResolveBlockerInput {
|
|
25
|
+
index?: number;
|
|
26
|
+
text?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function handle(input: unknown): Promise<ToolResponse> {
|
|
30
|
+
try {
|
|
31
|
+
const typed = (input ?? {}) as ResolveBlockerInput;
|
|
32
|
+
const hasIndex = typeof typed.index === 'number';
|
|
33
|
+
const hasText = typeof typed.text === 'string' && typed.text.length > 0;
|
|
34
|
+
if (hasIndex === hasText) {
|
|
35
|
+
throwValidation(
|
|
36
|
+
'ONEOF_REQUIRED',
|
|
37
|
+
'resolve_blocker requires exactly one of: index OR text',
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
if (hasIndex && (typed.index as number) < 0) {
|
|
41
|
+
throwValidation('INDEX_NEGATIVE', 'index must be >= 0');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const path = resolveStatePath();
|
|
45
|
+
let removed: Blocker | null = null;
|
|
46
|
+
let countAfter = 0;
|
|
47
|
+
const after = await mutate(path, (s) => {
|
|
48
|
+
if (hasIndex) {
|
|
49
|
+
const idx = typed.index as number;
|
|
50
|
+
if (idx >= s.blockers.length) {
|
|
51
|
+
operationFailed(
|
|
52
|
+
'BLOCKER_NOT_FOUND',
|
|
53
|
+
`no blocker at index ${idx} (length=${s.blockers.length})`,
|
|
54
|
+
{ index: idx, length: s.blockers.length },
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const [deleted] = s.blockers.splice(idx, 1);
|
|
58
|
+
removed = deleted ?? null;
|
|
59
|
+
} else {
|
|
60
|
+
const target = typed.text as string;
|
|
61
|
+
const idx = s.blockers.findIndex((b) => b.text === target);
|
|
62
|
+
if (idx === -1) {
|
|
63
|
+
operationFailed(
|
|
64
|
+
'BLOCKER_NOT_FOUND',
|
|
65
|
+
`no blocker matches text "${target}"`,
|
|
66
|
+
{ text: target },
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const [deleted] = s.blockers.splice(idx, 1);
|
|
70
|
+
removed = deleted ?? null;
|
|
71
|
+
}
|
|
72
|
+
countAfter = s.blockers.length;
|
|
73
|
+
return s;
|
|
74
|
+
});
|
|
75
|
+
if (removed === null) {
|
|
76
|
+
// Unreachable — mutate's fn either removed a row or threw.
|
|
77
|
+
operationFailed('BLOCKER_NOT_FOUND', 'no blocker was removed (unreachable)');
|
|
78
|
+
}
|
|
79
|
+
emitStateMutation(name, { removed, count: countAfter }, after);
|
|
80
|
+
return okResponse({ removed, count: countAfter });
|
|
81
|
+
} catch (err) {
|
|
82
|
+
return errorResponse(err);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// scripts/mcp-servers/gdd-state/tools/set_status.ts
|
|
2
|
+
//
|
|
3
|
+
// Tool: gdd_state__set_status
|
|
4
|
+
// Purpose: Update <position>.status. Thin convenience wrapper over
|
|
5
|
+
// update_progress — kept separate so skill prose can call the narrowest
|
|
6
|
+
// tool possible (reduces mis-writes to task_progress). Emits state.mutation.
|
|
7
|
+
|
|
8
|
+
import { mutate } from '../../../lib/gdd-state/index.ts';
|
|
9
|
+
import {
|
|
10
|
+
emitStateMutation,
|
|
11
|
+
errorResponse,
|
|
12
|
+
okResponse,
|
|
13
|
+
resolveStatePath,
|
|
14
|
+
throwValidation,
|
|
15
|
+
type ToolResponse,
|
|
16
|
+
} from './shared.ts';
|
|
17
|
+
|
|
18
|
+
export const name = 'gdd_state__set_status';
|
|
19
|
+
export const schemaPath = '../schemas/set_status.schema.json';
|
|
20
|
+
|
|
21
|
+
export interface SetStatusInput {
|
|
22
|
+
status: 'initialized' | 'in_progress' | 'completed' | 'blocked';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const STATUSES = new Set([
|
|
26
|
+
'initialized',
|
|
27
|
+
'in_progress',
|
|
28
|
+
'completed',
|
|
29
|
+
'blocked',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
export async function handle(input: unknown): Promise<ToolResponse> {
|
|
33
|
+
try {
|
|
34
|
+
const typed = (input ?? {}) as SetStatusInput;
|
|
35
|
+
if (typeof typed.status !== 'string' || !STATUSES.has(typed.status)) {
|
|
36
|
+
throwValidation(
|
|
37
|
+
'STATUS_INVALID',
|
|
38
|
+
`status "${String(typed.status)}" is not one of initialized/in_progress/completed/blocked`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const path = resolveStatePath();
|
|
43
|
+
const diff: Record<string, unknown> = {};
|
|
44
|
+
const after = await mutate(path, (s) => {
|
|
45
|
+
diff['status'] = { before: s.position.status, after: typed.status };
|
|
46
|
+
s.position.status = typed.status;
|
|
47
|
+
return s;
|
|
48
|
+
});
|
|
49
|
+
emitStateMutation(name, diff, after);
|
|
50
|
+
return okResponse({ status: after.position.status });
|
|
51
|
+
} catch (err) {
|
|
52
|
+
return errorResponse(err);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// scripts/mcp-servers/gdd-state/tools/shared.ts
|
|
2
|
+
//
|
|
3
|
+
// Shared types + helpers for the 11 gdd-state tool handlers (Plan 20-05).
|
|
4
|
+
// Every handler returns one of:
|
|
5
|
+
//
|
|
6
|
+
// { success: true, data: <tool-specific> }
|
|
7
|
+
// { success: false, error: { code, message, kind, context? } }
|
|
8
|
+
//
|
|
9
|
+
// Handlers NEVER throw out to the harness — every catch-all funnels
|
|
10
|
+
// through `toToolError()` from `scripts/lib/gdd-errors/classification.ts`.
|
|
11
|
+
// This mirrors the invariant in the plan: "Tool errors are returned as
|
|
12
|
+
// {success:false, error} — handlers never propagate exceptions."
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
ValidationError,
|
|
16
|
+
OperationFailedError,
|
|
17
|
+
} from '../../../lib/gdd-errors/index.ts';
|
|
18
|
+
import { toToolError } from '../../../lib/gdd-errors/classification.ts';
|
|
19
|
+
import type { ToolErrorPayload } from '../../../lib/gdd-errors/classification.ts';
|
|
20
|
+
import {
|
|
21
|
+
appendEvent,
|
|
22
|
+
type BaseEvent,
|
|
23
|
+
type StateMutationEvent,
|
|
24
|
+
type StateTransitionEvent,
|
|
25
|
+
} from '../../../lib/event-stream/index.ts';
|
|
26
|
+
import type { ParsedState, Stage } from '../../../lib/gdd-state/types.ts';
|
|
27
|
+
|
|
28
|
+
/** Public tool-handler response shape (consistent across all 11 tools). */
|
|
29
|
+
export type ToolResponse =
|
|
30
|
+
| { success: true; data: Record<string, unknown> }
|
|
31
|
+
| { success: false; error: ToolErrorPayload['error'] };
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Session-id generator. The MCP server stamps a single session ID per
|
|
35
|
+
* process (via `getSessionId()`) so every event emitted by that server
|
|
36
|
+
* run correlates to a single pipeline session. The generator produces a
|
|
37
|
+
* sortable, opaque string — `gdd-mcp-<iso>-<pid>`.
|
|
38
|
+
*/
|
|
39
|
+
export function makeSessionId(): string {
|
|
40
|
+
const iso = new Date().toISOString().replace(/[:.]/g, '-');
|
|
41
|
+
return `gdd-mcp-${iso}-${process.pid}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let CACHED_SESSION_ID: string | null = null;
|
|
45
|
+
|
|
46
|
+
/** Return the session id for this process, generating it lazily. */
|
|
47
|
+
export function getSessionId(): string {
|
|
48
|
+
if (CACHED_SESSION_ID === null) CACHED_SESSION_ID = makeSessionId();
|
|
49
|
+
return CACHED_SESSION_ID;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the target STATE.md path from the environment. Mirrors the
|
|
54
|
+
* plan's contract: `process.env.GDD_STATE_PATH ?? .design/STATE.md`.
|
|
55
|
+
*
|
|
56
|
+
* Resolution is relative to `process.cwd()` when the env override is
|
|
57
|
+
* missing or relative. Tests and the server both call this so the
|
|
58
|
+
* resolution logic stays in one place.
|
|
59
|
+
*/
|
|
60
|
+
export function resolveStatePath(): string {
|
|
61
|
+
const override = process.env['GDD_STATE_PATH'];
|
|
62
|
+
if (typeof override === 'string' && override.length > 0) return override;
|
|
63
|
+
return '.design/STATE.md';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Narrow helper: is this a well-known Stage string? */
|
|
67
|
+
export function isStageValue(value: unknown): value is Stage {
|
|
68
|
+
return (
|
|
69
|
+
value === 'brief' ||
|
|
70
|
+
value === 'explore' ||
|
|
71
|
+
value === 'plan' ||
|
|
72
|
+
value === 'design' ||
|
|
73
|
+
value === 'verify'
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Narrow helper: does this value look like ParsedState's position.stage shape. */
|
|
78
|
+
export function hasStage(state: ParsedState): state is ParsedState {
|
|
79
|
+
return typeof state.position?.stage === 'string';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Emit a state.mutation event after a successful mutation. Callers pass
|
|
84
|
+
* the mutating tool's `name` and an opaque `diff` describing what changed.
|
|
85
|
+
*
|
|
86
|
+
* The event is persisted to the JSONL stream AND broadcast on the bus —
|
|
87
|
+
* see `scripts/lib/event-stream/index.ts`. `appendEvent()` never throws
|
|
88
|
+
* on I/O (the writer swallows write errors into `writeErrors`), so this
|
|
89
|
+
* helper is safe to call inside a `success: true` return path.
|
|
90
|
+
*/
|
|
91
|
+
export function emitStateMutation(
|
|
92
|
+
tool: string,
|
|
93
|
+
diff: unknown,
|
|
94
|
+
stateAfter: ParsedState,
|
|
95
|
+
): void {
|
|
96
|
+
const stage = isStageValue(stateAfter.position.stage)
|
|
97
|
+
? stateAfter.position.stage
|
|
98
|
+
: undefined;
|
|
99
|
+
const ev: StateMutationEvent = {
|
|
100
|
+
type: 'state.mutation',
|
|
101
|
+
timestamp: new Date().toISOString(),
|
|
102
|
+
sessionId: getSessionId(),
|
|
103
|
+
...(stage !== undefined ? { stage } : {}),
|
|
104
|
+
...(typeof stateAfter.frontmatter.cycle === 'string' &&
|
|
105
|
+
stateAfter.frontmatter.cycle.length > 0
|
|
106
|
+
? { cycle: stateAfter.frontmatter.cycle }
|
|
107
|
+
: {}),
|
|
108
|
+
payload: { tool, diff },
|
|
109
|
+
};
|
|
110
|
+
appendEvent(ev);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Emit a state.transition event. Two shapes exist: `pass=true` after a
|
|
115
|
+
* successful advance, and `pass=false` after a gate veto. Both cases are
|
|
116
|
+
* worth recording — gate vetoes are user-visible operational data.
|
|
117
|
+
*/
|
|
118
|
+
export function emitStateTransition(
|
|
119
|
+
from: Stage,
|
|
120
|
+
to: Stage,
|
|
121
|
+
pass: boolean,
|
|
122
|
+
blockers: string[],
|
|
123
|
+
state: ParsedState | null,
|
|
124
|
+
): void {
|
|
125
|
+
const cycle =
|
|
126
|
+
state !== null &&
|
|
127
|
+
typeof state.frontmatter.cycle === 'string' &&
|
|
128
|
+
state.frontmatter.cycle.length > 0
|
|
129
|
+
? state.frontmatter.cycle
|
|
130
|
+
: undefined;
|
|
131
|
+
const ev: StateTransitionEvent = {
|
|
132
|
+
type: 'state.transition',
|
|
133
|
+
timestamp: new Date().toISOString(),
|
|
134
|
+
sessionId: getSessionId(),
|
|
135
|
+
stage: pass ? to : from,
|
|
136
|
+
...(cycle !== undefined ? { cycle } : {}),
|
|
137
|
+
payload: { from, to, blockers: [...blockers], pass },
|
|
138
|
+
};
|
|
139
|
+
appendEvent(ev);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Map an error into a tool-response `{success:false,error}` object.
|
|
144
|
+
* Single entry point for every handler — keeps the error-shape decision
|
|
145
|
+
* in one place and lets us layer extra context (e.g. transition blockers)
|
|
146
|
+
* consistently.
|
|
147
|
+
*/
|
|
148
|
+
export function errorResponse(err: unknown): ToolResponse {
|
|
149
|
+
const payload = toToolError(err);
|
|
150
|
+
return { success: false, error: payload.error };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Shorthand for a `{success:true,data}` return with a plain object.
|
|
155
|
+
*/
|
|
156
|
+
export function okResponse(data: Record<string, unknown>): ToolResponse {
|
|
157
|
+
return { success: true, data };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Raise a ValidationError whose `message` is human-readable and whose
|
|
162
|
+
* `code` is `VALIDATION_*` — the default error code used across Plan
|
|
163
|
+
* 20-05 for input shape problems. Handlers call this after the JSON
|
|
164
|
+
* Schema check (which catches the big structural issues); this covers
|
|
165
|
+
* invariant checks that the schema cannot express (e.g. "patch contains
|
|
166
|
+
* a forbidden key").
|
|
167
|
+
*/
|
|
168
|
+
export function throwValidation(
|
|
169
|
+
codeSuffix: string,
|
|
170
|
+
message: string,
|
|
171
|
+
context?: Record<string, unknown>,
|
|
172
|
+
): never {
|
|
173
|
+
throw new ValidationError(message, `VALIDATION_${codeSuffix}`, context);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Raise an OperationFailedError — the caller's input was well-formed,
|
|
178
|
+
* but the requested operation cannot complete in the current state.
|
|
179
|
+
* Example: `resolve_blocker` asked to delete a row that doesn't exist.
|
|
180
|
+
*/
|
|
181
|
+
export function operationFailed(
|
|
182
|
+
codeSuffix: string,
|
|
183
|
+
message: string,
|
|
184
|
+
context?: Record<string, unknown>,
|
|
185
|
+
): never {
|
|
186
|
+
throw new OperationFailedError(
|
|
187
|
+
message,
|
|
188
|
+
`OPERATION_${codeSuffix}`,
|
|
189
|
+
context,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Re-exports kept local to avoid every handler importing the whole taxonomy. */
|
|
194
|
+
export type { BaseEvent };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// scripts/mcp-servers/gdd-state/tools/transition_stage.ts
|
|
2
|
+
//
|
|
3
|
+
// Tool: gdd_state__transition_stage
|
|
4
|
+
// Purpose: Run gate and advance <position>.stage / frontmatter.stage on
|
|
5
|
+
// pass. On gate veto, returns {success:false, error:{code:'TRANSITION_
|
|
6
|
+
// GATE_FAILED', context:{blockers:[...]}}}. Does NOT throw to the MCP
|
|
7
|
+
// harness — the plan mandates a contained error path so gate failures
|
|
8
|
+
// do not crash the server.
|
|
9
|
+
//
|
|
10
|
+
// Emits state.transition (pass=true) on success and state.transition
|
|
11
|
+
// (pass=false) on gate veto. Plan 20-06's event-stream surface accepts
|
|
12
|
+
// both forms; Plan 22+ dashboards render both.
|
|
13
|
+
|
|
14
|
+
import { read, transition } from '../../../lib/gdd-state/index.ts';
|
|
15
|
+
import { isStage, type Stage } from '../../../lib/gdd-state/types.ts';
|
|
16
|
+
import { TransitionGateFailed } from '../../../lib/gdd-errors/index.ts';
|
|
17
|
+
import {
|
|
18
|
+
emitStateTransition,
|
|
19
|
+
errorResponse,
|
|
20
|
+
okResponse,
|
|
21
|
+
resolveStatePath,
|
|
22
|
+
throwValidation,
|
|
23
|
+
type ToolResponse,
|
|
24
|
+
} from './shared.ts';
|
|
25
|
+
|
|
26
|
+
export const name = 'gdd_state__transition_stage';
|
|
27
|
+
export const schemaPath = '../schemas/transition_stage.schema.json';
|
|
28
|
+
|
|
29
|
+
export interface TransitionStageInput {
|
|
30
|
+
to: Stage;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function handle(input: unknown): Promise<ToolResponse> {
|
|
34
|
+
try {
|
|
35
|
+
const typed = (input ?? {}) as TransitionStageInput;
|
|
36
|
+
if (!isStage(typed.to)) {
|
|
37
|
+
throwValidation(
|
|
38
|
+
'STAGE_INVALID',
|
|
39
|
+
`to "${String(typed.to)}" is not a recognized Stage`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const path = resolveStatePath();
|
|
44
|
+
// Read current state before transition so the event + response can
|
|
45
|
+
// report `from` accurately, even when the gate vetoes (and therefore
|
|
46
|
+
// the state is unchanged).
|
|
47
|
+
const before = await read(path);
|
|
48
|
+
const fromValue = before.position.stage;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const result = await transition(path, typed.to);
|
|
52
|
+
// transition() only returns a result when pass=true — the gate
|
|
53
|
+
// failure path throws TransitionGateFailed.
|
|
54
|
+
const fromNarrow = isStage(fromValue) ? fromValue : typed.to;
|
|
55
|
+
emitStateTransition(fromNarrow, typed.to, true, [], result.state);
|
|
56
|
+
return okResponse({
|
|
57
|
+
from: fromValue,
|
|
58
|
+
to: typed.to,
|
|
59
|
+
state: result.state,
|
|
60
|
+
});
|
|
61
|
+
} catch (inner) {
|
|
62
|
+
// Emit the failure as an event — gate vetoes are useful telemetry
|
|
63
|
+
// — then translate into {success:false,error} so the MCP harness
|
|
64
|
+
// never sees the throw.
|
|
65
|
+
if (inner instanceof TransitionGateFailed) {
|
|
66
|
+
const fromNarrow = isStage(fromValue) ? fromValue : typed.to;
|
|
67
|
+
emitStateTransition(
|
|
68
|
+
fromNarrow,
|
|
69
|
+
typed.to,
|
|
70
|
+
false,
|
|
71
|
+
[...inner.blockers],
|
|
72
|
+
before,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
throw inner;
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
return errorResponse(err);
|
|
79
|
+
}
|
|
80
|
+
}
|