@hegemonart/get-design-done 1.19.6 → 1.21.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 +11 -14
- package/.claude-plugin/plugin.json +9 -32
- package/CHANGELOG.md +138 -0
- package/README.md +54 -1
- package/agents/design-reflector.md +13 -0
- package/bin/gdd-sdk +55 -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 +32 -51
- package/reference/codex-tools.md +53 -0
- package/reference/config-schema.md +2 -2
- package/reference/error-recovery.md +58 -0
- package/reference/gemini-tools.md +53 -0
- package/reference/registry.json +21 -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/e2e/run-headless.ts +514 -0
- package/scripts/lib/cli/commands/audit.ts +382 -0
- package/scripts/lib/cli/commands/init.ts +217 -0
- package/scripts/lib/cli/commands/query.ts +329 -0
- package/scripts/lib/cli/commands/run.ts +656 -0
- package/scripts/lib/cli/commands/stage.ts +468 -0
- package/scripts/lib/cli/index.ts +167 -0
- package/scripts/lib/cli/parse-args.ts +336 -0
- package/scripts/lib/context-engine/index.ts +116 -0
- package/scripts/lib/context-engine/manifest.ts +69 -0
- package/scripts/lib/context-engine/truncate.ts +282 -0
- package/scripts/lib/context-engine/types.ts +59 -0
- package/scripts/lib/discuss-parallel-runner/aggregator.ts +448 -0
- package/scripts/lib/discuss-parallel-runner/discussants.ts +430 -0
- package/scripts/lib/discuss-parallel-runner/index.ts +223 -0
- package/scripts/lib/discuss-parallel-runner/types.ts +184 -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 +164 -0
- package/scripts/lib/event-stream/types.ts +127 -0
- package/scripts/lib/event-stream/writer.ts +154 -0
- package/scripts/lib/explore-parallel-runner/index.ts +294 -0
- package/scripts/lib/explore-parallel-runner/mappers.ts +290 -0
- package/scripts/lib/explore-parallel-runner/synthesizer.ts +295 -0
- package/scripts/lib/explore-parallel-runner/types.ts +139 -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/harness/detect.ts +90 -0
- package/scripts/lib/harness/index.ts +64 -0
- package/scripts/lib/harness/tool-map.ts +142 -0
- package/scripts/lib/init-runner/index.ts +396 -0
- package/scripts/lib/init-runner/researchers.ts +245 -0
- package/scripts/lib/init-runner/scaffold.ts +224 -0
- package/scripts/lib/init-runner/synthesizer.ts +224 -0
- package/scripts/lib/init-runner/types.ts +143 -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/logger/index.ts +251 -0
- package/scripts/lib/logger/sinks.ts +269 -0
- package/scripts/lib/logger/types.ts +110 -0
- package/scripts/lib/pipeline-runner/human-gate.ts +134 -0
- package/scripts/lib/pipeline-runner/index.ts +527 -0
- package/scripts/lib/pipeline-runner/stage-handlers.ts +339 -0
- package/scripts/lib/pipeline-runner/state-machine.ts +144 -0
- package/scripts/lib/pipeline-runner/types.ts +183 -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/lib/session-runner/errors.ts +406 -0
- package/scripts/lib/session-runner/index.ts +715 -0
- package/scripts/lib/session-runner/transcript.ts +189 -0
- package/scripts/lib/session-runner/types.ts +144 -0
- package/scripts/lib/tool-scoping/index.ts +219 -0
- package/scripts/lib/tool-scoping/parse-agent-tools.ts +207 -0
- package/scripts/lib/tool-scoping/stage-scopes.ts +139 -0
- package/scripts/lib/tool-scoping/types.ts +77 -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,90 @@
|
|
|
1
|
+
// scripts/lib/harness/detect.ts — Plan 21-10 (SDK-22 / SDK-23).
|
|
2
|
+
//
|
|
3
|
+
// Harness detection runtime. Inspects the process env (or an injected
|
|
4
|
+
// env map for tests) and reports which agent harness the current process
|
|
5
|
+
// is running inside: Claude Code, OpenAI Codex CLI, Google Gemini CLI,
|
|
6
|
+
// or `unknown` when no harness can be identified.
|
|
7
|
+
//
|
|
8
|
+
// Precedence (highest wins):
|
|
9
|
+
// 1. `GDD_HARNESS` — explicit override. Accepts 'claude-code' | 'codex'
|
|
10
|
+
// | 'gemini' | 'unknown' verbatim. Anything else → 'unknown'.
|
|
11
|
+
// 2. `CLAUDECODE=1` OR `CLAUDE_CODE=1` → 'claude-code'.
|
|
12
|
+
// 3. `CODEX_CLI_VERSION` set (any truthy value) → 'codex'.
|
|
13
|
+
// 4. `GEMINI_CLI_VERSION` set (any truthy value) → 'gemini'.
|
|
14
|
+
// 5. Fallback → 'unknown'.
|
|
15
|
+
//
|
|
16
|
+
// This module is pure — no side effects, no caching. Callers that want
|
|
17
|
+
// process-wide caching go through `scripts/lib/harness/index.ts`, which
|
|
18
|
+
// layers a `currentHarness()` helper on top.
|
|
19
|
+
|
|
20
|
+
export type Harness = 'claude-code' | 'codex' | 'gemini' | 'unknown';
|
|
21
|
+
|
|
22
|
+
/** The four canonical harness identifiers this plugin recognizes. */
|
|
23
|
+
export const KNOWN_HARNESSES: readonly Harness[] = Object.freeze([
|
|
24
|
+
'claude-code',
|
|
25
|
+
'codex',
|
|
26
|
+
'gemini',
|
|
27
|
+
'unknown',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Detect which agent harness the current process is running inside.
|
|
32
|
+
*
|
|
33
|
+
* Reads the supplied env map (defaults to `process.env`). Precedence is
|
|
34
|
+
* documented at the top of this file — the explicit `GDD_HARNESS` override
|
|
35
|
+
* wins over implicit env-var detection to make tests and simulated fixtures
|
|
36
|
+
* deterministic.
|
|
37
|
+
*/
|
|
38
|
+
export function detectHarness(env?: NodeJS.ProcessEnv): Harness {
|
|
39
|
+
const e: NodeJS.ProcessEnv = env ?? process.env;
|
|
40
|
+
|
|
41
|
+
// 1. Explicit override wins.
|
|
42
|
+
const override: string | undefined = e.GDD_HARNESS;
|
|
43
|
+
if (override !== undefined && override !== '') {
|
|
44
|
+
if (isHarness(override)) return override;
|
|
45
|
+
// Any other non-empty string → 'unknown' (override is present but invalid).
|
|
46
|
+
return 'unknown';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 2. Claude Code: either CLAUDECODE or CLAUDE_CODE set to "1".
|
|
50
|
+
if (e.CLAUDECODE === '1' || e.CLAUDE_CODE === '1') {
|
|
51
|
+
return 'claude-code';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 3. Codex: CODEX_CLI_VERSION present (any non-empty value).
|
|
55
|
+
if (e.CODEX_CLI_VERSION !== undefined && e.CODEX_CLI_VERSION !== '') {
|
|
56
|
+
return 'codex';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 4. Gemini: GEMINI_CLI_VERSION present (any non-empty value).
|
|
60
|
+
if (e.GEMINI_CLI_VERSION !== undefined && e.GEMINI_CLI_VERSION !== '') {
|
|
61
|
+
return 'gemini';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return 'unknown';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* True for harnesses that this plugin fully supports (Claude Code, Codex,
|
|
69
|
+
* Gemini). False for `'unknown'`.
|
|
70
|
+
*
|
|
71
|
+
* Callers use this as a gate before invoking harness-specific code paths;
|
|
72
|
+
* an unknown harness falls back to CC-native tool names (see
|
|
73
|
+
* `tool-map.ts TOOL_MAPS.unknown`).
|
|
74
|
+
*/
|
|
75
|
+
export function isSupportedHarness(h: Harness): boolean {
|
|
76
|
+
return h === 'claude-code' || h === 'codex' || h === 'gemini';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Narrow an arbitrary string to the `Harness` union. Not exported — used
|
|
81
|
+
* only by `detectHarness` to validate the `GDD_HARNESS` override.
|
|
82
|
+
*/
|
|
83
|
+
function isHarness(s: string): s is Harness {
|
|
84
|
+
return (
|
|
85
|
+
s === 'claude-code' ||
|
|
86
|
+
s === 'codex' ||
|
|
87
|
+
s === 'gemini' ||
|
|
88
|
+
s === 'unknown'
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// scripts/lib/harness/index.ts — Plan 21-10 (SDK-22 / SDK-23).
|
|
2
|
+
//
|
|
3
|
+
// Public API for the harness module. Re-exports the detect + tool-map
|
|
4
|
+
// surfaces and adds a `currentHarness()` helper that caches the
|
|
5
|
+
// first-call result for the life of the process.
|
|
6
|
+
//
|
|
7
|
+
// Why cache? Harness identity is a process-scoped invariant. The env
|
|
8
|
+
// vars that drive detection (CLAUDECODE, CODEX_CLI_VERSION,
|
|
9
|
+
// GEMINI_CLI_VERSION, GDD_HARNESS) are set by the harness when it
|
|
10
|
+
// spawns us — they do not change mid-process. Repeated env reads are
|
|
11
|
+
// cheap but the cache avoids any chance of divergent reads if a
|
|
12
|
+
// downstream caller mutates process.env (tests sometimes do this, and
|
|
13
|
+
// we want `currentHarness()` to stay monotonic within a test unless
|
|
14
|
+
// `resetHarnessCache()` is called explicitly).
|
|
15
|
+
//
|
|
16
|
+
// Use `resetHarnessCache()` in test `beforeEach` to re-read env after
|
|
17
|
+
// mutating it.
|
|
18
|
+
|
|
19
|
+
import { detectHarness, isSupportedHarness, type Harness } from './detect.ts';
|
|
20
|
+
|
|
21
|
+
export { detectHarness, isSupportedHarness, type Harness } from './detect.ts';
|
|
22
|
+
export {
|
|
23
|
+
TOOL_MAPS,
|
|
24
|
+
mapTool,
|
|
25
|
+
reverseMapTool,
|
|
26
|
+
CC_TOOLS,
|
|
27
|
+
type CCTool,
|
|
28
|
+
} from './tool-map.ts';
|
|
29
|
+
|
|
30
|
+
let cached: Harness | undefined = undefined;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Cached harness lookup. On first call, reads `process.env` via
|
|
34
|
+
* `detectHarness()` and stores the result. Every subsequent call
|
|
35
|
+
* returns the cached value, regardless of later env mutations.
|
|
36
|
+
*
|
|
37
|
+
* Call `resetHarnessCache()` to force a re-read.
|
|
38
|
+
*/
|
|
39
|
+
export function currentHarness(): Harness {
|
|
40
|
+
if (cached === undefined) {
|
|
41
|
+
cached = detectHarness(process.env);
|
|
42
|
+
}
|
|
43
|
+
return cached;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Clear the `currentHarness()` cache. Tests that mutate `process.env`
|
|
48
|
+
* between cases should call this in `beforeEach` (or equivalently)
|
|
49
|
+
* so each case sees a fresh detection.
|
|
50
|
+
*/
|
|
51
|
+
export function resetHarnessCache(): void {
|
|
52
|
+
cached = undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* True when the currently detected harness exposes MCP protocol support.
|
|
57
|
+
* Used by gdd-sdk audit to decide whether to spawn the gdd-state MCP
|
|
58
|
+
* server or import handlers directly.
|
|
59
|
+
*
|
|
60
|
+
* Claude Code, Codex, and Gemini all speak MCP; only `'unknown'` does not.
|
|
61
|
+
*/
|
|
62
|
+
export function harnessSupportsMCP(): boolean {
|
|
63
|
+
return isSupportedHarness(currentHarness());
|
|
64
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// scripts/lib/harness/tool-map.ts — Plan 21-10 (SDK-22 / SDK-23).
|
|
2
|
+
//
|
|
3
|
+
// Cross-harness tool-name lookup table + helpers. Given a Claude Code
|
|
4
|
+
// tool name (`Read`, `Write`, `Edit`, etc.) and a harness identifier,
|
|
5
|
+
// return the native tool name on that harness. Also supports inverse
|
|
6
|
+
// mapping (harness-native name → CC name).
|
|
7
|
+
//
|
|
8
|
+
// The maps are frozen (Object.freeze at two levels) — consumers cannot
|
|
9
|
+
// accidentally mutate the tables. Any mutation attempt throws in strict
|
|
10
|
+
// mode, silently no-ops otherwise; tests assert frozenness explicitly
|
|
11
|
+
// to lock the invariant.
|
|
12
|
+
//
|
|
13
|
+
// Task spawning — the CC `Task` tool has no direct native equivalent on
|
|
14
|
+
// Codex or Gemini (both require spawning a nested CLI instance as a
|
|
15
|
+
// shell subprocess rather than a tool call). The map returns `null`
|
|
16
|
+
// for those slots; callers check for null and fall back to a
|
|
17
|
+
// `shell("npx gdd-sdk …")` invocation. See AGENTS.md / GEMINI.md.
|
|
18
|
+
|
|
19
|
+
import type { Harness } from './detect.ts';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Canonical Claude Code tool names the plugin references in skill prose.
|
|
23
|
+
* This is the shape against which per-harness maps are typed so that
|
|
24
|
+
* adding a new CC tool to the canonical set fails TSC on every harness
|
|
25
|
+
* map that forgets to include it.
|
|
26
|
+
*/
|
|
27
|
+
export type CCTool =
|
|
28
|
+
| 'Read'
|
|
29
|
+
| 'Write'
|
|
30
|
+
| 'Edit'
|
|
31
|
+
| 'Bash'
|
|
32
|
+
| 'Grep'
|
|
33
|
+
| 'Glob'
|
|
34
|
+
| 'Task'
|
|
35
|
+
| 'WebSearch'
|
|
36
|
+
| 'WebFetch';
|
|
37
|
+
|
|
38
|
+
/** All nine CC tool names — useful for iteration in tests. */
|
|
39
|
+
export const CC_TOOLS: readonly CCTool[] = Object.freeze([
|
|
40
|
+
'Read',
|
|
41
|
+
'Write',
|
|
42
|
+
'Edit',
|
|
43
|
+
'Bash',
|
|
44
|
+
'Grep',
|
|
45
|
+
'Glob',
|
|
46
|
+
'Task',
|
|
47
|
+
'WebSearch',
|
|
48
|
+
'WebFetch',
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Per-harness lookup. Each entry maps every CC tool to its native name
|
|
53
|
+
* on that harness, or `null` when the harness has no direct equivalent
|
|
54
|
+
* (currently only `Task` on Codex + Gemini).
|
|
55
|
+
*
|
|
56
|
+
* The `unknown` row falls back to CC names — callers that cannot identify
|
|
57
|
+
* the harness get a reasonable default that works on Claude Code and
|
|
58
|
+
* fails loudly on any other harness (the harness will refuse an
|
|
59
|
+
* unrecognized tool call).
|
|
60
|
+
*/
|
|
61
|
+
export const TOOL_MAPS: Readonly<Record<Harness, Readonly<Record<CCTool, string | null>>>> = Object.freeze({
|
|
62
|
+
'claude-code': Object.freeze({
|
|
63
|
+
Read: 'Read',
|
|
64
|
+
Write: 'Write',
|
|
65
|
+
Edit: 'Edit',
|
|
66
|
+
Bash: 'Bash',
|
|
67
|
+
Grep: 'Grep',
|
|
68
|
+
Glob: 'Glob',
|
|
69
|
+
Task: 'Task',
|
|
70
|
+
WebSearch: 'WebSearch',
|
|
71
|
+
WebFetch: 'WebFetch',
|
|
72
|
+
}),
|
|
73
|
+
codex: Object.freeze({
|
|
74
|
+
Read: 'read_file',
|
|
75
|
+
Write: 'apply_patch',
|
|
76
|
+
Edit: 'apply_patch',
|
|
77
|
+
Bash: 'shell',
|
|
78
|
+
Grep: 'shell',
|
|
79
|
+
Glob: 'shell',
|
|
80
|
+
Task: null, // no native Task; use CLI subprocess
|
|
81
|
+
WebSearch: 'web_search',
|
|
82
|
+
WebFetch: 'shell',
|
|
83
|
+
}),
|
|
84
|
+
gemini: Object.freeze({
|
|
85
|
+
Read: 'read_file',
|
|
86
|
+
Write: 'write_file',
|
|
87
|
+
Edit: 'replace',
|
|
88
|
+
Bash: 'run_shell_command',
|
|
89
|
+
Grep: 'search_file_content',
|
|
90
|
+
Glob: 'glob',
|
|
91
|
+
Task: null, // no native Task; use CLI subprocess
|
|
92
|
+
WebSearch: 'google_web_search',
|
|
93
|
+
WebFetch: 'web_fetch',
|
|
94
|
+
}),
|
|
95
|
+
unknown: Object.freeze({
|
|
96
|
+
Read: 'Read',
|
|
97
|
+
Write: 'Write',
|
|
98
|
+
Edit: 'Edit',
|
|
99
|
+
Bash: 'Bash',
|
|
100
|
+
Grep: 'Grep',
|
|
101
|
+
Glob: 'Glob',
|
|
102
|
+
Task: 'Task',
|
|
103
|
+
WebSearch: 'WebSearch',
|
|
104
|
+
WebFetch: 'WebFetch',
|
|
105
|
+
}),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Return the harness-specific tool name for a CC tool. Returns `null`
|
|
110
|
+
* when the harness has no native equivalent — currently only `Task` on
|
|
111
|
+
* Codex + Gemini. Callers that receive `null` should fall back to a
|
|
112
|
+
* `shell`/`run_shell_command` invocation of `npx gdd-sdk …`.
|
|
113
|
+
*/
|
|
114
|
+
export function mapTool(harness: Harness, ccTool: CCTool): string | null {
|
|
115
|
+
const row = TOOL_MAPS[harness];
|
|
116
|
+
// Every Harness key is present in TOOL_MAPS by construction (the type
|
|
117
|
+
// forces it). The index result under `noUncheckedIndexedAccess` is
|
|
118
|
+
// still `string | null | undefined`; narrow with a hasOwnProperty
|
|
119
|
+
// check to keep TSC happy.
|
|
120
|
+
const native: string | null | undefined = row[ccTool];
|
|
121
|
+
return native ?? null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Inverse of `mapTool` — given a harness-native tool name (e.g.
|
|
126
|
+
* `'read_file'`), return the CC tool it came from (`'Read'`). Returns
|
|
127
|
+
* `null` when the native name is not in the harness's map at all.
|
|
128
|
+
*
|
|
129
|
+
* Note: on Codex several CC tools share a native name (`Write`, `Edit`,
|
|
130
|
+
* `WebFetch` all share `apply_patch` / `shell`). The reverse mapper
|
|
131
|
+
* returns the FIRST CC match walking in declaration order
|
|
132
|
+
* (`Read` → `Write` → `Edit` → …) — callers that need disambiguation
|
|
133
|
+
* between, e.g., create vs. update must consult the full forward map
|
|
134
|
+
* or inspect tool-call arguments.
|
|
135
|
+
*/
|
|
136
|
+
export function reverseMapTool(harness: Harness, nativeName: string): CCTool | null {
|
|
137
|
+
const row = TOOL_MAPS[harness];
|
|
138
|
+
for (const cc of CC_TOOLS) {
|
|
139
|
+
if (row[cc] === nativeName) return cc;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
// scripts/lib/init-runner/index.ts — public entry for the `gdd-sdk init`
|
|
2
|
+
// runner (Plan 21-08, SDK-20).
|
|
3
|
+
//
|
|
4
|
+
// Public surface:
|
|
5
|
+
//
|
|
6
|
+
// run(opts: InitRunnerOptions): Promise<InitRunnerResult>
|
|
7
|
+
// DEFAULT_RESEARCHERS: readonly ResearcherSpec[]
|
|
8
|
+
// + re-exports of submodule types + helpers.
|
|
9
|
+
//
|
|
10
|
+
// Orchestration algorithm:
|
|
11
|
+
//
|
|
12
|
+
// 1. Resolve cwd + .design/ path.
|
|
13
|
+
// 2. Existence check on .design/STATE.md:
|
|
14
|
+
// - exists + !force → return status: 'already-initialized'
|
|
15
|
+
// - exists + force → backup via backupExistingDesignDir
|
|
16
|
+
// 3. ensureDesignDirs (idempotent).
|
|
17
|
+
// 4. writeStateFromTemplate. Missing template → status: 'error'.
|
|
18
|
+
// 5. spawnResearchersParallel with concurrency ?? 4.
|
|
19
|
+
// 6. Filter to successful outcomes. Zero → status: 'no-researchers-succeeded'.
|
|
20
|
+
// 7. Read each successful output; build SynthesizerInput[].
|
|
21
|
+
// 8. spawnSynthesizer.
|
|
22
|
+
// 9. Aggregate usage + emit lifecycle logs.
|
|
23
|
+
// 10. Return InitRunnerResult.
|
|
24
|
+
//
|
|
25
|
+
// This module emits two logger events:
|
|
26
|
+
// * init.runner.started — before researcher dispatch.
|
|
27
|
+
// * init.runner.completed — regardless of status.
|
|
28
|
+
|
|
29
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
30
|
+
import { resolve } from 'node:path';
|
|
31
|
+
|
|
32
|
+
import { getLogger } from '../logger/index.ts';
|
|
33
|
+
import {
|
|
34
|
+
spawnResearcher,
|
|
35
|
+
spawnResearchersParallel,
|
|
36
|
+
} from './researchers.ts';
|
|
37
|
+
import {
|
|
38
|
+
buildSynthesizerPrompt,
|
|
39
|
+
DEFAULT_SYNTHESIZER_PROMPT,
|
|
40
|
+
spawnSynthesizer,
|
|
41
|
+
} from './synthesizer.ts';
|
|
42
|
+
import {
|
|
43
|
+
backupExistingDesignDir,
|
|
44
|
+
ensureDesignDirs,
|
|
45
|
+
resolveStateTemplatePath,
|
|
46
|
+
writeStateFromTemplate,
|
|
47
|
+
} from './scaffold.ts';
|
|
48
|
+
import type {
|
|
49
|
+
InitRunnerOptions,
|
|
50
|
+
InitRunnerResult,
|
|
51
|
+
InitStatus,
|
|
52
|
+
ResearcherName,
|
|
53
|
+
ResearcherOutcome,
|
|
54
|
+
ResearcherSpec,
|
|
55
|
+
} from './types.ts';
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Re-exports — consumers import everything from this file.
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
export type {
|
|
62
|
+
InitRunnerOptions,
|
|
63
|
+
InitRunnerResult,
|
|
64
|
+
InitStatus,
|
|
65
|
+
ResearcherName,
|
|
66
|
+
ResearcherOutcome,
|
|
67
|
+
ResearcherSpec,
|
|
68
|
+
} from './types.ts';
|
|
69
|
+
export { spawnResearcher, spawnResearchersParallel } from './researchers.ts';
|
|
70
|
+
export {
|
|
71
|
+
buildSynthesizerPrompt,
|
|
72
|
+
DEFAULT_SYNTHESIZER_PROMPT,
|
|
73
|
+
spawnSynthesizer,
|
|
74
|
+
} from './synthesizer.ts';
|
|
75
|
+
export type {
|
|
76
|
+
SpawnSynthesizerArgs,
|
|
77
|
+
SpawnSynthesizerResult,
|
|
78
|
+
SynthesizerInput,
|
|
79
|
+
} from './synthesizer.ts';
|
|
80
|
+
export type {
|
|
81
|
+
SpawnParallelOptions,
|
|
82
|
+
SpawnResearcherOptions,
|
|
83
|
+
} from './researchers.ts';
|
|
84
|
+
export {
|
|
85
|
+
backupExistingDesignDir,
|
|
86
|
+
ensureDesignDirs,
|
|
87
|
+
resolveStateTemplatePath,
|
|
88
|
+
writeStateFromTemplate,
|
|
89
|
+
} from './scaffold.ts';
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// DEFAULT_RESEARCHERS — frozen roster of four
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The locked 4-researcher roster run by default. Callers can override
|
|
97
|
+
* via `opts.researchers`, but the roster is stable enough that widening
|
|
98
|
+
* it would be a breaking change (CLI summary rendering, telemetry
|
|
99
|
+
* aggregation, and the synthesizer prompt all depend on exactly four).
|
|
100
|
+
*/
|
|
101
|
+
export const DEFAULT_RESEARCHERS: readonly ResearcherSpec[] = Object.freeze([
|
|
102
|
+
Object.freeze({
|
|
103
|
+
name: 'design-system-audit' as const,
|
|
104
|
+
prompt:
|
|
105
|
+
'Audit this repo for existing design system surface: design tokens (CSS vars, Tailwind config, JS const exports), components (file names + prop shapes), patterns (recurring UI idioms). Output .design/research/design-system-audit.md with findings organized as: Tokens, Components, Patterns, Gaps.',
|
|
106
|
+
outputPath: '.design/research/design-system-audit.md',
|
|
107
|
+
}),
|
|
108
|
+
Object.freeze({
|
|
109
|
+
name: 'brand-context' as const,
|
|
110
|
+
prompt:
|
|
111
|
+
'Scan this repo for brand signals: README, marketing/landing pages, style guides, voice/tone docs. Infer archetype (per reference/typography.md), voice, visual tone. Output .design/research/brand-context.md.',
|
|
112
|
+
outputPath: '.design/research/brand-context.md',
|
|
113
|
+
}),
|
|
114
|
+
Object.freeze({
|
|
115
|
+
name: 'accessibility-baseline' as const,
|
|
116
|
+
prompt:
|
|
117
|
+
'Scan this repo for WCAG conformance baseline: color contrast, keyboard navigation, ARIA labels, focus management, motion preferences. Output .design/research/accessibility-baseline.md with findings + a conformance score (AA / partial / fail).',
|
|
118
|
+
outputPath: '.design/research/accessibility-baseline.md',
|
|
119
|
+
}),
|
|
120
|
+
Object.freeze({
|
|
121
|
+
name: 'competitive-references' as const,
|
|
122
|
+
prompt:
|
|
123
|
+
'Identify 3-5 peer products in the same domain as this repo. Use WebSearch + WebFetch. For each, extract design patterns worth referencing. Output .design/research/competitive-references.md.',
|
|
124
|
+
outputPath: '.design/research/competitive-references.md',
|
|
125
|
+
}),
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// run — top-level orchestrator
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
/** Default concurrency when not specified. Matches the 4-researcher roster. */
|
|
133
|
+
const DEFAULT_CONCURRENCY = 4;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Bootstrap a `.design/` directory for a fresh project. See module
|
|
137
|
+
* header for the algorithm. Never throws; every failure mode surfaces
|
|
138
|
+
* as `InitRunnerResult.status`.
|
|
139
|
+
*/
|
|
140
|
+
export async function run(opts: InitRunnerOptions): Promise<InitRunnerResult> {
|
|
141
|
+
const logger = getLogger();
|
|
142
|
+
const cwd = resolve(opts.cwd ?? process.cwd());
|
|
143
|
+
const designDir = resolve(cwd, '.design');
|
|
144
|
+
const researchers = opts.researchers ?? DEFAULT_RESEARCHERS;
|
|
145
|
+
|
|
146
|
+
logger.info('init.runner.started', {
|
|
147
|
+
cwd,
|
|
148
|
+
design_dir: designDir,
|
|
149
|
+
researcher_count: researchers.length,
|
|
150
|
+
force: opts.force === true,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ------------------------------------------------------------------
|
|
154
|
+
// 1. Re-init safety check.
|
|
155
|
+
// ------------------------------------------------------------------
|
|
156
|
+
const stateMdPath = resolve(designDir, 'STATE.md');
|
|
157
|
+
let backupDir: string | null = null;
|
|
158
|
+
if (existsSync(stateMdPath)) {
|
|
159
|
+
if (opts.force !== true) {
|
|
160
|
+
const result = buildResult({
|
|
161
|
+
status: 'already-initialized',
|
|
162
|
+
cwd,
|
|
163
|
+
designDir,
|
|
164
|
+
researchers: [],
|
|
165
|
+
stateMdWritten: false,
|
|
166
|
+
designContextMdWritten: false,
|
|
167
|
+
totalUsage: zeroUsage(),
|
|
168
|
+
});
|
|
169
|
+
logger.info('init.runner.completed', logPayloadFor(result));
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
backupDir = backupExistingDesignDir(cwd);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ------------------------------------------------------------------
|
|
176
|
+
// 2. Ensure .design/ + .design/research/ exist.
|
|
177
|
+
// ------------------------------------------------------------------
|
|
178
|
+
ensureDesignDirs(cwd);
|
|
179
|
+
|
|
180
|
+
// ------------------------------------------------------------------
|
|
181
|
+
// 3. Write STATE.md from template.
|
|
182
|
+
// ------------------------------------------------------------------
|
|
183
|
+
const templatePath =
|
|
184
|
+
opts.stateTemplatePath ?? resolveStateTemplatePath() ?? '';
|
|
185
|
+
const stateWritten = templatePath === ''
|
|
186
|
+
? false
|
|
187
|
+
: writeStateFromTemplate({
|
|
188
|
+
cwd,
|
|
189
|
+
templatePath,
|
|
190
|
+
destPath: stateMdPath,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (!stateWritten) {
|
|
194
|
+
const result = buildResult({
|
|
195
|
+
status: 'error',
|
|
196
|
+
cwd,
|
|
197
|
+
designDir,
|
|
198
|
+
researchers: [],
|
|
199
|
+
stateMdWritten: false,
|
|
200
|
+
designContextMdWritten: false,
|
|
201
|
+
totalUsage: zeroUsage(),
|
|
202
|
+
...(backupDir !== null ? { backupDir } : {}),
|
|
203
|
+
});
|
|
204
|
+
logger.error('init.runner.completed', {
|
|
205
|
+
...logPayloadFor(result),
|
|
206
|
+
reason: 'STATE-TEMPLATE.md not found or unreadable',
|
|
207
|
+
template_path: templatePath,
|
|
208
|
+
});
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ------------------------------------------------------------------
|
|
213
|
+
// 4. Spawn researchers in parallel.
|
|
214
|
+
// ------------------------------------------------------------------
|
|
215
|
+
const outcomes = await spawnResearchersParallel(researchers, {
|
|
216
|
+
concurrency: opts.concurrency ?? DEFAULT_CONCURRENCY,
|
|
217
|
+
budget: opts.budget,
|
|
218
|
+
maxTurns: opts.maxTurnsPerResearcher,
|
|
219
|
+
cwd,
|
|
220
|
+
...(opts.runOverride !== undefined
|
|
221
|
+
? { runOverride: opts.runOverride }
|
|
222
|
+
: {}),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const successful = outcomes.filter(
|
|
226
|
+
(o) => o.status === 'completed' && o.output_exists,
|
|
227
|
+
);
|
|
228
|
+
if (successful.length === 0) {
|
|
229
|
+
const totalUsage = aggregateUsage(outcomes);
|
|
230
|
+
const result = buildResult({
|
|
231
|
+
status: 'no-researchers-succeeded',
|
|
232
|
+
cwd,
|
|
233
|
+
designDir,
|
|
234
|
+
researchers: outcomes,
|
|
235
|
+
stateMdWritten: true,
|
|
236
|
+
designContextMdWritten: false,
|
|
237
|
+
totalUsage,
|
|
238
|
+
...(backupDir !== null ? { backupDir } : {}),
|
|
239
|
+
});
|
|
240
|
+
logger.warn('init.runner.completed', {
|
|
241
|
+
...logPayloadFor(result),
|
|
242
|
+
reason: 'all researchers failed or produced no output',
|
|
243
|
+
});
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ------------------------------------------------------------------
|
|
248
|
+
// 5. Load successful researcher outputs and spawn synthesizer.
|
|
249
|
+
// ------------------------------------------------------------------
|
|
250
|
+
const specByName = new Map<ResearcherName, ResearcherSpec>(
|
|
251
|
+
researchers.map((s) => [s.name, s]),
|
|
252
|
+
);
|
|
253
|
+
const synthesizerInputs = successful
|
|
254
|
+
.map((o) => {
|
|
255
|
+
const spec = specByName.get(o.name);
|
|
256
|
+
if (spec === undefined) return null;
|
|
257
|
+
const absPath = resolve(cwd, spec.outputPath);
|
|
258
|
+
let content: string;
|
|
259
|
+
try {
|
|
260
|
+
content = readFileSync(absPath, 'utf8');
|
|
261
|
+
} catch {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
return { name: o.name, path: absPath, content };
|
|
265
|
+
})
|
|
266
|
+
.filter((x): x is { name: ResearcherName; path: string; content: string } =>
|
|
267
|
+
x !== null,
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const synth = await spawnSynthesizer({
|
|
271
|
+
researcherOutputs: synthesizerInputs,
|
|
272
|
+
cwd,
|
|
273
|
+
budget: opts.synthesizerBudget,
|
|
274
|
+
maxTurns: opts.synthesizerMaxTurns,
|
|
275
|
+
...(opts.runOverride !== undefined
|
|
276
|
+
? { runOverride: opts.runOverride }
|
|
277
|
+
: {}),
|
|
278
|
+
...(opts.synthesizerPromptOverride !== undefined
|
|
279
|
+
? { promptOverride: opts.synthesizerPromptOverride }
|
|
280
|
+
: {}),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// ------------------------------------------------------------------
|
|
284
|
+
// 6. Aggregate + return.
|
|
285
|
+
// ------------------------------------------------------------------
|
|
286
|
+
const totalUsage = aggregateUsageAll(outcomes, synth.usage);
|
|
287
|
+
const designContextWritten = synth.status === 'completed';
|
|
288
|
+
const status: InitStatus = 'completed';
|
|
289
|
+
|
|
290
|
+
const result = buildResult({
|
|
291
|
+
status,
|
|
292
|
+
cwd,
|
|
293
|
+
designDir,
|
|
294
|
+
researchers: outcomes,
|
|
295
|
+
stateMdWritten: true,
|
|
296
|
+
designContextMdWritten: designContextWritten,
|
|
297
|
+
totalUsage,
|
|
298
|
+
...(backupDir !== null ? { backupDir } : {}),
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
if (synth.status !== 'completed') {
|
|
302
|
+
logger.warn('init.runner.completed', {
|
|
303
|
+
...logPayloadFor(result),
|
|
304
|
+
synthesizer_error: synth.error ?? 'unknown',
|
|
305
|
+
});
|
|
306
|
+
} else {
|
|
307
|
+
logger.info('init.runner.completed', logPayloadFor(result));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
// Helpers
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
function zeroUsage(): InitRunnerResult['total_usage'] {
|
|
318
|
+
return { input_tokens: 0, output_tokens: 0, usd_cost: 0 };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function aggregateUsage(
|
|
322
|
+
outcomes: readonly ResearcherOutcome[],
|
|
323
|
+
): InitRunnerResult['total_usage'] {
|
|
324
|
+
let input = 0;
|
|
325
|
+
let output = 0;
|
|
326
|
+
let cost = 0;
|
|
327
|
+
for (const o of outcomes) {
|
|
328
|
+
input += o.usage.input_tokens;
|
|
329
|
+
output += o.usage.output_tokens;
|
|
330
|
+
cost += o.usage.usd_cost;
|
|
331
|
+
}
|
|
332
|
+
return { input_tokens: input, output_tokens: output, usd_cost: cost };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function aggregateUsageAll(
|
|
336
|
+
outcomes: readonly ResearcherOutcome[],
|
|
337
|
+
synthUsage: { input_tokens: number; output_tokens: number; usd_cost: number },
|
|
338
|
+
): InitRunnerResult['total_usage'] {
|
|
339
|
+
const researcherUsage = aggregateUsage(outcomes);
|
|
340
|
+
return {
|
|
341
|
+
input_tokens: researcherUsage.input_tokens + synthUsage.input_tokens,
|
|
342
|
+
output_tokens: researcherUsage.output_tokens + synthUsage.output_tokens,
|
|
343
|
+
usd_cost: researcherUsage.usd_cost + synthUsage.usd_cost,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
interface BuildResultInput {
|
|
348
|
+
readonly status: InitStatus;
|
|
349
|
+
readonly cwd: string;
|
|
350
|
+
readonly designDir: string;
|
|
351
|
+
readonly researchers: readonly ResearcherOutcome[];
|
|
352
|
+
readonly stateMdWritten: boolean;
|
|
353
|
+
readonly designContextMdWritten: boolean;
|
|
354
|
+
readonly totalUsage: InitRunnerResult['total_usage'];
|
|
355
|
+
readonly backupDir?: string;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function buildResult(input: BuildResultInput): InitRunnerResult {
|
|
359
|
+
const scaffold: InitRunnerResult['scaffold'] = input.backupDir !== undefined
|
|
360
|
+
? {
|
|
361
|
+
state_md_written: input.stateMdWritten,
|
|
362
|
+
design_context_md_written: input.designContextMdWritten,
|
|
363
|
+
backup_dir: input.backupDir,
|
|
364
|
+
}
|
|
365
|
+
: {
|
|
366
|
+
state_md_written: input.stateMdWritten,
|
|
367
|
+
design_context_md_written: input.designContextMdWritten,
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
return Object.freeze({
|
|
371
|
+
status: input.status,
|
|
372
|
+
cwd: input.cwd,
|
|
373
|
+
design_dir: input.designDir,
|
|
374
|
+
researchers: input.researchers,
|
|
375
|
+
scaffold,
|
|
376
|
+
total_usage: input.totalUsage,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function logPayloadFor(result: InitRunnerResult): Record<string, unknown> {
|
|
381
|
+
return {
|
|
382
|
+
status: result.status,
|
|
383
|
+
cwd: result.cwd,
|
|
384
|
+
design_dir: result.design_dir,
|
|
385
|
+
researcher_total: result.researchers.length,
|
|
386
|
+
researcher_succeeded: result.researchers.filter(
|
|
387
|
+
(r) => r.status === 'completed' && r.output_exists,
|
|
388
|
+
).length,
|
|
389
|
+
state_md_written: result.scaffold.state_md_written,
|
|
390
|
+
design_context_md_written: result.scaffold.design_context_md_written,
|
|
391
|
+
...(result.scaffold.backup_dir !== undefined
|
|
392
|
+
? { backup_dir: result.scaffold.backup_dir }
|
|
393
|
+
: {}),
|
|
394
|
+
total_usage: result.total_usage,
|
|
395
|
+
};
|
|
396
|
+
}
|