@hegemonart/get-design-done 1.33.0 → 1.33.6
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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +49 -0
- package/README.md +4 -0
- package/SKILL.md +1 -0
- package/agents/design-authority-watcher.md +4 -0
- package/connections/connections.md +2 -0
- package/connections/openrouter.md +86 -0
- package/hooks/budget-enforcer.ts +103 -0
- package/package.json +5 -2
- package/reference/gdd-runtime-audit.md +111 -0
- package/reference/gdd-threat-model.md +399 -0
- package/reference/openrouter-tier-mapping.md +98 -0
- package/reference/prices.openrouter.md +26 -0
- package/reference/registry.json +28 -0
- package/scripts/lib/authority-watcher/index.cjs +147 -0
- package/scripts/lib/budget-enforcer.cjs +16 -0
- package/scripts/lib/openrouter/catalog-fetcher.cjs +326 -0
- package/scripts/lib/peer-cli/acp-client.cjs +9 -1
- package/scripts/lib/peer-cli/asp-client.cjs +10 -1
- package/scripts/lib/peer-cli/sanitize-env.cjs +198 -0
- package/scripts/lib/redact.cjs +20 -1
- package/scripts/lib/tier-resolver-openrouter.cjs +343 -0
- package/scripts/lib/transports/ws.cjs +67 -3
- package/sdk/event-stream/types.ts +24 -2
- package/sdk/mcp/gdd-state/schemas/add_blocker.schema.json +2 -0
- package/sdk/mcp/gdd-state/schemas/add_decision.schema.json +1 -0
- package/sdk/mcp/gdd-state/schemas/add_must_have.schema.json +1 -0
- package/sdk/mcp/gdd-state/schemas/checkpoint.schema.json +1 -0
- package/sdk/mcp/gdd-state/schemas/frontmatter_update.schema.json +1 -1
- package/sdk/mcp/gdd-state/schemas/get.schema.json +2 -1
- package/sdk/mcp/gdd-state/schemas/probe_connections.schema.json +2 -0
- package/sdk/mcp/gdd-state/schemas/resolve_blocker.schema.json +1 -0
- package/sdk/mcp/gdd-state/server.js +137 -48
- package/sdk/mcp/gdd-state/tools/add_blocker.ts +2 -0
- package/sdk/mcp/gdd-state/tools/add_decision.ts +2 -0
- package/sdk/mcp/gdd-state/tools/add_must_have.ts +2 -0
- package/sdk/mcp/gdd-state/tools/checkpoint.ts +2 -0
- package/sdk/mcp/gdd-state/tools/frontmatter_update.ts +2 -0
- package/sdk/mcp/gdd-state/tools/get.ts +2 -0
- package/sdk/mcp/gdd-state/tools/probe_connections.ts +2 -0
- package/sdk/mcp/gdd-state/tools/resolve_blocker.ts +2 -0
- package/sdk/mcp/gdd-state/tools/set_status.ts +2 -0
- package/sdk/mcp/gdd-state/tools/shared.ts +117 -7
- package/sdk/mcp/gdd-state/tools/transition_stage.ts +2 -0
- package/sdk/mcp/gdd-state/tools/update_progress.ts +2 -0
- package/skills/openrouter-status/SKILL.md +86 -0
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type ConnectionStatus,
|
|
12
12
|
} from '../../../state/types.ts';
|
|
13
13
|
import {
|
|
14
|
+
assertInputWithinLimits,
|
|
14
15
|
emitStateMutation,
|
|
15
16
|
errorResponse,
|
|
16
17
|
okResponse,
|
|
@@ -28,6 +29,7 @@ export interface ProbeConnectionsInput {
|
|
|
28
29
|
|
|
29
30
|
export async function handle(input: unknown): Promise<ToolResponse> {
|
|
30
31
|
try {
|
|
32
|
+
assertInputWithinLimits(input);
|
|
31
33
|
const typed = (input ?? {}) as ProbeConnectionsInput;
|
|
32
34
|
if (!Array.isArray(typed.probe_results) || typed.probe_results.length === 0) {
|
|
33
35
|
throwValidation(
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { mutate } from '../../../state/index.ts';
|
|
10
10
|
import type { Blocker } from '../../../state/types.ts';
|
|
11
11
|
import {
|
|
12
|
+
assertInputWithinLimits,
|
|
12
13
|
emitStateMutation,
|
|
13
14
|
errorResponse,
|
|
14
15
|
okResponse,
|
|
@@ -28,6 +29,7 @@ export interface ResolveBlockerInput {
|
|
|
28
29
|
|
|
29
30
|
export async function handle(input: unknown): Promise<ToolResponse> {
|
|
30
31
|
try {
|
|
32
|
+
assertInputWithinLimits(input);
|
|
31
33
|
const typed = (input ?? {}) as ResolveBlockerInput;
|
|
32
34
|
const hasIndex = typeof typed.index === 'number';
|
|
33
35
|
const hasText = typeof typed.text === 'string' && typed.text.length > 0;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { mutate } from '../../../state/index.ts';
|
|
9
9
|
import {
|
|
10
|
+
assertInputWithinLimits,
|
|
10
11
|
emitStateMutation,
|
|
11
12
|
errorResponse,
|
|
12
13
|
okResponse,
|
|
@@ -31,6 +32,7 @@ const STATUSES = new Set([
|
|
|
31
32
|
|
|
32
33
|
export async function handle(input: unknown): Promise<ToolResponse> {
|
|
33
34
|
try {
|
|
35
|
+
assertInputWithinLimits(input);
|
|
34
36
|
const typed = (input ?? {}) as SetStatusInput;
|
|
35
37
|
if (typeof typed.status !== 'string' || !STATUSES.has(typed.status)) {
|
|
36
38
|
throwValidation(
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// This mirrors the invariant in the plan: "Tool errors are returned as
|
|
12
12
|
// {success:false, error} — handlers never propagate exceptions."
|
|
13
13
|
|
|
14
|
+
import path from 'node:path';
|
|
14
15
|
import {
|
|
15
16
|
ValidationError,
|
|
16
17
|
OperationFailedError,
|
|
@@ -50,17 +51,126 @@ export function getSessionId(): string {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
|
-
* Resolve the target STATE.md path from the environment
|
|
54
|
-
*
|
|
54
|
+
* Resolve the target STATE.md path from the environment, with a
|
|
55
|
+
* PATH-TRAVERSAL guard (Plan 33.5-03, D-08).
|
|
55
56
|
*
|
|
56
|
-
* Resolution
|
|
57
|
-
*
|
|
58
|
-
*
|
|
57
|
+
* Resolution: `process.env.GDD_STATE_PATH ?? .design/STATE.md`. The
|
|
58
|
+
* path-traversal threat this guards against is a RELATIVE override that uses
|
|
59
|
+
* `..` to escape the project root — that is REJECTED with a
|
|
60
|
+
* `VALIDATION_STATE_PATH_ESCAPE` error. An ABSOLUTE override is an explicit
|
|
61
|
+
* operator choice (a relocated state file, a CI tmp `.design`) and is ALLOWED,
|
|
62
|
+
* normalized (D-08) — it is NOT rejected merely for living outside cwd (that
|
|
63
|
+
* would break legitimate operator overrides, and a realpath-based boundary
|
|
64
|
+
* check diverges across platforms, e.g. macOS /var → /private/var).
|
|
65
|
+
*
|
|
66
|
+
* Tests and the server both call this so the resolution logic stays in one
|
|
67
|
+
* place.
|
|
59
68
|
*/
|
|
60
69
|
export function resolveStatePath(): string {
|
|
61
70
|
const override = process.env['GDD_STATE_PATH'];
|
|
62
|
-
if (typeof override
|
|
63
|
-
|
|
71
|
+
if (typeof override !== 'string' || override.length === 0) {
|
|
72
|
+
return '.design/STATE.md';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ABSOLUTE override = explicit operator choice → allow, normalized (D-08).
|
|
76
|
+
if (path.isAbsolute(override)) {
|
|
77
|
+
return path.resolve(override);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// RELATIVE override: resolve against the project root and REJECT any `..`
|
|
81
|
+
// traversal that escapes the boundary. In-boundary iff it equals root or
|
|
82
|
+
// sits beneath `root + sep`.
|
|
83
|
+
const root = path.resolve(process.cwd());
|
|
84
|
+
const resolved = path.resolve(root, override);
|
|
85
|
+
const withSep = root.endsWith(path.sep) ? root : root + path.sep;
|
|
86
|
+
if (resolved !== root && !resolved.startsWith(withSep)) {
|
|
87
|
+
throwValidation(
|
|
88
|
+
'STATE_PATH_ESCAPE',
|
|
89
|
+
`GDD_STATE_PATH (relative) escapes the project boundary: ${override}`,
|
|
90
|
+
{ raw: override, resolved, root },
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return resolved;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Documented input limits for the gdd-state tools (Plan 33.5-03, D-08).
|
|
98
|
+
* Defends against JSON-bomb / memory-exhaustion inputs. The schema
|
|
99
|
+
* `maxLength` bounds are the declarative twin of MAX_STRING_LEN.
|
|
100
|
+
*/
|
|
101
|
+
export const MAX_INPUT_BYTES = 64 * 1024; // 64 KiB serialized input cap
|
|
102
|
+
export const MAX_STRING_LEN = 8192; // longest single free-form string field
|
|
103
|
+
export const MAX_DEPTH = 32; // deepest object/array nesting
|
|
104
|
+
|
|
105
|
+
interface InputLimitOpts {
|
|
106
|
+
maxInputBytes?: number;
|
|
107
|
+
maxStringLen?: number;
|
|
108
|
+
maxDepth?: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Reject oversized / pathologically deep tool inputs BEFORE processing
|
|
113
|
+
* (Plan 33.5-03, D-08). Throws a `VALIDATION_INPUT_*` error on breach:
|
|
114
|
+
* - INPUT_TOO_LARGE — serialized JSON byte-size exceeds the cap
|
|
115
|
+
* - INPUT_FIELD_TOO_LARGE — a single string field exceeds MAX_STRING_LEN
|
|
116
|
+
* - INPUT_TOO_DEEP — object/array nesting exceeds MAX_DEPTH
|
|
117
|
+
* Handlers call this on their raw input; it is also unit-tested directly.
|
|
118
|
+
*/
|
|
119
|
+
export function assertInputWithinLimits(
|
|
120
|
+
input: unknown,
|
|
121
|
+
opts?: InputLimitOpts,
|
|
122
|
+
): void {
|
|
123
|
+
const maxBytes = opts?.maxInputBytes ?? MAX_INPUT_BYTES;
|
|
124
|
+
const maxStr = opts?.maxStringLen ?? MAX_STRING_LEN;
|
|
125
|
+
const maxDepth = opts?.maxDepth ?? MAX_DEPTH;
|
|
126
|
+
|
|
127
|
+
// Walk first so a deep/long field is caught even on huge inputs, bounding
|
|
128
|
+
// the depth so the walk itself cannot be turned into the attack.
|
|
129
|
+
const walk = (node: unknown, depth: number): void => {
|
|
130
|
+
if (depth > maxDepth) {
|
|
131
|
+
throwValidation(
|
|
132
|
+
'INPUT_TOO_DEEP',
|
|
133
|
+
`Input nesting exceeds the maximum depth of ${maxDepth}`,
|
|
134
|
+
{ maxDepth },
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (typeof node === 'string') {
|
|
138
|
+
if (node.length > maxStr) {
|
|
139
|
+
throwValidation(
|
|
140
|
+
'INPUT_FIELD_TOO_LARGE',
|
|
141
|
+
`A string field exceeds the maximum length of ${maxStr}`,
|
|
142
|
+
{ maxStringLen: maxStr, length: node.length },
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (Array.isArray(node)) {
|
|
148
|
+
for (const item of node) walk(item, depth + 1);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (node !== null && typeof node === 'object') {
|
|
152
|
+
for (const value of Object.values(node as Record<string, unknown>)) {
|
|
153
|
+
walk(value, depth + 1);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
walk(input, 0);
|
|
158
|
+
|
|
159
|
+
// Total serialized byte-size cap (JSON-bomb guard). Guard against a circular
|
|
160
|
+
// structure — JSON.stringify would throw; treat that as a rejectable input.
|
|
161
|
+
let bytes: number;
|
|
162
|
+
try {
|
|
163
|
+
bytes = Buffer.byteLength(JSON.stringify(input) ?? '');
|
|
164
|
+
} catch {
|
|
165
|
+
throwValidation('INPUT_TOO_LARGE', 'Input is not serializable JSON', {});
|
|
166
|
+
}
|
|
167
|
+
if (bytes > maxBytes) {
|
|
168
|
+
throwValidation(
|
|
169
|
+
'INPUT_TOO_LARGE',
|
|
170
|
+
`Serialized input (${bytes} bytes) exceeds the maximum of ${maxBytes} bytes`,
|
|
171
|
+
{ maxInputBytes: maxBytes, bytes },
|
|
172
|
+
);
|
|
173
|
+
}
|
|
64
174
|
}
|
|
65
175
|
|
|
66
176
|
/** Narrow helper: is this a well-known Stage string? */
|
|
@@ -15,6 +15,7 @@ import { read, transition } from '../../../state/index.ts';
|
|
|
15
15
|
import { isStage, type Stage } from '../../../state/types.ts';
|
|
16
16
|
import { TransitionGateFailed } from '../../../errors/index.ts';
|
|
17
17
|
import {
|
|
18
|
+
assertInputWithinLimits,
|
|
18
19
|
emitStateTransition,
|
|
19
20
|
errorResponse,
|
|
20
21
|
okResponse,
|
|
@@ -32,6 +33,7 @@ export interface TransitionStageInput {
|
|
|
32
33
|
|
|
33
34
|
export async function handle(input: unknown): Promise<ToolResponse> {
|
|
34
35
|
try {
|
|
36
|
+
assertInputWithinLimits(input);
|
|
35
37
|
const typed = (input ?? {}) as TransitionStageInput;
|
|
36
38
|
if (!isStage(typed.to)) {
|
|
37
39
|
throwValidation(
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { mutate } from '../../../state/index.ts';
|
|
8
8
|
import {
|
|
9
|
+
assertInputWithinLimits,
|
|
9
10
|
emitStateMutation,
|
|
10
11
|
errorResponse,
|
|
11
12
|
okResponse,
|
|
@@ -35,6 +36,7 @@ const STATUSES = new Set([
|
|
|
35
36
|
|
|
36
37
|
export async function handle(input: unknown): Promise<ToolResponse> {
|
|
37
38
|
try {
|
|
39
|
+
assertInputWithinLimits(input);
|
|
38
40
|
const typed = (input ?? {}) as UpdateProgressInput;
|
|
39
41
|
if (typed.task_progress === undefined && typed.status === undefined) {
|
|
40
42
|
throwValidation(
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gdd-openrouter-status
|
|
3
|
+
description: "Read-only OpenRouter catalog + tier-mapping diagnostic — surfaces catalog freshness (fetched_at vs the 24h TTL), the last-fetch timestamp, the resolved opus/sonnet/haiku → model mappings (via the Phase-33.6 adapter), and a per-tier preview. Use when investigating which OpenRouter model a tier resolves to, or whether the catalog cache is fresh/stale. Phase 33.6 (v1.33.6) diagnostic — /gdd:openrouter-status."
|
|
4
|
+
argument-hint: "[--refresh]"
|
|
5
|
+
tools: Read, Bash
|
|
6
|
+
disable-model-invocation: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# gdd-openrouter-status
|
|
10
|
+
|
|
11
|
+
## Role
|
|
12
|
+
|
|
13
|
+
You are a deterministic, read-only diagnostic skill. You do **not** spawn agents and you do **not** modify the catalog cache. You read `.design/cache/openrouter-models.json` (the Phase-33.6-01 catalog cache) via `scripts/lib/openrouter/catalog-fetcher.cjs#readCatalog`, resolve the `opus`/`sonnet`/`haiku` tiers via `scripts/lib/tier-resolver-openrouter.cjs#resolve`, and emit a single Markdown status block. Read-only — to refresh the catalog you pass `--refresh` (a single opt-in fetch gated on `OPENROUTER_API_KEY`); there is no other mutation. See `connections/openrouter.md` for setup and `reference/openrouter-tier-mapping.md` for the resolution heuristic.
|
|
14
|
+
|
|
15
|
+
This is `disable-model-invocation: true` (mirroring `skills/cache-manager/SKILL.md`): the skill is user-invoked only — the model must not auto-spawn it. It never makes a model call.
|
|
16
|
+
|
|
17
|
+
## Invocation Contract
|
|
18
|
+
|
|
19
|
+
- **Input**: optional `--refresh`. When absent, the skill is purely read-only (cache + resolve). When `--refresh` is set AND `OPENROUTER_API_KEY` is present, it calls the Phase-33.6-01 fetcher once to refresh the cache before reading; when `--refresh` is set but no key is present, it prints the empty-state/no-key message and does NOT fetch.
|
|
20
|
+
- **Output**: a Markdown OpenRouter-status block to stdout. The block is the entire output.
|
|
21
|
+
|
|
22
|
+
## Procedure
|
|
23
|
+
|
|
24
|
+
### 1. (Optional) refresh
|
|
25
|
+
|
|
26
|
+
If `--refresh` is set and `OPENROUTER_API_KEY` is present, run a single fetch to refresh the cache:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
node -e "require('./scripts/lib/openrouter/catalog-fetcher.cjs').fetchCatalog().then(()=>{}).catch(()=>{})"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This is the ONLY mutation the skill performs, and only on explicit `--refresh`. The fetch never throws (D-08); a failure degrades to the existing cache.
|
|
33
|
+
|
|
34
|
+
### 2. Read the catalog cache
|
|
35
|
+
|
|
36
|
+
Read `.design/cache/openrouter-models.json` via `readCatalog`. Missing or empty → emit the empty-state message and stop:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
## OpenRouter Status
|
|
40
|
+
|
|
41
|
+
No OpenRouter catalog yet — set OPENROUTER_API_KEY and run a cycle, or `/gdd:openrouter-status --refresh`.
|
|
42
|
+
|
|
43
|
+
Tier resolution is currently falling back to the native provider (graceful degrade — D-08).
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 3. Compute freshness
|
|
47
|
+
|
|
48
|
+
Read `fetched_at` from the cache object and compare against the 24h TTL (D-02): `age = now - fetched_at`. `age < 24h` → **fresh**; otherwise → **stale** (a stale catalog still resolves — the adapter uses the last good cache).
|
|
49
|
+
|
|
50
|
+
### 4. Resolve the tiers
|
|
51
|
+
|
|
52
|
+
For each of `opus`, `sonnet`, `haiku`, resolve via the adapter (it reads `.design/config.json#openrouter_tier_overrides` and applies the heuristic over the cached catalog):
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
node -e "const r=require('./scripts/lib/tier-resolver-openrouter.cjs');for(const t of ['opus','sonnet','haiku'])console.log(t, '->', r.resolve(t) || '(null → native fallback)')"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
A `null` for a tier means OpenRouter has no pick → the native provider resolves that tier (D-08). Note any tier that resolved from an explicit `openrouter_tier_overrides` pin vs the heuristic.
|
|
59
|
+
|
|
60
|
+
### 5. Print the status block
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
## OpenRouter Status
|
|
64
|
+
|
|
65
|
+
Catalog source: <source URL from cache>
|
|
66
|
+
Last fetched: <fetched_at> (<fresh | stale> — TTL 24h)
|
|
67
|
+
Models in catalog: <count>
|
|
68
|
+
|
|
69
|
+
| Tier | Resolved model id | Source |
|
|
70
|
+
|--------|----------------------------------|--------------------|
|
|
71
|
+
| opus | <id or (null → native fallback)> | <override | heuristic> |
|
|
72
|
+
| sonnet | <id or (null → native fallback)> | <override | heuristic> |
|
|
73
|
+
| haiku | <id or (null → native fallback)> | <override | heuristic> |
|
|
74
|
+
|
|
75
|
+
> Resolution: override (`.design/config.json#openrouter_tier_overrides`) wins, else the closed-vs-open + pricing heuristic over the catalog.
|
|
76
|
+
> A null resolution means tier resolution falls back to the native provider (D-08).
|
|
77
|
+
> Read-only — this skill never modifies the cache; use `--refresh` to re-fetch (needs OPENROUTER_API_KEY).
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Completion marker
|
|
81
|
+
|
|
82
|
+
End the output with:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
## OPENROUTER-STATUS COMPLETE
|
|
86
|
+
```
|