crewhaus 0.1.8 → 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.
- package/README.md +10 -3
- package/dist/advice-apply.d.ts +182 -0
- package/dist/advice-apply.js +286 -0
- package/dist/advise-rules.d.ts +348 -0
- package/dist/advise-rules.js +905 -0
- package/dist/alert-sink.d.ts +48 -0
- package/dist/alert-sink.js +86 -0
- package/dist/approval-gate.d.ts +127 -0
- package/dist/approval-gate.js +254 -0
- package/dist/audit-verify.d.ts +69 -0
- package/dist/audit-verify.js +97 -0
- package/dist/autodistill.d.ts +113 -0
- package/dist/autodistill.js +256 -0
- package/dist/channel-provision.d.ts +360 -0
- package/dist/channel-provision.js +881 -0
- package/dist/ci-scaffold.d.ts +31 -0
- package/dist/ci-scaffold.js +343 -0
- package/dist/compile-check.d.ts +90 -0
- package/dist/compile-check.js +285 -0
- package/dist/compliance-schedule.d.ts +35 -0
- package/dist/compliance-schedule.js +36 -0
- package/dist/context-pressure.d.ts +80 -0
- package/dist/context-pressure.js +166 -0
- package/dist/dataset-mine.d.ts +172 -0
- package/dist/dataset-mine.js +403 -0
- package/dist/datasets.d.ts +124 -0
- package/dist/datasets.js +260 -0
- package/dist/deploy-canary.d.ts +83 -0
- package/dist/deploy-canary.js +87 -0
- package/dist/doctor-checks.d.ts +33 -0
- package/dist/doctor-checks.js +92 -0
- package/dist/doctor-detect.d.ts +108 -0
- package/dist/doctor-detect.js +214 -0
- package/dist/doctor-fix.d.ts +81 -0
- package/dist/doctor-fix.js +164 -0
- package/dist/egress-triage.d.ts +121 -0
- package/dist/egress-triage.js +261 -0
- package/dist/eval-bridge.d.ts +114 -0
- package/dist/eval-bridge.js +158 -0
- package/dist/eval-coverage.d.ts +140 -0
- package/dist/eval-coverage.js +428 -0
- package/dist/eval-history.d.ts +48 -0
- package/dist/eval-history.js +157 -0
- package/dist/eval-matrix.d.ts +80 -0
- package/dist/eval-matrix.js +182 -0
- package/dist/eval-sentinel.d.ts +65 -0
- package/dist/eval-sentinel.js +132 -0
- package/dist/faq.d.ts +68 -0
- package/dist/faq.js +168 -0
- package/dist/feedback.d.ts +9 -2
- package/dist/feedback.js +17 -7
- package/dist/fewshot.d.ts +83 -0
- package/dist/fewshot.js +158 -0
- package/dist/fleet.d.ts +207 -0
- package/dist/fleet.js +488 -0
- package/dist/flywheel.d.ts +193 -0
- package/dist/flywheel.js +519 -0
- package/dist/graders-suggest.d.ts +186 -0
- package/dist/graders-suggest.js +658 -0
- package/dist/incident.d.ts +99 -0
- package/dist/incident.js +217 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +11601 -964
- package/dist/init-interactive.d.ts +105 -0
- package/dist/init-interactive.js +208 -0
- package/dist/intents.d.ts +105 -0
- package/dist/intents.js +292 -0
- package/dist/judge-calibrate.d.ts +137 -0
- package/dist/judge-calibrate.js +247 -0
- package/dist/justification-calibrate.d.ts +150 -0
- package/dist/justification-calibrate.js +262 -0
- package/dist/justification-gate.d.ts +27 -6
- package/dist/justification-gate.js +30 -6
- package/dist/knowledge-sync.d.ts +179 -0
- package/dist/knowledge-sync.js +551 -0
- package/dist/lessons.d.ts +87 -0
- package/dist/lessons.js +207 -0
- package/dist/lint.d.ts +127 -0
- package/dist/lint.js +226 -0
- package/dist/loadtest.d.ts +114 -0
- package/dist/loadtest.js +196 -0
- package/dist/marketplace-cli.d.ts +110 -0
- package/dist/marketplace-cli.js +250 -0
- package/dist/mcp-doctor.d.ts +121 -0
- package/dist/mcp-doctor.js +249 -0
- package/dist/model-scan.d.ts +116 -0
- package/dist/model-scan.js +226 -0
- package/dist/onchain-tune.d.ts +164 -0
- package/dist/onchain-tune.js +346 -0
- package/dist/permissions-suggest.d.ts +126 -0
- package/dist/permissions-suggest.js +333 -0
- package/dist/pii-tune.d.ts +107 -0
- package/dist/pii-tune.js +122 -0
- package/dist/propose.d.ts +117 -0
- package/dist/propose.js +184 -0
- package/dist/refresh-goldens.d.ts +82 -0
- package/dist/refresh-goldens.js +221 -0
- package/dist/regression-pin.d.ts +160 -0
- package/dist/regression-pin.js +281 -0
- package/dist/retention.d.ts +193 -0
- package/dist/retention.js +607 -0
- package/dist/retire.d.ts +118 -0
- package/dist/retire.js +291 -0
- package/dist/right-size.d.ts +100 -0
- package/dist/right-size.js +123 -0
- package/dist/scaffold-evals.d.ts +138 -0
- package/dist/scaffold-evals.js +410 -0
- package/dist/scope-audit-drift.d.ts +139 -0
- package/dist/scope-audit-drift.js +260 -0
- package/dist/security-corpus.d.ts +237 -0
- package/dist/security-corpus.js +516 -0
- package/dist/security-digest.d.ts +173 -0
- package/dist/security-digest.js +650 -0
- package/dist/sessions-index.d.ts +27 -0
- package/dist/sessions-index.js +51 -0
- package/dist/slo-doctor.d.ts +67 -0
- package/dist/slo-doctor.js +119 -0
- package/dist/slo-sink.d.ts +96 -0
- package/dist/slo-sink.js +107 -0
- package/dist/spec-changelog.d.ts +102 -0
- package/dist/spec-changelog.js +237 -0
- package/dist/state-backup.d.ts +223 -0
- package/dist/state-backup.js +648 -0
- package/dist/tools-cli.d.ts +170 -0
- package/dist/tools-cli.js +298 -0
- package/dist/triage.d.ts +202 -0
- package/dist/triage.js +403 -0
- package/dist/upgrade.d.ts +57 -0
- package/dist/upgrade.js +113 -0
- package/dist/version.d.ts +6 -0
- package/dist/version.js +27 -0
- package/dist/voice-eval.d.ts +138 -0
- package/dist/voice-eval.js +309 -0
- package/dist/watch.d.ts +58 -0
- package/dist/watch.js +97 -0
- package/package.json +89 -65
|
@@ -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
|
+
}
|