@hegemonart/get-design-done 1.31.5 → 1.33.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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +63 -0
- package/NOTICE +81 -5
- package/README.md +25 -0
- package/SKILL.md +4 -0
- package/hooks/hooks.json +9 -0
- package/hooks/inject-using-gdd.sh +72 -0
- package/hooks/run-hook.cmd +35 -0
- package/package.json +2 -2
- package/reference/schemas/events.schema.json +63 -1
- package/reference/schemas/pressure-scenario.schema.json +69 -0
- package/scripts/lib/health-mirror/index.cjs +79 -1
- package/scripts/lib/skill-behavior/runner.cjs +187 -0
- package/scripts/lib/skill-behavior/stub-invoker.cjs +95 -0
- package/scripts/lib/skill-behavior/telemetry.cjs +379 -0
- package/sdk/mcp/gdd-mcp/server.js +42 -0
- package/skills/audit/SKILL.md +13 -0
- package/skills/brief/SKILL.md +25 -0
- package/skills/design/SKILL.md +17 -0
- package/skills/discuss/SKILL.md +13 -0
- package/skills/explore/SKILL.md +17 -0
- package/skills/health/SKILL.md +6 -0
- package/skills/plan/SKILL.md +25 -0
- package/skills/router/SKILL.md +4 -0
- package/skills/router/router-pick-emitter.md +78 -0
- package/skills/using-gdd/SKILL.md +78 -0
- package/skills/verify/SKILL.md +17 -0
- package/scripts/lib/cli/index.ts +0 -29
- package/scripts/lib/error-classifier.cjs +0 -29
- package/scripts/lib/event-stream/index.ts +0 -29
- package/scripts/lib/gdd-errors/index.ts +0 -29
- package/scripts/lib/gdd-state/index.ts +0 -29
- package/scripts/lib/iteration-budget.cjs +0 -29
- package/scripts/lib/jittered-backoff.cjs +0 -29
- package/scripts/lib/lockfile.cjs +0 -29
- package/scripts/mcp-servers/gdd-mcp/server.ts +0 -35
- package/scripts/mcp-servers/gdd-state/server.ts +0 -34
|
@@ -8,13 +8,14 @@
|
|
|
8
8
|
// Surface:
|
|
9
9
|
// async getHealthChecks(rootDir) → { checks: HealthCheck[] }
|
|
10
10
|
//
|
|
11
|
-
// The
|
|
11
|
+
// The 7 checks (in stable order) are:
|
|
12
12
|
// 1. claude_md — CLAUDE.md presence
|
|
13
13
|
// 2. planning_dir — .planning/ presence
|
|
14
14
|
// 3. design_dir — .design/ presence
|
|
15
15
|
// 4. package_json — package.json present AND parseable
|
|
16
16
|
// 5. issue_reporter — kill-switch state (Plan 30-06 / D-08)
|
|
17
17
|
// 6. figma_extract — extract readiness + Free-tier signal (Plan 31-09)
|
|
18
|
+
// 7. skill_discipline — using-gdd bootstrap + SessionStart inject (Plan 32-07)
|
|
18
19
|
//
|
|
19
20
|
// Check 5 was added in Plan 30-06 — surfaces the report-issue kill-switch
|
|
20
21
|
// (env or config disable) so users can verify why the command is
|
|
@@ -34,6 +35,17 @@
|
|
|
34
35
|
// logged, or placed in the detail. The Free-tier state is derived from a LOCAL
|
|
35
36
|
// signal only (a prior pull's _meta.json recording a 403/skip on the Variables
|
|
36
37
|
// endpoint) — never a live network call (health-mirror is pure read-only).
|
|
38
|
+
//
|
|
39
|
+
// Check 7 was added in Plan 32-07 — surfaces whether the skill-discipline
|
|
40
|
+
// bootstrap (Phase 32) is live so a user can confirm the using-gdd SessionStart
|
|
41
|
+
// inject is wired. The detail line is one of three exact strings:
|
|
42
|
+
// - "skill-discipline: ready" (using-gdd present AND hooks.json
|
|
43
|
+
// SessionStart wires inject-using-gdd.sh)
|
|
44
|
+
// - "skill-discipline: missing using-gdd" (skills/using-gdd/SKILL.md absent)
|
|
45
|
+
// - "skill-discipline: hook not wired" (skill present but no SessionStart
|
|
46
|
+
// inject-using-gdd entry)
|
|
47
|
+
// status: 'ok' when ready, 'warn' otherwise. PURE read-only (rootDir-relative
|
|
48
|
+
// file + JSON inspection only) — NEVER throws, NEVER networks.
|
|
37
49
|
|
|
38
50
|
const fs = require('node:fs');
|
|
39
51
|
const path = require('node:path');
|
|
@@ -174,9 +186,75 @@ async function getHealthChecks(rootDir) {
|
|
|
174
186
|
checks.push({ name: 'figma_extract', status, detail });
|
|
175
187
|
}
|
|
176
188
|
|
|
189
|
+
// 7. skill_discipline — using-gdd bootstrap + SessionStart inject (Plan 32-07).
|
|
190
|
+
// Reports exactly one of three states. PURE read-only: file existence +
|
|
191
|
+
// hooks.json JSON inspection only. NEVER throws, NEVER networks (every read
|
|
192
|
+
// is wrapped defensively like the figma_extract check above).
|
|
193
|
+
{
|
|
194
|
+
const skillPresent = fileExists(
|
|
195
|
+
path.join(rootDir, 'skills', 'using-gdd', 'SKILL.md')
|
|
196
|
+
);
|
|
197
|
+
const hookWired = skillPresent && sessionStartWiresInject(rootDir);
|
|
198
|
+
|
|
199
|
+
let detail;
|
|
200
|
+
let status;
|
|
201
|
+
if (!skillPresent) {
|
|
202
|
+
detail = 'skill-discipline: missing using-gdd';
|
|
203
|
+
status = 'warn';
|
|
204
|
+
} else if (!hookWired) {
|
|
205
|
+
detail = 'skill-discipline: hook not wired';
|
|
206
|
+
status = 'warn';
|
|
207
|
+
} else {
|
|
208
|
+
detail = 'skill-discipline: ready';
|
|
209
|
+
status = 'ok';
|
|
210
|
+
}
|
|
211
|
+
checks.push({ name: 'skill_discipline', status, detail });
|
|
212
|
+
}
|
|
213
|
+
|
|
177
214
|
return { checks };
|
|
178
215
|
}
|
|
179
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Does hooks/hooks.json wire the inject-using-gdd SessionStart entry?
|
|
219
|
+
* PURE read-only JSON inspection. Defensive: a missing/garbage hooks.json or an
|
|
220
|
+
* unexpected shape returns false (→ "hook not wired") rather than throwing — the
|
|
221
|
+
* health probe must never crash on this check. NEVER networks.
|
|
222
|
+
*
|
|
223
|
+
* @param {string} rootDir project root passed to getHealthChecks
|
|
224
|
+
* @returns {boolean} true iff a SessionStart hook command references inject-using-gdd
|
|
225
|
+
*/
|
|
226
|
+
function sessionStartWiresInject(rootDir) {
|
|
227
|
+
try {
|
|
228
|
+
const p = path.join(rootDir, 'hooks', 'hooks.json');
|
|
229
|
+
let hooks;
|
|
230
|
+
try {
|
|
231
|
+
hooks = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
232
|
+
} catch {
|
|
233
|
+
return false; // missing/garbage hooks.json → not wired
|
|
234
|
+
}
|
|
235
|
+
const sessionStart =
|
|
236
|
+
hooks && hooks.hooks && Array.isArray(hooks.hooks.SessionStart)
|
|
237
|
+
? hooks.hooks.SessionStart
|
|
238
|
+
: [];
|
|
239
|
+
for (const entry of sessionStart) {
|
|
240
|
+
const inner = entry && Array.isArray(entry.hooks) ? entry.hooks : [];
|
|
241
|
+
for (const h of inner) {
|
|
242
|
+
if (
|
|
243
|
+
h &&
|
|
244
|
+
typeof h.command === 'string' &&
|
|
245
|
+
/inject-using-gdd/.test(h.command)
|
|
246
|
+
) {
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return false;
|
|
252
|
+
} catch {
|
|
253
|
+
// Absolute safety net — never crash the health probe on this check.
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
180
258
|
/**
|
|
181
259
|
* Free-tier signal (LOCAL only — never a network call). The raw-pull stage
|
|
182
260
|
* (scripts/lib/figma-extract/pull.cjs) writes a _meta.json per file key under
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runner.cjs — manifest-driven pressure-scenario runner (Plan 33-01).
|
|
3
|
+
*
|
|
4
|
+
* The ROOT engine of Phase 33: every later plan (33-03 scenarios, 33-04 A/B,
|
|
5
|
+
* 33-05 telemetry) builds on this. It loads a parsed pressure-scenario
|
|
6
|
+
* manifest, invokes an agent via an INJECTABLE `invokeAgent(prompt, opts) ->
|
|
7
|
+
* { text }` seam, runs N attempts (default 3), scores each response against
|
|
8
|
+
* the manifest's expected_compliance[] (must-match regexes) and
|
|
9
|
+
* expected_violations[] (failure regexes), applies a STRICT 2/3 majority
|
|
10
|
+
* rule, and emits a structured result.
|
|
11
|
+
*
|
|
12
|
+
* D-03 — invoker-agnostic, NO direct Anthropic SDK dependency:
|
|
13
|
+
* This file deps on node:fs + node:path ONLY. It NEVER requires the
|
|
14
|
+
* Anthropic SDK package. The default invoker is the deterministic stub at
|
|
15
|
+
* ./stub-invoker.cjs so CI/tests run with no API key and no network. A
|
|
16
|
+
* maintainer later wires a real invoker (peer-CLI ACP spawn or a thin keyed
|
|
17
|
+
* SDK adapter) by passing opts.invokeAgent. (The guard test asserts the
|
|
18
|
+
* exact package name never appears in this source.)
|
|
19
|
+
*
|
|
20
|
+
* Purity / injectability:
|
|
21
|
+
* invokeAgent, the clock (now), and fs are all injectable via opts so every
|
|
22
|
+
* test drives the stub with a fixed clock.
|
|
23
|
+
*
|
|
24
|
+
* Result (EXACT shape):
|
|
25
|
+
* {
|
|
26
|
+
* scenario: string, // = manifest.name
|
|
27
|
+
* attempts: Array<{ // one entry per attempt (length === attempts)
|
|
28
|
+
* text: string,
|
|
29
|
+
* pass: boolean, // ALL compliance matched AND zero violations
|
|
30
|
+
* compliance_hits: number, // # expected_compliance regexes matching this text
|
|
31
|
+
* violation_hits: number, // # expected_violations regexes matching this text
|
|
32
|
+
* }>,
|
|
33
|
+
* pass: boolean, // MAJORITY: (#passing attempts) * 2 > attempts.length
|
|
34
|
+
* compliance_hits: number, // aggregate sum across attempts
|
|
35
|
+
* violation_hits: number, // aggregate sum across attempts
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
38
|
+
* Pattern reference (NOT a dependency): scripts/lib/event-chain.cjs shows the
|
|
39
|
+
* house CommonJS idiom (defensive fs, pure functions). Style mirrored, not imported.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
'use strict';
|
|
43
|
+
|
|
44
|
+
const nodeFs = require('node:fs');
|
|
45
|
+
const path = require('node:path');
|
|
46
|
+
|
|
47
|
+
const DEFAULT_ATTEMPTS = 3;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Load a pressure-scenario manifest. Accepts either an already-parsed object
|
|
51
|
+
* (returned as-is) or a path to a JSON file (read + parsed via the injectable
|
|
52
|
+
* fs). Keeping this injectable lets later plans (33-03) load real manifest
|
|
53
|
+
* files while tests pass inline objects.
|
|
54
|
+
*
|
|
55
|
+
* @param {object | string} input parsed manifest OR a path to a JSON manifest
|
|
56
|
+
* @param {{ fs?: typeof import('node:fs') }} [deps]
|
|
57
|
+
* @returns {object} the parsed manifest
|
|
58
|
+
*/
|
|
59
|
+
function loadManifest(input, deps) {
|
|
60
|
+
if (input && typeof input === 'object') {
|
|
61
|
+
return input;
|
|
62
|
+
}
|
|
63
|
+
if (typeof input === 'string') {
|
|
64
|
+
const fs = (deps && deps.fs) || nodeFs;
|
|
65
|
+
const abs = path.isAbsolute(input) ? input : path.resolve(process.cwd(), input);
|
|
66
|
+
const raw = fs.readFileSync(abs, 'utf8');
|
|
67
|
+
return JSON.parse(raw);
|
|
68
|
+
}
|
|
69
|
+
throw new TypeError('loadManifest: input must be a parsed manifest object or a path string');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Compile an array of regex SOURCE strings into RegExp objects. Manifests
|
|
74
|
+
* author patterns as plain strings (NOT pre-compiled) so they stay JSON-safe;
|
|
75
|
+
* the runner owns compilation.
|
|
76
|
+
*
|
|
77
|
+
* @param {unknown} sources
|
|
78
|
+
* @returns {RegExp[]}
|
|
79
|
+
*/
|
|
80
|
+
function compilePatterns(sources) {
|
|
81
|
+
if (!Array.isArray(sources)) return [];
|
|
82
|
+
return sources.map((src) => new RegExp(String(src)));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Coerce an invoker's `.text` to a string. A non-string (or absent) value
|
|
87
|
+
* becomes '' so scoring never throws and is treated as a compliance-miss.
|
|
88
|
+
*
|
|
89
|
+
* @param {unknown} response
|
|
90
|
+
* @returns {string}
|
|
91
|
+
*/
|
|
92
|
+
function textOf(response) {
|
|
93
|
+
if (response && typeof response.text === 'string') return response.text;
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Score a single response text against pre-compiled compliance/violation
|
|
99
|
+
* regexes.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} text
|
|
102
|
+
* @param {RegExp[]} complianceRes
|
|
103
|
+
* @param {RegExp[]} violationRes
|
|
104
|
+
* @returns {{ text: string, pass: boolean, compliance_hits: number, violation_hits: number }}
|
|
105
|
+
*/
|
|
106
|
+
function scoreAttempt(text, complianceRes, violationRes) {
|
|
107
|
+
const compliance_hits = complianceRes.filter((re) => re.test(text)).length;
|
|
108
|
+
const violation_hits = violationRes.filter((re) => re.test(text)).length;
|
|
109
|
+
// An attempt PASSES iff ALL compliance regexes matched AND zero violations did.
|
|
110
|
+
const pass = compliance_hits === complianceRes.length && violation_hits === 0;
|
|
111
|
+
return { text, pass, compliance_hits, violation_hits };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Run a pressure scenario: invoke the seam N times, score each response, and
|
|
116
|
+
* apply a strict majority rule.
|
|
117
|
+
*
|
|
118
|
+
* @param {object} manifest parsed pressure-scenario manifest
|
|
119
|
+
* { name, target_skill, pressures[], setup_prompt, expected_compliance[], expected_violations[] }
|
|
120
|
+
* @param {{
|
|
121
|
+
* invokeAgent?: (prompt: string, opts: object) => { text: string },
|
|
122
|
+
* attempts?: number,
|
|
123
|
+
* now?: () => number,
|
|
124
|
+
* fs?: typeof import('node:fs'),
|
|
125
|
+
* }} [opts]
|
|
126
|
+
* @returns {{
|
|
127
|
+
* scenario: string,
|
|
128
|
+
* attempts: Array<{ text: string, pass: boolean, compliance_hits: number, violation_hits: number }>,
|
|
129
|
+
* pass: boolean,
|
|
130
|
+
* compliance_hits: number,
|
|
131
|
+
* violation_hits: number,
|
|
132
|
+
* }}
|
|
133
|
+
*/
|
|
134
|
+
function runScenario(manifest, opts) {
|
|
135
|
+
const o = opts || {};
|
|
136
|
+
// D-03: default to the deterministic stub invoker — never the real SDK.
|
|
137
|
+
const invokeAgent = o.invokeAgent || require('./stub-invoker.cjs').invokeAgent;
|
|
138
|
+
const attempts =
|
|
139
|
+
Number.isInteger(o.attempts) && o.attempts > 0 ? o.attempts : DEFAULT_ATTEMPTS;
|
|
140
|
+
// Injectable clock (reserved for future telemetry timestamps; called so the
|
|
141
|
+
// seam is exercised and a fixed now() is honored).
|
|
142
|
+
const now = typeof o.now === 'function' ? o.now : Date.now;
|
|
143
|
+
|
|
144
|
+
const complianceRes = compilePatterns(manifest && manifest.expected_compliance);
|
|
145
|
+
const violationRes = compilePatterns(manifest && manifest.expected_violations);
|
|
146
|
+
const scenario = manifest && manifest.name;
|
|
147
|
+
const prompt = (manifest && manifest.setup_prompt) || '';
|
|
148
|
+
|
|
149
|
+
const attemptResults = [];
|
|
150
|
+
for (let i = 0; i < attempts; i++) {
|
|
151
|
+
now(); // exercise the injectable clock (deterministic under a fixed now)
|
|
152
|
+
let text = '';
|
|
153
|
+
try {
|
|
154
|
+
// Pass the scenario key through so the stub (or a real invoker) can key on it.
|
|
155
|
+
const response = invokeAgent(prompt, { scenario, attempt: i });
|
|
156
|
+
text = textOf(response);
|
|
157
|
+
} catch (_err) {
|
|
158
|
+
// A thrown invoker must NOT crash the run — record a failed empty attempt.
|
|
159
|
+
text = '';
|
|
160
|
+
}
|
|
161
|
+
attemptResults.push(scoreAttempt(text, complianceRes, violationRes));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const passed = attemptResults.filter((a) => a.pass).length;
|
|
165
|
+
// STRICT majority: 2/3 and 3/3 pass; 0/3 and 1/3 fail.
|
|
166
|
+
const pass = passed * 2 > attemptResults.length;
|
|
167
|
+
|
|
168
|
+
const compliance_hits = attemptResults.reduce((sum, a) => sum + a.compliance_hits, 0);
|
|
169
|
+
const violation_hits = attemptResults.reduce((sum, a) => sum + a.violation_hits, 0);
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
scenario,
|
|
173
|
+
attempts: attemptResults,
|
|
174
|
+
pass,
|
|
175
|
+
compliance_hits,
|
|
176
|
+
violation_hits,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = {
|
|
181
|
+
runScenario,
|
|
182
|
+
loadManifest,
|
|
183
|
+
// Exposed for unit-level reuse / later plans; not part of the core contract.
|
|
184
|
+
scoreAttempt,
|
|
185
|
+
compilePatterns,
|
|
186
|
+
DEFAULT_ATTEMPTS,
|
|
187
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stub-invoker.cjs — deterministic, scenario-keyed agent invoker (Plan 33-01).
|
|
3
|
+
*
|
|
4
|
+
* The DEFAULT invokeAgent seam for `runner.cjs` (D-03): the runner is
|
|
5
|
+
* invoker-agnostic and exposes an injectable `invokeAgent(prompt, opts) ->
|
|
6
|
+
* { text }` seam. A maintainer later wires a REAL invoker (a peer-CLI ACP
|
|
7
|
+
* spawn of a local `claude`/`codex`, or a thin keyed SDK adapter); this stub
|
|
8
|
+
* is what every Phase-33 CI/structural test drives so runs are reproducible
|
|
9
|
+
* with NO API key and NO network.
|
|
10
|
+
*
|
|
11
|
+
* Determinism contract:
|
|
12
|
+
* * NO randomness, NO network, NO @anthropic-ai/sdk.
|
|
13
|
+
* * A canned response is resolved by a KEY derived from
|
|
14
|
+
* opts.scenario || opts.stubKey, falling back to scanning `prompt` for a
|
|
15
|
+
* registered key marker.
|
|
16
|
+
* * An UNKNOWN key returns a neutral { text: '' } so the runner never throws.
|
|
17
|
+
*
|
|
18
|
+
* Tests MAY instead pass their own inline invokeAgent to runScenario — both
|
|
19
|
+
* paths are valid (D-03). This module is the no-arg default.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
// Internal canned-response table: key -> response text. Seeded with one
|
|
25
|
+
// illustrative scenario; callers extend it via register().
|
|
26
|
+
const TABLE = new Map([
|
|
27
|
+
// A neutral, compliance-shaped sample so the default stub is non-empty for a
|
|
28
|
+
// known demo key. Real scenarios register their own canned text.
|
|
29
|
+
[
|
|
30
|
+
'runner-demo',
|
|
31
|
+
'A <HARD-GATE> blocks me — I must write the brief before any other stage.',
|
|
32
|
+
],
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Seed or overwrite a canned response for a scenario key.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} key scenario name / stub key
|
|
39
|
+
* @param {string} text canned response text the stub returns for that key
|
|
40
|
+
* @returns {void}
|
|
41
|
+
*/
|
|
42
|
+
function register(key, text) {
|
|
43
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
44
|
+
throw new TypeError('register: key must be a non-empty string');
|
|
45
|
+
}
|
|
46
|
+
TABLE.set(key, typeof text === 'string' ? text : String(text == null ? '' : text));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve a response key from opts, then (as a fallback) by scanning the
|
|
51
|
+
* prompt for any registered key as a substring marker.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} prompt
|
|
54
|
+
* @param {{scenario?: string, stubKey?: string} | undefined} opts
|
|
55
|
+
* @returns {string | undefined}
|
|
56
|
+
*/
|
|
57
|
+
function resolveKey(prompt, opts) {
|
|
58
|
+
if (opts && typeof opts.scenario === 'string' && opts.scenario.length > 0) {
|
|
59
|
+
return opts.scenario;
|
|
60
|
+
}
|
|
61
|
+
if (opts && typeof opts.stubKey === 'string' && opts.stubKey.length > 0) {
|
|
62
|
+
return opts.stubKey;
|
|
63
|
+
}
|
|
64
|
+
if (typeof prompt === 'string' && prompt.length > 0) {
|
|
65
|
+
for (const key of TABLE.keys()) {
|
|
66
|
+
if (prompt.includes(key)) return key;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Deterministic invokeAgent-shaped function. Returns a canned { text } for a
|
|
74
|
+
* known scenario key, or a neutral { text: '' } for an unknown key (so the
|
|
75
|
+
* runner can score it as a compliance-miss without throwing).
|
|
76
|
+
*
|
|
77
|
+
* @param {string} prompt
|
|
78
|
+
* @param {{scenario?: string, stubKey?: string}} [opts]
|
|
79
|
+
* @returns {{ text: string }}
|
|
80
|
+
*/
|
|
81
|
+
function invokeAgent(prompt, opts) {
|
|
82
|
+
const key = resolveKey(prompt, opts);
|
|
83
|
+
if (key !== undefined && TABLE.has(key)) {
|
|
84
|
+
return { text: TABLE.get(key) };
|
|
85
|
+
}
|
|
86
|
+
// Unknown key -> neutral default; never throw.
|
|
87
|
+
return { text: '' };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
invokeAgent,
|
|
92
|
+
register,
|
|
93
|
+
// Exposed for advanced callers/tests that want to inspect or reset seeds.
|
|
94
|
+
_table: TABLE,
|
|
95
|
+
};
|