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,121 @@
|
|
|
1
|
+
import type { Spec } from "@crewhaus/spec";
|
|
2
|
+
import type { AdviceFinding } from "./advise-rules";
|
|
3
|
+
/**
|
|
4
|
+
* AUTOMATION-OPPORTUNITIES.md item 20 — `crewhaus egress review [--propose]`:
|
|
5
|
+
* triage durable egress/boundary history into learned security spec
|
|
6
|
+
* suggestions. Side-effect-free so it is unit-testable, mirroring
|
|
7
|
+
* `advise-rules.ts` (all filesystem access stays in `index.ts`).
|
|
8
|
+
*
|
|
9
|
+
* INPUT (durability, verified 2026-07): egress verdicts are NOW durable —
|
|
10
|
+
* runtime-core's `egressAuditSink` (this branch) appends one `egress_decision`
|
|
11
|
+
* record per NON-PASS verdict to the hash-chained `.crewhaus/audit`, payload
|
|
12
|
+
* `{ sinkId, sinkScope, verdict: "warn"|"block", originsFound[], matchCount }`
|
|
13
|
+
* (lineage summary only, never the raw payload). Rule-based justification
|
|
14
|
+
* denials come from the `permission_justification_evaluated` records the
|
|
15
|
+
* intent gate already writes.
|
|
16
|
+
*
|
|
17
|
+
* OUTPUT: three proposal families, matching the item:
|
|
18
|
+
*
|
|
19
|
+
* 1. Per-sink RELAXATION — sinks that warned ≥ warnRelaxMin times with ZERO
|
|
20
|
+
* blocks and a single stable origin: the flow is chronic-but-benign, so
|
|
21
|
+
* the operator may want to whitelist it. EMITTED AS ADVICE TEXT, never a
|
|
22
|
+
* patch: the natural home is a `security.egressPolicy` block, but that
|
|
23
|
+
* schema field is RESERVED for the egress-fabric FRs (FR-002/006) — the
|
|
24
|
+
* spec schema intentionally does NOT carry it today, and
|
|
25
|
+
* `OPTIMIZABLE_PATHS` lists `["security","egressPolicy"]` on their behalf.
|
|
26
|
+
* Adding the field here would clobber their ownership, so we coordinate:
|
|
27
|
+
* the proposal is advice text pointing at where the relaxation will live
|
|
28
|
+
* once that block ships. (See packages/spec/src/index.ts securityBlock
|
|
29
|
+
* doc comment: "Do NOT add egressPolicy here".)
|
|
30
|
+
*
|
|
31
|
+
* 2. `security.egressMatcher: semantic` — when warn-noise is HIGH but blocks
|
|
32
|
+
* are ZERO across sinks, substring matching is over-firing on benign
|
|
33
|
+
* lineage overlaps; the embedding matcher scores by similarity instead.
|
|
34
|
+
* ADVICE-ONLY: `["security","egressMatcher"]` is NOT in OPTIMIZABLE_PATHS
|
|
35
|
+
* (switching the matcher changes detection semantics — a human decision),
|
|
36
|
+
* so it must never ride `optimize --from-advice`.
|
|
37
|
+
*
|
|
38
|
+
* 3. `security.justification.judge: claude` — when RULE-BASED justification
|
|
39
|
+
* denials are frequent, the deterministic judge is likely over-denying;
|
|
40
|
+
* the model-backed judge reasons about intent. `["security","justification"]`
|
|
41
|
+
* IS whitelisted (cli/managed), so this rides the eval-gated
|
|
42
|
+
* `optimize --from-advice` apply as a SPEC-PATCH when a spec is present
|
|
43
|
+
* and the patch validates; otherwise it degrades to advice text.
|
|
44
|
+
*/
|
|
45
|
+
export type EgressDecisionRecord = {
|
|
46
|
+
readonly sinkId: string;
|
|
47
|
+
readonly sinkScope: string;
|
|
48
|
+
readonly verdict: "warn" | "block";
|
|
49
|
+
readonly originsFound: ReadonlyArray<string>;
|
|
50
|
+
readonly matchCount: number;
|
|
51
|
+
};
|
|
52
|
+
/** Per (sink, origin, scope) cluster of egress records. */
|
|
53
|
+
export type EgressCluster = {
|
|
54
|
+
readonly sinkId: string;
|
|
55
|
+
readonly sinkScope: string;
|
|
56
|
+
/** Origins observed across this sink's non-pass records, deduped + sorted. */
|
|
57
|
+
readonly origins: ReadonlyArray<string>;
|
|
58
|
+
readonly warned: number;
|
|
59
|
+
readonly blocked: number;
|
|
60
|
+
/** Total records folded into this cluster. */
|
|
61
|
+
readonly total: number;
|
|
62
|
+
};
|
|
63
|
+
export type EgressTriageContext = {
|
|
64
|
+
/** Non-pass egress clusters keyed by sinkId, ranked by (blocked, warned). */
|
|
65
|
+
readonly clusters: ReadonlyArray<EgressCluster>;
|
|
66
|
+
/** Total warn / block records across all sinks (the matcher-noise signal). */
|
|
67
|
+
readonly totalWarned: number;
|
|
68
|
+
readonly totalBlocked: number;
|
|
69
|
+
/** Rule-based justification denials — the judge-upgrade signal. */
|
|
70
|
+
readonly ruleBasedDenials: number;
|
|
71
|
+
readonly ruleBasedEvaluated: number;
|
|
72
|
+
/** Egress records parsed; malformed/other kinds skipped. */
|
|
73
|
+
readonly egressRecords: number;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Fold parsed `.crewhaus/audit` records into the triage context. Tolerant of
|
|
77
|
+
* malformed/unknown records (skipped). Reads two kinds:
|
|
78
|
+
* - `egress_decision` → per-sink warn/block clusters + totals
|
|
79
|
+
* - `permission_justification_evaluated` → rule-based deny counts
|
|
80
|
+
*/
|
|
81
|
+
export declare function buildEgressTriageContext(auditRecords: ReadonlyArray<unknown>): EgressTriageContext;
|
|
82
|
+
export type EgressTriageThresholds = {
|
|
83
|
+
/** Warns on ONE sink (with 0 blocks) at/above which a relaxation is proposed. */
|
|
84
|
+
readonly warnRelaxMin: number;
|
|
85
|
+
/** Total warns (with 0 total blocks) at/above which semantic is proposed. */
|
|
86
|
+
readonly semanticWarnMin: number;
|
|
87
|
+
/** Rule-based denials at/above which the claude-judge upgrade is proposed. */
|
|
88
|
+
readonly judgeDenialMin: number;
|
|
89
|
+
};
|
|
90
|
+
export declare const DEFAULT_EGRESS_TRIAGE_THRESHOLDS: EgressTriageThresholds;
|
|
91
|
+
/** Targets whose schema carries `security.justification` AND for which
|
|
92
|
+
* `["security","justification"]` is optimizable (rides --from-advice). */
|
|
93
|
+
export declare const JUSTIFICATION_PATCH_TARGETS: ReadonlySet<string>;
|
|
94
|
+
export type EgressTriageOptions = {
|
|
95
|
+
readonly spec?: Spec;
|
|
96
|
+
readonly thresholds?: Partial<EgressTriageThresholds>;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Proposal 1 — per-sink relaxation for chronic warn-only flows. ADVICE-ONLY:
|
|
100
|
+
* the relaxation's home (`security.egressPolicy`) is reserved for the egress
|
|
101
|
+
* FRs and does not exist in the schema yet, so we emit guidance rather than a
|
|
102
|
+
* schema-adding patch (coordinate, don't clobber).
|
|
103
|
+
*/
|
|
104
|
+
export declare function ruleSinkRelaxation(ctx: EgressTriageContext, opts?: EgressTriageOptions): AdviceFinding[];
|
|
105
|
+
/**
|
|
106
|
+
* Proposal 2 — `security.egressMatcher: semantic` when warn-noise is high but
|
|
107
|
+
* blocks are zero (the substring matcher is over-firing on benign lineage
|
|
108
|
+
* overlaps). ADVICE-ONLY: egressMatcher is not optimizer-whitelisted.
|
|
109
|
+
*/
|
|
110
|
+
export declare function ruleSemanticMatcher(ctx: EgressTriageContext, opts?: EgressTriageOptions): AdviceFinding[];
|
|
111
|
+
/**
|
|
112
|
+
* Proposal 3 — `security.justification.judge: claude` when rule-based denials
|
|
113
|
+
* are frequent. SPEC-PATCH when the spec is a justification-optimizable target
|
|
114
|
+
* and the patch validates (rides `optimize --from-advice`); else advice.
|
|
115
|
+
*/
|
|
116
|
+
export declare function ruleJustificationJudgeUpgrade(ctx: EgressTriageContext, opts?: EgressTriageOptions): AdviceFinding[];
|
|
117
|
+
export declare const EGRESS_TRIAGE_RULES: readonly [typeof ruleSinkRelaxation, typeof ruleSemanticMatcher, typeof ruleJustificationJudgeUpgrade];
|
|
118
|
+
/** Run every triage rule and rank: warn before info, then by primary count. */
|
|
119
|
+
export declare function runEgressTriage(ctx: EgressTriageContext, opts?: EgressTriageOptions): AdviceFinding[];
|
|
120
|
+
/** One line per finding for the CLI's text mode (mirrors formatFindingLines). */
|
|
121
|
+
export declare function formatEgressFindingLines(f: AdviceFinding): string[];
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { validatePatch } from "@crewhaus/spec-patch";
|
|
2
|
+
function asObject(v) {
|
|
3
|
+
return v !== null && typeof v === "object" && !Array.isArray(v)
|
|
4
|
+
? v
|
|
5
|
+
: undefined;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Fold parsed `.crewhaus/audit` records into the triage context. Tolerant of
|
|
9
|
+
* malformed/unknown records (skipped). Reads two kinds:
|
|
10
|
+
* - `egress_decision` → per-sink warn/block clusters + totals
|
|
11
|
+
* - `permission_justification_evaluated` → rule-based deny counts
|
|
12
|
+
*/
|
|
13
|
+
export function buildEgressTriageContext(auditRecords) {
|
|
14
|
+
const bySink = new Map();
|
|
15
|
+
let totalWarned = 0;
|
|
16
|
+
let totalBlocked = 0;
|
|
17
|
+
let ruleBasedDenials = 0;
|
|
18
|
+
let ruleBasedEvaluated = 0;
|
|
19
|
+
let egressRecords = 0;
|
|
20
|
+
for (const rec of auditRecords) {
|
|
21
|
+
const obj = asObject(rec);
|
|
22
|
+
if (obj === undefined)
|
|
23
|
+
continue;
|
|
24
|
+
const kind = obj["kind"];
|
|
25
|
+
if (kind === "egress_decision") {
|
|
26
|
+
const p = asObject(obj["payload"]);
|
|
27
|
+
if (p === undefined)
|
|
28
|
+
continue;
|
|
29
|
+
const verdict = p["verdict"];
|
|
30
|
+
if (verdict !== "warn" && verdict !== "block")
|
|
31
|
+
continue;
|
|
32
|
+
egressRecords += 1;
|
|
33
|
+
const sinkId = typeof p["sinkId"] === "string" ? p["sinkId"] : "(unknown sink)";
|
|
34
|
+
const scope = typeof p["sinkScope"] === "string" ? p["sinkScope"] : "(unknown scope)";
|
|
35
|
+
const acc = bySink.get(sinkId) ?? {
|
|
36
|
+
scope,
|
|
37
|
+
origins: new Set(),
|
|
38
|
+
warned: 0,
|
|
39
|
+
blocked: 0,
|
|
40
|
+
total: 0,
|
|
41
|
+
};
|
|
42
|
+
if (verdict === "warn") {
|
|
43
|
+
acc.warned += 1;
|
|
44
|
+
totalWarned += 1;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
acc.blocked += 1;
|
|
48
|
+
totalBlocked += 1;
|
|
49
|
+
}
|
|
50
|
+
acc.total += 1;
|
|
51
|
+
if (Array.isArray(p["originsFound"])) {
|
|
52
|
+
for (const o of p["originsFound"])
|
|
53
|
+
if (typeof o === "string")
|
|
54
|
+
acc.origins.add(o);
|
|
55
|
+
}
|
|
56
|
+
bySink.set(sinkId, acc);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (kind === "permission_justification_evaluated") {
|
|
60
|
+
const p = asObject(obj["payload"]);
|
|
61
|
+
if (p === undefined)
|
|
62
|
+
continue;
|
|
63
|
+
// The rule-based judge stamps judgeModel "rule-based" (justification-gate).
|
|
64
|
+
if (p["judgeModel"] !== "rule-based")
|
|
65
|
+
continue;
|
|
66
|
+
ruleBasedEvaluated += 1;
|
|
67
|
+
if (p["verdict"] === "deny")
|
|
68
|
+
ruleBasedDenials += 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const clusters = [...bySink.entries()]
|
|
72
|
+
.map(([sinkId, a]) => ({
|
|
73
|
+
sinkId,
|
|
74
|
+
sinkScope: a.scope,
|
|
75
|
+
origins: [...a.origins].sort(),
|
|
76
|
+
warned: a.warned,
|
|
77
|
+
blocked: a.blocked,
|
|
78
|
+
total: a.total,
|
|
79
|
+
}))
|
|
80
|
+
.sort((x, y) => y.blocked - x.blocked || y.warned - x.warned || x.sinkId.localeCompare(y.sinkId));
|
|
81
|
+
return {
|
|
82
|
+
clusters,
|
|
83
|
+
totalWarned,
|
|
84
|
+
totalBlocked,
|
|
85
|
+
ruleBasedDenials,
|
|
86
|
+
ruleBasedEvaluated,
|
|
87
|
+
egressRecords,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export const DEFAULT_EGRESS_TRIAGE_THRESHOLDS = {
|
|
91
|
+
warnRelaxMin: 5,
|
|
92
|
+
semanticWarnMin: 10,
|
|
93
|
+
judgeDenialMin: 5,
|
|
94
|
+
};
|
|
95
|
+
/** Targets whose schema carries `security.justification` AND for which
|
|
96
|
+
* `["security","justification"]` is optimizable (rides --from-advice). */
|
|
97
|
+
export const JUSTIFICATION_PATCH_TARGETS = new Set(["cli", "managed"]);
|
|
98
|
+
// -------- helper: patch-or-advice (mirrors advise-rules) --------
|
|
99
|
+
function patchOrAdvice(spec, patch, fallback) {
|
|
100
|
+
if (spec === undefined)
|
|
101
|
+
return { kind: "advice", text: fallback };
|
|
102
|
+
try {
|
|
103
|
+
validatePatch(spec, patch);
|
|
104
|
+
return { kind: "spec-patch", patch };
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return { kind: "advice", text: fallback };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function specJustificationJudge(spec) {
|
|
111
|
+
if (spec === undefined)
|
|
112
|
+
return undefined;
|
|
113
|
+
const security = spec["security"];
|
|
114
|
+
const just = asObject(security)?.["justification"];
|
|
115
|
+
const judge = asObject(just)?.["judge"];
|
|
116
|
+
return typeof judge === "string" ? judge : undefined;
|
|
117
|
+
}
|
|
118
|
+
function resolveThresholds(opts) {
|
|
119
|
+
return { ...DEFAULT_EGRESS_TRIAGE_THRESHOLDS, ...(opts?.thresholds ?? {}) };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Proposal 1 — per-sink relaxation for chronic warn-only flows. ADVICE-ONLY:
|
|
123
|
+
* the relaxation's home (`security.egressPolicy`) is reserved for the egress
|
|
124
|
+
* FRs and does not exist in the schema yet, so we emit guidance rather than a
|
|
125
|
+
* schema-adding patch (coordinate, don't clobber).
|
|
126
|
+
*/
|
|
127
|
+
export function ruleSinkRelaxation(ctx, opts) {
|
|
128
|
+
const t = resolveThresholds(opts);
|
|
129
|
+
const findings = [];
|
|
130
|
+
for (const c of ctx.clusters) {
|
|
131
|
+
if (c.blocked > 0)
|
|
132
|
+
continue; // never relax a sink that also blocked
|
|
133
|
+
if (c.warned < t.warnRelaxMin)
|
|
134
|
+
continue;
|
|
135
|
+
const originList = c.origins.length > 0 ? c.origins.join(", ") : "(unknown)";
|
|
136
|
+
findings.push({
|
|
137
|
+
id: `egress-relax:${c.sinkId}`,
|
|
138
|
+
severity: "info",
|
|
139
|
+
summary: `sink ${c.sinkId} warned ${c.warned}× with 0 blocks (origins: ${originList})`,
|
|
140
|
+
evidence: [
|
|
141
|
+
`${c.warned} warn-tier egress decisions to ${c.sinkId} (scope ${c.sinkScope}), 0 blocks (threshold: ≥${t.warnRelaxMin} warns)`,
|
|
142
|
+
`origins seen: ${originList}`,
|
|
143
|
+
],
|
|
144
|
+
counts: { warned: c.warned, blocked: c.blocked },
|
|
145
|
+
suggestion: {
|
|
146
|
+
kind: "advice",
|
|
147
|
+
text: `The sink \`${c.sinkId}\` has warned repeatedly (${c.warned}×) carrying content from [${originList}] but never had to BLOCK — a chronic-but-benign cross-origin flow. ` +
|
|
148
|
+
`If this flow is intended, relax it per-sink once the \`security.egressPolicy\` block ships (reserved for the egress-fabric FRs; not yet in the spec schema): a per-sink override like \`${c.sinkId}: { ${c.origins[0] ?? "origin"}: pass }\`. ` +
|
|
149
|
+
`Until then, review whether \`${c.sinkId}\` should carry [${originList}] content at all — a durable warn stream is exactly the signal to audit before whitelisting. This is deliberately NOT auto-applied.`,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return findings;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Proposal 2 — `security.egressMatcher: semantic` when warn-noise is high but
|
|
157
|
+
* blocks are zero (the substring matcher is over-firing on benign lineage
|
|
158
|
+
* overlaps). ADVICE-ONLY: egressMatcher is not optimizer-whitelisted.
|
|
159
|
+
*/
|
|
160
|
+
export function ruleSemanticMatcher(ctx, opts) {
|
|
161
|
+
const t = resolveThresholds(opts);
|
|
162
|
+
if (ctx.totalBlocked > 0)
|
|
163
|
+
return []; // real blocks → don't loosen matching
|
|
164
|
+
if (ctx.totalWarned < t.semanticWarnMin)
|
|
165
|
+
return [];
|
|
166
|
+
const current = asObject(opts?.spec?.["security"])?.["egressMatcher"];
|
|
167
|
+
if (current === "semantic")
|
|
168
|
+
return []; // already on
|
|
169
|
+
return [
|
|
170
|
+
{
|
|
171
|
+
id: "egress-matcher-semantic",
|
|
172
|
+
severity: "warn",
|
|
173
|
+
summary: `${ctx.totalWarned} warn-tier egress decisions, 0 blocks — substring matcher likely over-firing`,
|
|
174
|
+
evidence: [
|
|
175
|
+
`${ctx.totalWarned} warns across ${ctx.clusters.length} sink(s), 0 blocks (threshold: ≥${t.semanticWarnMin} warns, 0 blocks)`,
|
|
176
|
+
"high warn-noise with zero blocks is the signature of a substring matcher hitting benign lineage overlaps",
|
|
177
|
+
],
|
|
178
|
+
counts: { warned: ctx.totalWarned, blocked: ctx.totalBlocked },
|
|
179
|
+
suggestion: {
|
|
180
|
+
kind: "advice",
|
|
181
|
+
text: "Set `security.egressMatcher: semantic` in the spec so outbound payloads are scored against tagged data-lineage by cosine similarity instead of verbatim substring — this cuts the false-positive warn stream while keeping the same per-origin/per-sink block policy. Switching matchers changes detection semantics, so it is a human decision (never auto-applied) and needs an embedder wired (`--egress-embedder`).",
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Proposal 3 — `security.justification.judge: claude` when rule-based denials
|
|
188
|
+
* are frequent. SPEC-PATCH when the spec is a justification-optimizable target
|
|
189
|
+
* and the patch validates (rides `optimize --from-advice`); else advice.
|
|
190
|
+
*/
|
|
191
|
+
export function ruleJustificationJudgeUpgrade(ctx, opts) {
|
|
192
|
+
const t = resolveThresholds(opts);
|
|
193
|
+
if (ctx.ruleBasedDenials < t.judgeDenialMin)
|
|
194
|
+
return [];
|
|
195
|
+
const spec = opts?.spec;
|
|
196
|
+
if (specJustificationJudge(spec) === "claude")
|
|
197
|
+
return []; // already upgraded
|
|
198
|
+
const denyRate = ctx.ruleBasedEvaluated > 0 ? ctx.ruleBasedDenials / ctx.ruleBasedEvaluated : null;
|
|
199
|
+
const adviceText = "The rule-based justification judge denied frequently — the deterministic judge is prone to over-denying legitimate calls whose justification does not lexically match the goal. Set `security.justification.judge: claude` (and optionally `security.justification.model`) so the model-backed judge reasons about intent. This is eval-gated: `optimize --from-advice` applies it only if the eval pass-rate holds.";
|
|
200
|
+
// The whole `security.justification` block is the optimizable path; compose
|
|
201
|
+
// it preserving any existing model selection.
|
|
202
|
+
const existingModel = asObject(asObject(spec?.["security"])?.["justification"])?.["model"];
|
|
203
|
+
const value = { judge: "claude" };
|
|
204
|
+
if (typeof existingModel === "string")
|
|
205
|
+
value["model"] = existingModel;
|
|
206
|
+
const patch = {
|
|
207
|
+
target: spec?.target ?? "cli",
|
|
208
|
+
path: ["security", "justification"],
|
|
209
|
+
op: specJustificationJudge(spec) !== undefined ? "replace" : "add",
|
|
210
|
+
value,
|
|
211
|
+
rationale: `egress review: ${ctx.ruleBasedDenials} rule-based justification denials — upgrade to the claude judge`,
|
|
212
|
+
};
|
|
213
|
+
// Gate the patch on a justification-optimizable target; otherwise advice.
|
|
214
|
+
const suggestion = spec !== undefined && JUSTIFICATION_PATCH_TARGETS.has(spec.target)
|
|
215
|
+
? patchOrAdvice(spec, patch, adviceText)
|
|
216
|
+
: { kind: "advice", text: adviceText };
|
|
217
|
+
return [
|
|
218
|
+
{
|
|
219
|
+
id: "justification-judge-upgrade",
|
|
220
|
+
severity: "warn",
|
|
221
|
+
summary: `${ctx.ruleBasedDenials} rule-based justification denials — consider the claude judge`,
|
|
222
|
+
evidence: [
|
|
223
|
+
`${ctx.ruleBasedDenials} of ${ctx.ruleBasedEvaluated} rule-based justification evaluations denied${denyRate !== null ? ` (${(denyRate * 100).toFixed(0)}% deny rate)` : ""} (threshold: ≥${t.judgeDenialMin} denials)`,
|
|
224
|
+
],
|
|
225
|
+
counts: { denials: ctx.ruleBasedDenials, evaluated: ctx.ruleBasedEvaluated },
|
|
226
|
+
suggestion,
|
|
227
|
+
},
|
|
228
|
+
];
|
|
229
|
+
}
|
|
230
|
+
export const EGRESS_TRIAGE_RULES = [
|
|
231
|
+
ruleSinkRelaxation,
|
|
232
|
+
ruleSemanticMatcher,
|
|
233
|
+
ruleJustificationJudgeUpgrade,
|
|
234
|
+
];
|
|
235
|
+
/** Run every triage rule and rank: warn before info, then by primary count. */
|
|
236
|
+
export function runEgressTriage(ctx, opts) {
|
|
237
|
+
const findings = EGRESS_TRIAGE_RULES.flatMap((rule) => rule(ctx, opts));
|
|
238
|
+
const magnitude = (f) => Object.values(f.counts).reduce((max, n) => Math.max(max, n), 0);
|
|
239
|
+
return findings.sort((a, b) => {
|
|
240
|
+
if (a.severity !== b.severity)
|
|
241
|
+
return a.severity === "warn" ? -1 : 1;
|
|
242
|
+
const diff = magnitude(b) - magnitude(a);
|
|
243
|
+
if (diff !== 0)
|
|
244
|
+
return diff;
|
|
245
|
+
return a.id.localeCompare(b.id);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/** One line per finding for the CLI's text mode (mirrors formatFindingLines). */
|
|
249
|
+
export function formatEgressFindingLines(f) {
|
|
250
|
+
const lines = [`[${f.severity}] ${f.id} — ${f.summary}`];
|
|
251
|
+
for (const e of f.evidence)
|
|
252
|
+
lines.push(` · ${e}`);
|
|
253
|
+
if (f.suggestion.kind === "spec-patch") {
|
|
254
|
+
const p = f.suggestion.patch;
|
|
255
|
+
lines.push(` patch: ${p.op} ${p.path.join(".")} → ${JSON.stringify(p.value)}`);
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
lines.push(` advice: ${f.suggestion.text}`);
|
|
259
|
+
}
|
|
260
|
+
return lines;
|
|
261
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Item 10 — `crewhaus compile --with-eval-harness`: auto-generate an eval
|
|
3
|
+
* bridge (a `target: eval` bundle) for ANY shape's spec.
|
|
4
|
+
*
|
|
5
|
+
* `crewhaus eval` / `optimize` / `flywheel` hard-error on non-cli targets, so
|
|
6
|
+
* the ~13 non-cli shapes (channel-bot, voice, managed, onchain, …) cannot
|
|
7
|
+
* consume their own distilled Slack/web-UI feedback through the eval loop.
|
|
8
|
+
* The eval bridge closes that gap: it PROJECTS a shape's lowered IR into a
|
|
9
|
+
* sibling `IrEvalV0` — reusing the SAME agent model/instructions/tools — so
|
|
10
|
+
* `target-eval-bundle` can emit a first-class eval bundle that drives the
|
|
11
|
+
* shape's own datasets + graders.
|
|
12
|
+
*
|
|
13
|
+
* Two pieces live here, both side-effect-free (the CLI entry file does the IO
|
|
14
|
+
* — `mkdirSync`/`writeFileSync` of the emitted bundle):
|
|
15
|
+
*
|
|
16
|
+
* 1. `projectEvalIr(ir, opts)` — the IR → IrEvalV0 projection. Variant-
|
|
17
|
+
* agnostic: it pulls `{ model, instructions, tools }` off whichever agent
|
|
18
|
+
* sub-shape the source variant carries (cli/channel/managed/voice/browser/
|
|
19
|
+
* onchain/onchain-game/batch/research all carry a top-level `agent`).
|
|
20
|
+
* Shapes with NO single agent (workflow/graph/crew/pipeline are
|
|
21
|
+
* multi-stage) throw a clear diagnostic — their eval bridge is a per-step
|
|
22
|
+
* concern, deferred.
|
|
23
|
+
*
|
|
24
|
+
* 2. `selectInvoker(target)` — names the per-shape AgentInvoker strategy the
|
|
25
|
+
* eval run should wrap the shape's runtime with (via the eval-runner
|
|
26
|
+
* `opts.invoker` seam). The emitted bundle drives the default single-turn
|
|
27
|
+
* `runChatLoop` invoker (correct for the one-shot shapes); this mapping
|
|
28
|
+
* records the runtime seam each shape's eval should ideally wrap so the
|
|
29
|
+
* bundle README + the CLI report point the author at it. Deterministic and
|
|
30
|
+
* exhaustive over the shape set.
|
|
31
|
+
*/
|
|
32
|
+
import type { IrNode } from "@crewhaus/ir";
|
|
33
|
+
/** Thrown on an un-projectable shape / malformed bridge options. The CLI entry
|
|
34
|
+
* file routes it through `die()`; tests assert on `.message`. */
|
|
35
|
+
export declare class EvalBridgeError extends Error {
|
|
36
|
+
readonly name = "EvalBridgeError";
|
|
37
|
+
}
|
|
38
|
+
/** The projected eval IR — structurally an `IrEvalV0` (target: "eval"). Kept as
|
|
39
|
+
* a local type so this module needs no value import from `@crewhaus/ir`. */
|
|
40
|
+
export type ProjectedEvalIr = {
|
|
41
|
+
readonly version: 0;
|
|
42
|
+
readonly name: string;
|
|
43
|
+
readonly target: "eval";
|
|
44
|
+
readonly agent: {
|
|
45
|
+
readonly model: string;
|
|
46
|
+
readonly instructions: string;
|
|
47
|
+
readonly tools: readonly string[];
|
|
48
|
+
};
|
|
49
|
+
readonly dataset: {
|
|
50
|
+
readonly name: string;
|
|
51
|
+
readonly version: string;
|
|
52
|
+
readonly split: "train" | "dev" | "test";
|
|
53
|
+
};
|
|
54
|
+
readonly graders: readonly {
|
|
55
|
+
readonly name: string;
|
|
56
|
+
readonly opts?: Record<string, unknown>;
|
|
57
|
+
}[];
|
|
58
|
+
readonly concurrency: number;
|
|
59
|
+
readonly seed?: number;
|
|
60
|
+
};
|
|
61
|
+
/** The per-shape invoker strategy an eval run should wrap the shape with. Each
|
|
62
|
+
* entry names the runtime seam the shape resumes/steps through so a downstream
|
|
63
|
+
* invoker (via eval-runner `opts.invoker`) can drive it deterministically. */
|
|
64
|
+
export type InvokerStrategy = {
|
|
65
|
+
/** The source shape this strategy is for. */
|
|
66
|
+
readonly target: string;
|
|
67
|
+
/** Stable id for the invoker approach (used in README + report text). */
|
|
68
|
+
readonly kind: "single-turn-chat-loop" | "channel-resume-turn" | "voice-replay" | "gateway-request" | "chain-trigger" | "batch-item";
|
|
69
|
+
/** One-line human description of what the invoker wraps. */
|
|
70
|
+
readonly description: string;
|
|
71
|
+
};
|
|
72
|
+
export type ProjectEvalOptions = {
|
|
73
|
+
/**
|
|
74
|
+
* Dataset the eval bundle consumes. Defaults to `<specName>-eval` @ v1 on the
|
|
75
|
+
* dev split — the convention `dataset mine` / `distill --register` write to,
|
|
76
|
+
* so a shape's distilled feedback is discoverable without extra config.
|
|
77
|
+
*/
|
|
78
|
+
readonly datasetName?: string;
|
|
79
|
+
readonly datasetVersion?: string;
|
|
80
|
+
readonly datasetSplit?: "train" | "dev" | "test";
|
|
81
|
+
/**
|
|
82
|
+
* Grader names for the projected bundle. Defaults to a single
|
|
83
|
+
* `substring_match` (a registry built-in) so the bundle compiles + runs
|
|
84
|
+
* credential-free; authors swap in their own graders.yaml-derived names.
|
|
85
|
+
*/
|
|
86
|
+
readonly graders?: readonly {
|
|
87
|
+
readonly name: string;
|
|
88
|
+
readonly opts?: Record<string, unknown>;
|
|
89
|
+
}[];
|
|
90
|
+
readonly concurrency?: number;
|
|
91
|
+
readonly seed?: number;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Project a lowered IR node of ANY projectable shape into an `IrEvalV0`. The
|
|
95
|
+
* eval bundle emitted from the result reuses the source shape's agent verbatim,
|
|
96
|
+
* so the shape is evaluated on the exact model/instructions/tools it ships.
|
|
97
|
+
*
|
|
98
|
+
* Throws `EvalBridgeError` when the source is already `target: eval` (nothing to
|
|
99
|
+
* bridge) or is a multi-stage shape with no single agent.
|
|
100
|
+
*/
|
|
101
|
+
export declare function projectEvalIr(ir: IrNode, opts?: ProjectEvalOptions): ProjectedEvalIr;
|
|
102
|
+
/**
|
|
103
|
+
* Select the per-shape AgentInvoker strategy an eval run should wrap the
|
|
104
|
+
* shape's runtime with. Exhaustive over the projectable shapes; unknown shapes
|
|
105
|
+
* fall back to the single-turn chat loop (the eval-runner default invoker).
|
|
106
|
+
*/
|
|
107
|
+
export declare function selectInvoker(target: string): InvokerStrategy;
|
|
108
|
+
/** Where the eval bridge bundle is written, relative to the primary out-dir. */
|
|
109
|
+
export declare const EVAL_BRIDGE_SUBDIR = "eval";
|
|
110
|
+
/**
|
|
111
|
+
* Render the one-line summary the CLI prints after emitting an eval bridge, so
|
|
112
|
+
* the author knows which invoker seam to wrap for a faithful eval.
|
|
113
|
+
*/
|
|
114
|
+
export declare function describeBridge(projected: ProjectedEvalIr, strategy: InvokerStrategy): string;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/** Thrown on an un-projectable shape / malformed bridge options. The CLI entry
|
|
2
|
+
* file routes it through `die()`; tests assert on `.message`. */
|
|
3
|
+
export class EvalBridgeError extends Error {
|
|
4
|
+
name = "EvalBridgeError";
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Shapes that carry a single top-level `agent { model, instructions, tools }`
|
|
8
|
+
* and can therefore be projected 1:1 into an eval bundle. Multi-stage shapes
|
|
9
|
+
* (workflow/graph/crew/pipeline) are intentionally excluded — their eval
|
|
10
|
+
* bridge is a per-step projection, out of scope for slice 1.
|
|
11
|
+
*/
|
|
12
|
+
const PROJECTABLE_TARGETS = new Set([
|
|
13
|
+
"cli",
|
|
14
|
+
"channel",
|
|
15
|
+
"managed",
|
|
16
|
+
"voice",
|
|
17
|
+
"browser",
|
|
18
|
+
"onchain",
|
|
19
|
+
"onchain-game",
|
|
20
|
+
"batch",
|
|
21
|
+
"research",
|
|
22
|
+
]);
|
|
23
|
+
/** Multi-stage shapes with no single agent — a clearer error than "no model". */
|
|
24
|
+
const MULTISTAGE_TARGETS = new Map([
|
|
25
|
+
["workflow", "steps"],
|
|
26
|
+
["graph", "nodes"],
|
|
27
|
+
["crew", "roles"],
|
|
28
|
+
["pipeline", "stages"],
|
|
29
|
+
]);
|
|
30
|
+
const DEFAULT_GRADERS = [{ name: "substring_match" }];
|
|
31
|
+
const DEFAULT_CONCURRENCY = 2;
|
|
32
|
+
/**
|
|
33
|
+
* Pull `{ model, instructions, tools }` off a lowered IR node, variant-
|
|
34
|
+
* agnostically. Returns undefined for shapes with no single agent sub-shape.
|
|
35
|
+
*/
|
|
36
|
+
function extractAgent(ir) {
|
|
37
|
+
const node = ir;
|
|
38
|
+
const agent = node.agent;
|
|
39
|
+
if (agent === undefined || typeof agent !== "object")
|
|
40
|
+
return undefined;
|
|
41
|
+
if (typeof agent.model !== "string" || typeof agent.instructions !== "string")
|
|
42
|
+
return undefined;
|
|
43
|
+
// Tools may live on `agent.tools` (eval/browser/channel/onchain/…) or on a
|
|
44
|
+
// sibling top-level `tools` (some variants). Prefer the agent-scoped list.
|
|
45
|
+
const tools = Array.isArray(agent.tools)
|
|
46
|
+
? agent.tools
|
|
47
|
+
: Array.isArray(node.tools)
|
|
48
|
+
? node.tools
|
|
49
|
+
: [];
|
|
50
|
+
return {
|
|
51
|
+
model: agent.model,
|
|
52
|
+
instructions: agent.instructions,
|
|
53
|
+
tools: tools.filter((t) => typeof t === "string"),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Project a lowered IR node of ANY projectable shape into an `IrEvalV0`. The
|
|
58
|
+
* eval bundle emitted from the result reuses the source shape's agent verbatim,
|
|
59
|
+
* so the shape is evaluated on the exact model/instructions/tools it ships.
|
|
60
|
+
*
|
|
61
|
+
* Throws `EvalBridgeError` when the source is already `target: eval` (nothing to
|
|
62
|
+
* bridge) or is a multi-stage shape with no single agent.
|
|
63
|
+
*/
|
|
64
|
+
export function projectEvalIr(ir, opts = {}) {
|
|
65
|
+
if (ir.target === "eval") {
|
|
66
|
+
throw new EvalBridgeError(`spec "${ir.name}" is already target: eval — no eval bridge needed (compile it directly)`);
|
|
67
|
+
}
|
|
68
|
+
if (ir.target === "cli") {
|
|
69
|
+
throw new EvalBridgeError(`spec "${ir.name}" is target: cli — use \`crewhaus eval\` directly (the eval bridge is for non-cli shapes)`);
|
|
70
|
+
}
|
|
71
|
+
const multistage = MULTISTAGE_TARGETS.get(ir.target);
|
|
72
|
+
if (multistage !== undefined) {
|
|
73
|
+
throw new EvalBridgeError(`spec "${ir.name}" is target: ${ir.target} — a multi-stage shape (${multistage}) has no single agent to project; per-step eval bridges are not yet supported`);
|
|
74
|
+
}
|
|
75
|
+
if (!PROJECTABLE_TARGETS.has(ir.target)) {
|
|
76
|
+
throw new EvalBridgeError(`spec "${ir.name}" target: ${ir.target} is not projectable into an eval bridge`);
|
|
77
|
+
}
|
|
78
|
+
const agent = extractAgent(ir);
|
|
79
|
+
if (agent === undefined) {
|
|
80
|
+
throw new EvalBridgeError(`spec "${ir.name}" (target: ${ir.target}) has no agent { model, instructions } to project into an eval bridge`);
|
|
81
|
+
}
|
|
82
|
+
const graders = opts.graders !== undefined && opts.graders.length > 0 ? opts.graders : DEFAULT_GRADERS;
|
|
83
|
+
const projected = {
|
|
84
|
+
version: 0,
|
|
85
|
+
name: ir.name,
|
|
86
|
+
target: "eval",
|
|
87
|
+
agent: { model: agent.model, instructions: agent.instructions, tools: agent.tools },
|
|
88
|
+
dataset: {
|
|
89
|
+
name: opts.datasetName ?? `${ir.name}-eval`,
|
|
90
|
+
version: opts.datasetVersion ?? "v1",
|
|
91
|
+
split: opts.datasetSplit ?? "dev",
|
|
92
|
+
},
|
|
93
|
+
graders: graders.map((g) => ({
|
|
94
|
+
name: g.name,
|
|
95
|
+
...(g.opts !== undefined ? { opts: g.opts } : {}),
|
|
96
|
+
})),
|
|
97
|
+
concurrency: opts.concurrency ?? DEFAULT_CONCURRENCY,
|
|
98
|
+
...(opts.seed !== undefined ? { seed: opts.seed } : {}),
|
|
99
|
+
};
|
|
100
|
+
return projected;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Select the per-shape AgentInvoker strategy an eval run should wrap the
|
|
104
|
+
* shape's runtime with. Exhaustive over the projectable shapes; unknown shapes
|
|
105
|
+
* fall back to the single-turn chat loop (the eval-runner default invoker).
|
|
106
|
+
*/
|
|
107
|
+
export function selectInvoker(target) {
|
|
108
|
+
switch (target) {
|
|
109
|
+
case "channel":
|
|
110
|
+
return {
|
|
111
|
+
target,
|
|
112
|
+
kind: "channel-resume-turn",
|
|
113
|
+
description: "wrap the channel-bot single-turn resume path (channel-adapter runTurn) so each dataset sample is a fresh inbound message",
|
|
114
|
+
};
|
|
115
|
+
case "voice":
|
|
116
|
+
return {
|
|
117
|
+
target,
|
|
118
|
+
kind: "voice-replay",
|
|
119
|
+
description: "replay recorded call-session transcripts through the voice runtime (see `crewhaus eval --voice`)",
|
|
120
|
+
};
|
|
121
|
+
case "managed":
|
|
122
|
+
return {
|
|
123
|
+
target,
|
|
124
|
+
kind: "gateway-request",
|
|
125
|
+
description: "drive the managed gateway's run handler per sample (one authenticated request per dataset row)",
|
|
126
|
+
};
|
|
127
|
+
case "onchain":
|
|
128
|
+
case "onchain-game":
|
|
129
|
+
return {
|
|
130
|
+
target,
|
|
131
|
+
kind: "chain-trigger",
|
|
132
|
+
description: "invoke the onchain daemon's trigger handler per sample against a simulated chain adapter (no live broadcast)",
|
|
133
|
+
};
|
|
134
|
+
case "batch":
|
|
135
|
+
return {
|
|
136
|
+
target,
|
|
137
|
+
kind: "batch-item",
|
|
138
|
+
description: "run one batch-worker item per dataset sample through the queue consumer",
|
|
139
|
+
};
|
|
140
|
+
default:
|
|
141
|
+
return {
|
|
142
|
+
target,
|
|
143
|
+
kind: "single-turn-chat-loop",
|
|
144
|
+
description: "single-turn runChatLoop over the shape's agent (the eval-runner default invoker)",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** Where the eval bridge bundle is written, relative to the primary out-dir. */
|
|
149
|
+
export const EVAL_BRIDGE_SUBDIR = "eval";
|
|
150
|
+
/**
|
|
151
|
+
* Render the one-line summary the CLI prints after emitting an eval bridge, so
|
|
152
|
+
* the author knows which invoker seam to wrap for a faithful eval.
|
|
153
|
+
*/
|
|
154
|
+
export function describeBridge(projected, strategy) {
|
|
155
|
+
return (`eval bridge for target: ${strategy.target} → dataset ${projected.dataset.name}@${projected.dataset.version}` +
|
|
156
|
+
`#${projected.dataset.split}, graders [${projected.graders.map((g) => g.name).join(", ")}]; ` +
|
|
157
|
+
`invoker: ${strategy.kind} (${strategy.description})`);
|
|
158
|
+
}
|