crewhaus 0.1.7 → 0.2.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.
Files changed (136) hide show
  1. package/README.md +10 -3
  2. package/dist/advice-apply.d.ts +182 -0
  3. package/dist/advice-apply.js +286 -0
  4. package/dist/advise-rules.d.ts +348 -0
  5. package/dist/advise-rules.js +905 -0
  6. package/dist/alert-sink.d.ts +48 -0
  7. package/dist/alert-sink.js +86 -0
  8. package/dist/approval-gate.d.ts +127 -0
  9. package/dist/approval-gate.js +254 -0
  10. package/dist/audit-verify.d.ts +69 -0
  11. package/dist/audit-verify.js +97 -0
  12. package/dist/autodistill.d.ts +113 -0
  13. package/dist/autodistill.js +256 -0
  14. package/dist/channel-provision.d.ts +360 -0
  15. package/dist/channel-provision.js +881 -0
  16. package/dist/ci-scaffold.d.ts +31 -0
  17. package/dist/ci-scaffold.js +343 -0
  18. package/dist/compile-check.d.ts +90 -0
  19. package/dist/compile-check.js +285 -0
  20. package/dist/compliance-schedule.d.ts +35 -0
  21. package/dist/compliance-schedule.js +36 -0
  22. package/dist/context-pressure.d.ts +80 -0
  23. package/dist/context-pressure.js +166 -0
  24. package/dist/dataset-mine.d.ts +172 -0
  25. package/dist/dataset-mine.js +403 -0
  26. package/dist/datasets.d.ts +124 -0
  27. package/dist/datasets.js +260 -0
  28. package/dist/deploy-canary.d.ts +83 -0
  29. package/dist/deploy-canary.js +87 -0
  30. package/dist/doctor-checks.d.ts +33 -0
  31. package/dist/doctor-checks.js +92 -0
  32. package/dist/doctor-detect.d.ts +108 -0
  33. package/dist/doctor-detect.js +214 -0
  34. package/dist/doctor-fix.d.ts +81 -0
  35. package/dist/doctor-fix.js +164 -0
  36. package/dist/egress-triage.d.ts +121 -0
  37. package/dist/egress-triage.js +261 -0
  38. package/dist/eval-bridge.d.ts +114 -0
  39. package/dist/eval-bridge.js +158 -0
  40. package/dist/eval-coverage.d.ts +140 -0
  41. package/dist/eval-coverage.js +428 -0
  42. package/dist/eval-history.d.ts +48 -0
  43. package/dist/eval-history.js +157 -0
  44. package/dist/eval-matrix.d.ts +80 -0
  45. package/dist/eval-matrix.js +182 -0
  46. package/dist/eval-sentinel.d.ts +65 -0
  47. package/dist/eval-sentinel.js +132 -0
  48. package/dist/faq.d.ts +68 -0
  49. package/dist/faq.js +168 -0
  50. package/dist/feedback.d.ts +246 -0
  51. package/dist/feedback.js +612 -0
  52. package/dist/fewshot.d.ts +83 -0
  53. package/dist/fewshot.js +158 -0
  54. package/dist/fleet.d.ts +207 -0
  55. package/dist/fleet.js +488 -0
  56. package/dist/flywheel.d.ts +193 -0
  57. package/dist/flywheel.js +519 -0
  58. package/dist/graders-suggest.d.ts +186 -0
  59. package/dist/graders-suggest.js +658 -0
  60. package/dist/incident.d.ts +99 -0
  61. package/dist/incident.js +217 -0
  62. package/dist/index.d.ts +9 -1
  63. package/dist/index.js +11727 -740
  64. package/dist/init-interactive.d.ts +105 -0
  65. package/dist/init-interactive.js +208 -0
  66. package/dist/intents.d.ts +105 -0
  67. package/dist/intents.js +292 -0
  68. package/dist/judge-calibrate.d.ts +137 -0
  69. package/dist/judge-calibrate.js +247 -0
  70. package/dist/justification-calibrate.d.ts +150 -0
  71. package/dist/justification-calibrate.js +262 -0
  72. package/dist/justification-gate.d.ts +27 -6
  73. package/dist/justification-gate.js +30 -6
  74. package/dist/knowledge-sync.d.ts +179 -0
  75. package/dist/knowledge-sync.js +551 -0
  76. package/dist/lessons.d.ts +87 -0
  77. package/dist/lessons.js +207 -0
  78. package/dist/lint.d.ts +127 -0
  79. package/dist/lint.js +226 -0
  80. package/dist/loadtest.d.ts +114 -0
  81. package/dist/loadtest.js +196 -0
  82. package/dist/marketplace-cli.d.ts +110 -0
  83. package/dist/marketplace-cli.js +250 -0
  84. package/dist/mcp-doctor.d.ts +121 -0
  85. package/dist/mcp-doctor.js +249 -0
  86. package/dist/model-scan.d.ts +116 -0
  87. package/dist/model-scan.js +226 -0
  88. package/dist/onchain-tune.d.ts +164 -0
  89. package/dist/onchain-tune.js +346 -0
  90. package/dist/permissions-suggest.d.ts +126 -0
  91. package/dist/permissions-suggest.js +333 -0
  92. package/dist/pii-tune.d.ts +107 -0
  93. package/dist/pii-tune.js +122 -0
  94. package/dist/propose.d.ts +117 -0
  95. package/dist/propose.js +184 -0
  96. package/dist/refresh-goldens.d.ts +82 -0
  97. package/dist/refresh-goldens.js +221 -0
  98. package/dist/regression-pin.d.ts +160 -0
  99. package/dist/regression-pin.js +281 -0
  100. package/dist/retention.d.ts +193 -0
  101. package/dist/retention.js +607 -0
  102. package/dist/retire.d.ts +118 -0
  103. package/dist/retire.js +291 -0
  104. package/dist/right-size.d.ts +100 -0
  105. package/dist/right-size.js +123 -0
  106. package/dist/scaffold-evals.d.ts +138 -0
  107. package/dist/scaffold-evals.js +410 -0
  108. package/dist/scope-audit-drift.d.ts +139 -0
  109. package/dist/scope-audit-drift.js +260 -0
  110. package/dist/security-corpus.d.ts +237 -0
  111. package/dist/security-corpus.js +516 -0
  112. package/dist/security-digest.d.ts +173 -0
  113. package/dist/security-digest.js +650 -0
  114. package/dist/sessions-index.d.ts +27 -0
  115. package/dist/sessions-index.js +51 -0
  116. package/dist/slo-doctor.d.ts +67 -0
  117. package/dist/slo-doctor.js +119 -0
  118. package/dist/slo-sink.d.ts +96 -0
  119. package/dist/slo-sink.js +107 -0
  120. package/dist/spec-changelog.d.ts +102 -0
  121. package/dist/spec-changelog.js +237 -0
  122. package/dist/state-backup.d.ts +223 -0
  123. package/dist/state-backup.js +648 -0
  124. package/dist/tools-cli.d.ts +170 -0
  125. package/dist/tools-cli.js +298 -0
  126. package/dist/triage.d.ts +202 -0
  127. package/dist/triage.js +403 -0
  128. package/dist/upgrade.d.ts +57 -0
  129. package/dist/upgrade.js +113 -0
  130. package/dist/version.d.ts +6 -0
  131. package/dist/version.js +27 -0
  132. package/dist/voice-eval.d.ts +138 -0
  133. package/dist/voice-eval.js +309 -0
  134. package/dist/watch.d.ts +58 -0
  135. package/dist/watch.js +97 -0
  136. package/package.json +89 -64
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Item 13 — `crewhaus scaffold-evals` + `init --with-evals`: day-one eval
3
+ * assets generated FROM the spec itself, so a fresh harness can run
4
+ * `crewhaus eval` before its first user rating ever lands.
5
+ *
6
+ * Two generation modes share one pipeline:
7
+ *
8
+ * - **Model mode** (credentials for the spec's model — or an explicit
9
+ * `--model` — are present): ONE model call through the same model-router
10
+ * path `optimize --mutator claude` uses (`resolveModel`) asks for N
11
+ * realistic user prompts. The call itself lives in the CLI entry file
12
+ * behind an injectable seam; this module owns only the pure halves — the
13
+ * meta-prompt ({@link buildSampleGenerationPrompt}) and the tolerant
14
+ * response parser ({@link parseModelSampleInputs}) — so the model path is
15
+ * unit-testable without a provider.
16
+ * - **Template mode** (offline, and ALWAYS for `init --with-evals` — init
17
+ * must never require credentials): {@link taskPhrasesFromInstructions}
18
+ * parses the instruction sentences into task phrases and
19
+ * {@link templateSampleInputs} wraps them in deterministic prompt
20
+ * templates. Same inputs → same outputs, byte for byte.
21
+ *
22
+ * Graders reuse feedback.ts's synthesis machinery as a spec-driven
23
+ * front-end: online mode emits ONE `llm_judge` rubric with all five anchors
24
+ * pre-filled from the spec's stated goals; offline mode emits the same safe
25
+ * non-empty-answer floor grader `distill` falls back to
26
+ * (`synthesizeGraders([])`). Either way exactly one grader is emitted —
27
+ * stacking graders hard-ANDs their scores (the eval-grader `all(...)`
28
+ * min-collapse), exactly the gotcha distill's output already documents.
29
+ *
30
+ * Kept in a side-effect-free module (the CLI entry file runs an argv switch
31
+ * on import) mirroring `feedback.ts` / `datasets.ts`; all filesystem access
32
+ * and the actual model call live in `apps/cli/src/index.ts`.
33
+ */
34
+ import type { Sample } from "@crewhaus/eval-dataset";
35
+ import { type GradersConfigObject } from "./feedback";
36
+ /** Thrown on a spec this scaffold cannot derive eval assets from (and on
37
+ * invalid flag values). The CLI entry file routes it through `die()`;
38
+ * tests assert on `.message` without the process exiting. */
39
+ export declare class ScaffoldEvalsError extends Error {
40
+ readonly name = "ScaffoldEvalsError";
41
+ }
42
+ /** Default number of Sample stubs `scaffold-evals` generates. */
43
+ export declare const DEFAULT_SCAFFOLD_SAMPLES = 8;
44
+ /** Header for the scaffolded graders.yaml (threaded through feedback.ts's
45
+ * `gradersConfigToYaml`, replacing its distill-specific default). */
46
+ export declare const SCAFFOLD_GRADERS_HEADER: ReadonlyArray<string>;
47
+ /** The spec fields the scaffold generates from. */
48
+ export type ScaffoldInfo = {
49
+ readonly name: string;
50
+ readonly target: string;
51
+ /** agent.instructions (cli/channel/… agent-block shapes), or the joined
52
+ * step instructions for workflow specs. */
53
+ readonly instructions: string;
54
+ /** The spec-level tools list, verbatim spec names (e.g. `webSearch`). */
55
+ readonly tools: ReadonlyArray<string>;
56
+ /** agent.model / workflow model — the model whose credentials decide
57
+ * online vs offline mode when `--model` is not given. */
58
+ readonly model?: string;
59
+ /** True when the spec already declares a `feedback:` block. */
60
+ readonly hasFeedback: boolean;
61
+ };
62
+ /**
63
+ * Tolerantly extract the scaffold source from a spec. Supports every shape
64
+ * with an `agent.instructions` block (cli, channel, voice, browser, …) plus
65
+ * workflow specs (step instructions joined — each step is a stated goal).
66
+ * Throws ScaffoldEvalsError for shapes carrying no instructions at all;
67
+ * parse failures propagate as SpecParseError (a CrewhausError) so the CLI
68
+ * reports the real YAML problem.
69
+ */
70
+ export declare function extractScaffoldInfo(yamlText: string): ScaffoldInfo;
71
+ export declare function taskPhrasesFromInstructions(instructions: string): string[];
72
+ /**
73
+ * Deterministic template mode: `n` sample-input prompts derived from the
74
+ * instructions' task phrases (cycling phrase × template), or from generic
75
+ * task-shaped stubs when no phrase survives the filter (e.g. the bare
76
+ * `crewhaus init` placeholder spec). Pure — same info + n always yields the
77
+ * identical list.
78
+ */
79
+ export declare function templateSampleInputs(info: ScaffoldInfo, n: number): string[];
80
+ /**
81
+ * The runtime tool names a sample input obviously implies, drawn ONLY from
82
+ * the spec's own tools list. Builtin spec names map to their runtime
83
+ * PascalCase form (`webSearch` → `WebSearch` — the casing tool graders
84
+ * compare against); unknown/custom tools (e.g. `mcp__*`) are implied only
85
+ * when the input mentions them verbatim and pass through unchanged.
86
+ */
87
+ export declare function impliedTools(input: string, specTools: ReadonlyArray<string>): string[];
88
+ export type ScaffoldGenerator = "template" | "model";
89
+ /**
90
+ * Wrap generated inputs into SampleSchema-valid stubs: zero-padded stable
91
+ * ids, `expected_tools` where the spec's tools list is obviously implied,
92
+ * and metadata marking them as scaffold stubs to edit.
93
+ */
94
+ export declare function buildScaffoldSamples(info: ScaffoldInfo, inputs: ReadonlyArray<string>, generator: ScaffoldGenerator): Sample[];
95
+ /**
96
+ * The starter graders config — a spec-driven front-end over feedback.ts's
97
+ * synthesis machinery, single-grader in both modes (hard-AND-safe, exactly
98
+ * like distill emits):
99
+ *
100
+ * - `online: true` → one `llm_judge` rubric whose five anchors are
101
+ * pre-filled against the spec's stated goals (the task phrases parsed
102
+ * from its instructions). Deterministic text — no model call is needed to
103
+ * WRITE the rubric; credentials are needed to RUN it, which is why this
104
+ * mode keys on their presence.
105
+ * - `online: false` → `synthesizeGraders([])`'s safe non-empty-answer floor
106
+ * grader, so a credential-free `crewhaus eval` still runs end to end.
107
+ */
108
+ export declare function buildScaffoldGraders(info: ScaffoldInfo, opts: {
109
+ online: boolean;
110
+ model?: string;
111
+ }): GradersConfigObject;
112
+ /** System block for the one-shot sample-input generation call. */
113
+ export declare const SCAFFOLD_GENERATION_SYSTEM = "You write starter eval datasets for AI agent harnesses. You will receive an agent's instructions and tool list. Produce realistic, distinct user prompts that exercise the agent's stated tasks \u2014 including at least one edge case (ambiguous, adversarial, or out-of-scope request).\n\nHard rules:\n- Output exactly one JSON object: {\"inputs\": [\"...\", \"...\"]} with the requested number of entries. No text outside the JSON.\n- Each entry is one user prompt, phrased as a real user would type it.\n- Do NOT include expected answers, numbering, or commentary.";
114
+ /** The user message for the generation call — the spec's instructions
115
+ * (clipped) + tools + the requested sample count. */
116
+ export declare function buildSampleGenerationPrompt(info: ScaffoldInfo, n: number): string;
117
+ /**
118
+ * Tolerant parse of the generation response: first balanced `{…}` substring
119
+ * (```json fences and leading prose tolerated), `inputs` must be an array of
120
+ * non-empty strings. Returns the trimmed, deduped prompts clipped to `max`;
121
+ * `[]` on ANY failure — the caller falls back to (or tops up from) the
122
+ * deterministic template.
123
+ */
124
+ export declare function parseModelSampleInputs(raw: string, max: number): string[];
125
+ /** Top up `primary` (model inputs) from `fallback` (template inputs) to `n`
126
+ * entries, skipping duplicates — a short model response still yields a full
127
+ * dataset. */
128
+ export declare function mergeInputs(primary: ReadonlyArray<string>, fallback: ReadonlyArray<string>, n: number): string[];
129
+ /**
130
+ * Never overwrite existing eval assets without `--force`. Returns the error
131
+ * message when blocked (the CLI routes it through `die`), undefined when the
132
+ * write may proceed. `exists` is injected so the guard is unit-testable.
133
+ */
134
+ export declare function checkNoOverwrite(paths: ReadonlyArray<string>, exists: (path: string) => boolean, force: boolean): string | undefined;
135
+ /** The `feedback:` block suggestion printed (never auto-applied) when the
136
+ * spec lacks one — ratings are what upgrade these scaffolds into distilled
137
+ * datasets later. */
138
+ export declare function feedbackBlockSuggestion(): string;
@@ -0,0 +1,410 @@
1
+ import { parseSpec } from "@crewhaus/spec";
2
+ import { synthesizeGraders } from "./feedback";
3
+ /** Thrown on a spec this scaffold cannot derive eval assets from (and on
4
+ * invalid flag values). The CLI entry file routes it through `die()`;
5
+ * tests assert on `.message` without the process exiting. */
6
+ export class ScaffoldEvalsError extends Error {
7
+ name = "ScaffoldEvalsError";
8
+ }
9
+ /** Default number of Sample stubs `scaffold-evals` generates. */
10
+ export const DEFAULT_SCAFFOLD_SAMPLES = 8;
11
+ /** Header for the scaffolded graders.yaml (threaded through feedback.ts's
12
+ * `gradersConfigToYaml`, replacing its distill-specific default). */
13
+ export const SCAFFOLD_GRADERS_HEADER = [
14
+ "# Scaffolded by `crewhaus scaffold-evals` from the spec's stated goals.",
15
+ "# Exactly one grader — stacking graders hard-ANDs their scores (see eval-grader `all`).",
16
+ ];
17
+ /**
18
+ * Tolerantly extract the scaffold source from a spec. Supports every shape
19
+ * with an `agent.instructions` block (cli, channel, voice, browser, …) plus
20
+ * workflow specs (step instructions joined — each step is a stated goal).
21
+ * Throws ScaffoldEvalsError for shapes carrying no instructions at all;
22
+ * parse failures propagate as SpecParseError (a CrewhausError) so the CLI
23
+ * reports the real YAML problem.
24
+ */
25
+ export function extractScaffoldInfo(yamlText) {
26
+ const spec = parseSpec(yamlText);
27
+ const hasFeedback = spec.feedback !== undefined;
28
+ if (typeof spec.agent?.instructions === "string") {
29
+ return {
30
+ name: spec.name,
31
+ target: spec.target,
32
+ instructions: spec.agent.instructions,
33
+ tools: spec.tools ?? [],
34
+ ...(spec.agent.model !== undefined ? { model: spec.agent.model } : {}),
35
+ hasFeedback,
36
+ };
37
+ }
38
+ if (Array.isArray(spec.steps) && spec.steps.length > 0) {
39
+ const tools = [...new Set(spec.steps.flatMap((s) => s.tools ?? []))];
40
+ return {
41
+ name: spec.name,
42
+ target: spec.target,
43
+ instructions: spec.steps.map((s) => s.instructions).join("\n"),
44
+ tools,
45
+ ...(spec.model !== undefined ? { model: spec.model } : {}),
46
+ hasFeedback,
47
+ };
48
+ }
49
+ throw new ScaffoldEvalsError(`target "${spec.target}" carries no agent.instructions — scaffold-evals supports agent-block shapes (cli, channel, …) and workflow steps`);
50
+ }
51
+ // -------- template mode: instructions → task phrases → prompts --------
52
+ /** Sentences that describe the agent rather than a task it performs. The
53
+ * `init` placeholder line is filtered too so `init --with-evals` on a fresh
54
+ * scaffold falls through to the generic stubs instead of echoing it. */
55
+ const PERSONA_PATTERNS = [
56
+ /^you are\b/i,
57
+ /^your name\b/i,
58
+ /^act as\b/i,
59
+ /replace these instructions/i,
60
+ ];
61
+ /** Constraint sentences ("never …", "do not …") are guardrails, not tasks —
62
+ * a deterministic template can't invert them into a useful probe. */
63
+ const CONSTRAINT_PATTERNS = [/^never\b/i, /^do not\b/i, /^don'?t\b/i];
64
+ const MODAL_PREFIX = /^you (?:must|should|will|can|typically|generally|always)\s+/i;
65
+ /**
66
+ * Parse instruction sentences into task phrases — the deterministic half of
67
+ * sample-input generation. Persona/placeholder sentences and negative
68
+ * constraints are dropped; "You must/should/will/can X" and bare "You X"
69
+ * collapse to "X"; leading "Always" is stripped; short fragments (< 3 words)
70
+ * are discarded. Order follows the instructions; duplicates collapse.
71
+ */
72
+ const BULLET_PREFIX = /^(?:[-*•]|\d+[.)])\s+/;
73
+ /** Reflow instruction text into segments: YAML block scalars wrap sentences
74
+ * across lines, so plain continuation lines rejoin the previous one, while
75
+ * bullet markers and blank lines start a new segment. */
76
+ function reflowSegments(instructions) {
77
+ const segments = [];
78
+ let current = "";
79
+ const flush = () => {
80
+ if (current.trim() !== "")
81
+ segments.push(current.trim());
82
+ current = "";
83
+ };
84
+ for (const line of instructions.split("\n")) {
85
+ const trimmed = line.trim();
86
+ if (trimmed === "") {
87
+ flush();
88
+ continue;
89
+ }
90
+ if (BULLET_PREFIX.test(trimmed)) {
91
+ flush();
92
+ current = trimmed.replace(BULLET_PREFIX, "");
93
+ continue;
94
+ }
95
+ current = current === "" ? trimmed : `${current} ${trimmed}`;
96
+ }
97
+ flush();
98
+ return segments;
99
+ }
100
+ export function taskPhrasesFromInstructions(instructions) {
101
+ const phrases = [];
102
+ const seen = new Set();
103
+ for (const segment of reflowSegments(instructions)) {
104
+ for (const raw of segment.split(/(?<=[.!?])\s+/)) {
105
+ let s = raw.replace(/\s+/g, " ").trim();
106
+ if (s === "")
107
+ continue;
108
+ if (PERSONA_PATTERNS.some((p) => p.test(s)))
109
+ continue;
110
+ if (CONSTRAINT_PATTERNS.some((p) => p.test(s)))
111
+ continue;
112
+ s = s.replace(/^always\s+/i, "");
113
+ s = s.replace(MODAL_PREFIX, "");
114
+ s = s.replace(/^you\s+/i, "");
115
+ s = s.replace(/[.!?]+$/, "").trim();
116
+ if (s.split(" ").length < 3 || s.length < 12)
117
+ continue;
118
+ const key = s.toLowerCase();
119
+ if (seen.has(key))
120
+ continue;
121
+ seen.add(key);
122
+ phrases.push(s);
123
+ }
124
+ }
125
+ return phrases;
126
+ }
127
+ const capitalize = (s) => (s === "" ? s : s[0]?.toUpperCase() + s.slice(1));
128
+ const uncapitalize = (s) => (s === "" ? s : s[0]?.toLowerCase() + s.slice(1));
129
+ const TASK_TEMPLATES = [
130
+ (t) => `${capitalize(t)}.`,
131
+ (t) => `Please ${uncapitalize(t)}.`,
132
+ (t) => `Can you ${uncapitalize(t)}?`,
133
+ (t) => `Here is a request for you: ${uncapitalize(t)}. Walk through it step by step.`,
134
+ ];
135
+ const GENERIC_TEMPLATES = [
136
+ (n) => `Handle a typical ${n} request end to end, stating any assumptions you make.`,
137
+ (n) => `A user brings you the most common task ${n} exists for. Complete it and explain your steps.`,
138
+ () => "The request is ambiguous. Ask one clarifying question, then answer for the most likely interpretation.",
139
+ (n) => `Summarize what you can do as ${n}, then demonstrate the most useful capability on a small example.`,
140
+ ];
141
+ /**
142
+ * Deterministic template mode: `n` sample-input prompts derived from the
143
+ * instructions' task phrases (cycling phrase × template), or from generic
144
+ * task-shaped stubs when no phrase survives the filter (e.g. the bare
145
+ * `crewhaus init` placeholder spec). Pure — same info + n always yields the
146
+ * identical list.
147
+ */
148
+ export function templateSampleInputs(info, n) {
149
+ const phrases = taskPhrasesFromInstructions(info.instructions);
150
+ const inputs = [];
151
+ if (phrases.length > 0) {
152
+ for (let i = 0; i < n; i += 1) {
153
+ const phrase = phrases[i % phrases.length];
154
+ const tier = Math.floor(i / phrases.length);
155
+ const template = TASK_TEMPLATES[tier % TASK_TEMPLATES.length];
156
+ const variation = tier >= TASK_TEMPLATES.length ? ` (variation ${tier})` : "";
157
+ inputs.push(`${template(phrase)}${variation}`);
158
+ }
159
+ return inputs;
160
+ }
161
+ for (let i = 0; i < n; i += 1) {
162
+ const template = GENERIC_TEMPLATES[i % GENERIC_TEMPLATES.length];
163
+ const tier = Math.floor(i / GENERIC_TEMPLATES.length);
164
+ const variation = tier >= 1 ? ` (variation ${tier + 1})` : "";
165
+ inputs.push(`${template(info.name)}${variation}`);
166
+ }
167
+ return inputs;
168
+ }
169
+ // -------- tool implication --------
170
+ /**
171
+ * Spec tool name → the RUNTIME (PascalCase) tool name recorded in trace
172
+ * events and expected by tool graders, plus the input keywords that
173
+ * "obviously imply" the tool. Mirrors `loadToolMap` in index.ts /
174
+ * target-cli's BUILTIN_TOOL_MAP — keep the name column in sync. Keyword
175
+ * sets are deliberately conservative: a missed implication is a stub the
176
+ * user fills in; a false one is a failing grader they must debug.
177
+ */
178
+ const TOOL_IMPLICATIONS = {
179
+ read: { runtime: "Read", keywords: ["read the file", "open the file", "file contents"] },
180
+ write: { runtime: "Write", keywords: ["write a file", "create a file", "save to a file"] },
181
+ edit: { runtime: "Edit", keywords: ["edit", "modify the file", "refactor"] },
182
+ glob: { runtime: "Glob", keywords: ["find files", "list files", "matching files"] },
183
+ grep: {
184
+ runtime: "Grep",
185
+ keywords: ["grep", "search the code", "search the codebase", "find references"],
186
+ },
187
+ bash: {
188
+ runtime: "Bash",
189
+ keywords: ["run the", "execute", "shell", "command", "install", "the tests"],
190
+ },
191
+ todoWrite: { runtime: "TodoWrite", keywords: ["todo", "task list", "plan out"] },
192
+ webFetch: { runtime: "WebFetch", keywords: ["fetch", "url", "web page", "website"] },
193
+ webSearch: {
194
+ runtime: "WebSearch",
195
+ keywords: ["search the web", "search online", "look up online", "web search", "latest news"],
196
+ },
197
+ readImage: { runtime: "ReadImage", keywords: ["image", "screenshot", "photo"] },
198
+ fetch: { runtime: "Fetch", keywords: ["fetch", "http", "api", "endpoint", "url"] },
199
+ imageGenerate: {
200
+ runtime: "ImageGenerate",
201
+ keywords: ["generate an image", "draw", "illustration"],
202
+ },
203
+ ingestDocument: { runtime: "IngestDocument", keywords: ["pdf", "document", "docx"] },
204
+ };
205
+ function keywordImplied(input, keyword) {
206
+ // Word-boundary match so "read" can't fire inside "already". Keywords are
207
+ // hand-authored lowercase alphanumeric phrases, so no escaping is needed.
208
+ return new RegExp(`\\b${keyword}\\b`, "i").test(input);
209
+ }
210
+ /**
211
+ * The runtime tool names a sample input obviously implies, drawn ONLY from
212
+ * the spec's own tools list. Builtin spec names map to their runtime
213
+ * PascalCase form (`webSearch` → `WebSearch` — the casing tool graders
214
+ * compare against); unknown/custom tools (e.g. `mcp__*`) are implied only
215
+ * when the input mentions them verbatim and pass through unchanged.
216
+ */
217
+ export function impliedTools(input, specTools) {
218
+ const out = [];
219
+ for (const tool of specTools) {
220
+ const known = TOOL_IMPLICATIONS[tool];
221
+ if (known !== undefined) {
222
+ if (known.keywords.some((k) => keywordImplied(input, k)))
223
+ out.push(known.runtime);
224
+ }
225
+ else if (input.toLowerCase().includes(tool.toLowerCase())) {
226
+ out.push(tool);
227
+ }
228
+ }
229
+ return [...new Set(out)];
230
+ }
231
+ /**
232
+ * Wrap generated inputs into SampleSchema-valid stubs: zero-padded stable
233
+ * ids, `expected_tools` where the spec's tools list is obviously implied,
234
+ * and metadata marking them as scaffold stubs to edit.
235
+ */
236
+ export function buildScaffoldSamples(info, inputs, generator) {
237
+ const width = Math.max(2, String(inputs.length).length);
238
+ return inputs.map((input, i) => {
239
+ const tools = impliedTools(input, info.tools);
240
+ return {
241
+ id: `scaffold_${String(i + 1).padStart(width, "0")}`,
242
+ input,
243
+ ...(tools.length > 0 ? { expected_tools: tools } : {}),
244
+ metadata: {
245
+ source: "scaffold-evals",
246
+ generator,
247
+ note: "starter stub — edit into a real task (and add expected_output for gold answers)",
248
+ },
249
+ };
250
+ });
251
+ }
252
+ // -------- graders --------
253
+ /** Clip + quote-strip a goals summary for a rubric description (mirrors
254
+ * feedback.ts's summarizeComments bounds). */
255
+ function summarizeGoals(goals) {
256
+ const cleaned = goals.map((g) => g.trim().replace(/'/g, "")).filter((g) => g !== "");
257
+ if (cleaned.length === 0)
258
+ return undefined;
259
+ const joined = cleaned.slice(0, 4).join("; ");
260
+ return joined.length > 400 ? `${joined.slice(0, 400)}…` : joined;
261
+ }
262
+ /**
263
+ * The starter graders config — a spec-driven front-end over feedback.ts's
264
+ * synthesis machinery, single-grader in both modes (hard-AND-safe, exactly
265
+ * like distill emits):
266
+ *
267
+ * - `online: true` → one `llm_judge` rubric whose five anchors are
268
+ * pre-filled against the spec's stated goals (the task phrases parsed
269
+ * from its instructions). Deterministic text — no model call is needed to
270
+ * WRITE the rubric; credentials are needed to RUN it, which is why this
271
+ * mode keys on their presence.
272
+ * - `online: false` → `synthesizeGraders([])`'s safe non-empty-answer floor
273
+ * grader, so a credential-free `crewhaus eval` still runs end to end.
274
+ */
275
+ export function buildScaffoldGraders(info, opts) {
276
+ if (!opts.online) {
277
+ // The floor path of feedback.ts's synthesis (no positive turns yet); the
278
+ // scaffold prints its own offline note, so the distill-flavoured warning
279
+ // is discarded.
280
+ return synthesizeGraders([], []);
281
+ }
282
+ const goals = summarizeGoals(taskPhrasesFromInstructions(info.instructions));
283
+ const goalsNote = goals !== undefined
284
+ ? `: '${goals}'`
285
+ : ` of the "${info.name}" harness (see its crewhaus.yaml instructions)`;
286
+ return {
287
+ graders: [
288
+ {
289
+ name: "spec_goal_alignment",
290
+ type: "llm_judge",
291
+ rubric: {
292
+ criteria: [
293
+ {
294
+ name: "spec_goal_alignment",
295
+ description: `Judge how well the response fulfils the spec's stated goals${goalsNote}. Score strictly against the anchors.`,
296
+ anchors: {
297
+ "1": "Off-task or contradicts the stated goals; ignores the harness's purpose.",
298
+ "2": "Barely on-task; most of the stated goals are unmet.",
299
+ "3": "Partially fulfils the stated goals; noticeable gaps or constraint violations.",
300
+ "4": "Fulfils the stated goals with only minor gaps.",
301
+ "5": "Fully fulfils the stated goals and respects the instructions' constraints.",
302
+ },
303
+ },
304
+ ],
305
+ passing_score: 3,
306
+ },
307
+ ...(opts.model !== undefined ? { model: opts.model } : {}),
308
+ },
309
+ ],
310
+ };
311
+ }
312
+ // -------- model mode (pure halves; the call lives in index.ts) --------
313
+ /** System block for the one-shot sample-input generation call. */
314
+ export const SCAFFOLD_GENERATION_SYSTEM = `You write starter eval datasets for AI agent harnesses. You will receive an agent's instructions and tool list. Produce realistic, distinct user prompts that exercise the agent's stated tasks — including at least one edge case (ambiguous, adversarial, or out-of-scope request).
315
+
316
+ Hard rules:
317
+ - Output exactly one JSON object: {"inputs": ["...", "..."]} with the requested number of entries. No text outside the JSON.
318
+ - Each entry is one user prompt, phrased as a real user would type it.
319
+ - Do NOT include expected answers, numbering, or commentary.`;
320
+ const MAX_INSTRUCTIONS_IN_PROMPT = 6000;
321
+ /** The user message for the generation call — the spec's instructions
322
+ * (clipped) + tools + the requested sample count. */
323
+ export function buildSampleGenerationPrompt(info, n) {
324
+ const instructions = info.instructions.length > MAX_INSTRUCTIONS_IN_PROMPT
325
+ ? `${info.instructions.slice(0, MAX_INSTRUCTIONS_IN_PROMPT)}…`
326
+ : info.instructions;
327
+ const tools = info.tools.length > 0 ? info.tools.join(", ") : "(none declared)";
328
+ return [
329
+ `HARNESS: ${info.name} (target: ${info.target})`,
330
+ "",
331
+ "AGENT INSTRUCTIONS:",
332
+ instructions,
333
+ "",
334
+ `TOOLS: ${tools}`,
335
+ "",
336
+ `Produce exactly ${n} eval input prompts as {"inputs": [...]}.`,
337
+ ].join("\n");
338
+ }
339
+ /**
340
+ * Tolerant parse of the generation response: first balanced `{…}` substring
341
+ * (```json fences and leading prose tolerated), `inputs` must be an array of
342
+ * non-empty strings. Returns the trimmed, deduped prompts clipped to `max`;
343
+ * `[]` on ANY failure — the caller falls back to (or tops up from) the
344
+ * deterministic template.
345
+ */
346
+ export function parseModelSampleInputs(raw, max) {
347
+ const jsonMatch = raw.match(/\{[\s\S]*\}/);
348
+ if (jsonMatch === null)
349
+ return [];
350
+ let parsed;
351
+ try {
352
+ parsed = JSON.parse(jsonMatch[0]);
353
+ }
354
+ catch {
355
+ return [];
356
+ }
357
+ const inputs = parsed.inputs;
358
+ if (!Array.isArray(inputs))
359
+ return [];
360
+ const out = [];
361
+ const seen = new Set();
362
+ for (const entry of inputs) {
363
+ if (typeof entry !== "string")
364
+ continue;
365
+ const trimmed = entry.trim();
366
+ if (trimmed === "" || seen.has(trimmed))
367
+ continue;
368
+ seen.add(trimmed);
369
+ out.push(trimmed);
370
+ if (out.length >= max)
371
+ break;
372
+ }
373
+ return out;
374
+ }
375
+ /** Top up `primary` (model inputs) from `fallback` (template inputs) to `n`
376
+ * entries, skipping duplicates — a short model response still yields a full
377
+ * dataset. */
378
+ export function mergeInputs(primary, fallback, n) {
379
+ const out = primary.slice(0, n);
380
+ const seen = new Set(out);
381
+ for (const f of fallback) {
382
+ if (out.length >= n)
383
+ break;
384
+ if (seen.has(f))
385
+ continue;
386
+ seen.add(f);
387
+ out.push(f);
388
+ }
389
+ return out;
390
+ }
391
+ // -------- overwrite guard + suggestions --------
392
+ /**
393
+ * Never overwrite existing eval assets without `--force`. Returns the error
394
+ * message when blocked (the CLI routes it through `die`), undefined when the
395
+ * write may proceed. `exists` is injected so the guard is unit-testable.
396
+ */
397
+ export function checkNoOverwrite(paths, exists, force) {
398
+ if (force)
399
+ return undefined;
400
+ const existing = paths.filter(exists);
401
+ if (existing.length === 0)
402
+ return undefined;
403
+ return `refusing to overwrite existing eval asset(s): ${existing.join(", ")} — re-run with --force to replace them`;
404
+ }
405
+ /** The `feedback:` block suggestion printed (never auto-applied) when the
406
+ * spec lacks one — ratings are what upgrade these scaffolds into distilled
407
+ * datasets later. */
408
+ export function feedbackBlockSuggestion() {
409
+ return ["feedback:", " modality: binary", " autoDistill: true"].join("\n");
410
+ }