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.
- 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 +246 -0
- package/dist/feedback.js +612 -0
- 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 +11727 -740
- 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 -64
package/dist/fewshot.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Item #54 — harvest up-rated turns into a golden few-shot pool.
|
|
3
|
+
*
|
|
4
|
+
* `crewhaus fewshot harvest` mines POSITIVELY-rated conversation turns (the
|
|
5
|
+
* same `user_feedback` ratings `distill` consumes) into a curated pool of
|
|
6
|
+
* input→good-output few-shot examples. The pool is a versioned, provenance-
|
|
7
|
+
* tagged JSONL under `.crewhaus/fewshot/<spec>.jsonl`; `crewhaus optimize
|
|
8
|
+
* --few-shot <file>` injects the top-K examples into the agent prompt as
|
|
9
|
+
* in-context demonstrations, and `agent.instructions` can carry them at
|
|
10
|
+
* compile time via `formatFewShotForPrompt`.
|
|
11
|
+
*
|
|
12
|
+
* Everything here is pure + deterministic (stable ordering, id-based dedupe)
|
|
13
|
+
* so it is unit-testable; all filesystem access + the model/redactor wiring
|
|
14
|
+
* lives in `apps/cli/src/index.ts` (mirrors `feedback.ts` / `dataset-mine.ts`).
|
|
15
|
+
* The harvested OUTPUT text is the rated assistant answer (or the user's
|
|
16
|
+
* correction), so it is PII/secret-redacted before it lands in the pool: the
|
|
17
|
+
* caller threads a redactor built from `SYNTHESIZE_PII_DETECTORS` (the shared
|
|
18
|
+
* secret/API-key + DEFAULT_PII detector set) so a pasted credential never
|
|
19
|
+
* survives into the pool or the optimizer meta-prompt.
|
|
20
|
+
*/
|
|
21
|
+
import { mergeFeedback, normalizeRating } from "./feedback";
|
|
22
|
+
/** Schema version of a persisted few-shot pool line. */
|
|
23
|
+
export const FEWSHOT_SCHEMA_VERSION = 1;
|
|
24
|
+
function turnKey(sessionId, turnNumber) {
|
|
25
|
+
return `${sessionId}#${turnNumber}`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Harvest positively-rated turns into few-shot examples. A turn qualifies when
|
|
29
|
+
* its merged rating normalizes to ≥ `minScore`, OR it carries a `correction`
|
|
30
|
+
* (the user's own better answer — always gold). The example OUTPUT is the
|
|
31
|
+
* correction when present, else the assistant's answer; empty answers are
|
|
32
|
+
* skipped. Deterministic: examples are sorted by (score desc, id asc) and
|
|
33
|
+
* deduped by id, so re-harvesting the same feedback is idempotent.
|
|
34
|
+
*/
|
|
35
|
+
export async function harvestFewShot(turns, feedback, opts = {}) {
|
|
36
|
+
const minScore = opts.minScore ?? 0.7;
|
|
37
|
+
const redact = opts.redact ?? (async (t) => t);
|
|
38
|
+
const turnByKey = new Map();
|
|
39
|
+
for (const t of turns)
|
|
40
|
+
turnByKey.set(turnKey(t.sessionId, t.turnNumber), t);
|
|
41
|
+
const merged = mergeFeedback(feedback);
|
|
42
|
+
const byId = new Map();
|
|
43
|
+
let qualified = 0;
|
|
44
|
+
let skippedUnmatched = 0;
|
|
45
|
+
let skippedEmpty = 0;
|
|
46
|
+
for (const fb of merged) {
|
|
47
|
+
const turn = turnByKey.get(turnKey(fb.sessionId, fb.turnNumber));
|
|
48
|
+
if (turn === undefined) {
|
|
49
|
+
skippedUnmatched += 1;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const score = normalizeRating(fb);
|
|
53
|
+
const isCorrection = fb.correction !== undefined && fb.correction.trim() !== "";
|
|
54
|
+
const isPositive = (score !== undefined && score >= minScore) || isCorrection;
|
|
55
|
+
if (!isPositive)
|
|
56
|
+
continue;
|
|
57
|
+
const rawOutput = isCorrection ? fb.correction : turn.output;
|
|
58
|
+
if (rawOutput.trim() === "") {
|
|
59
|
+
skippedEmpty += 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const input = await redact(turn.input);
|
|
63
|
+
const output = await redact(rawOutput);
|
|
64
|
+
if (output.trim() === "") {
|
|
65
|
+
skippedEmpty += 1;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
qualified += 1;
|
|
69
|
+
const id = `${fb.sessionId}_t${fb.turnNumber}`;
|
|
70
|
+
byId.set(id, {
|
|
71
|
+
schemaVersion: FEWSHOT_SCHEMA_VERSION,
|
|
72
|
+
id,
|
|
73
|
+
input,
|
|
74
|
+
output,
|
|
75
|
+
score: isCorrection ? 1 : (score ?? 1),
|
|
76
|
+
provenance: {
|
|
77
|
+
sessionId: fb.sessionId,
|
|
78
|
+
turnNumber: fb.turnNumber,
|
|
79
|
+
source: isCorrection ? "correction" : "rating",
|
|
80
|
+
...(fb.rater !== undefined ? { rater: fb.rater } : {}),
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const examples = [...byId.values()].sort((a, b) => b.score - a.score || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
85
|
+
return {
|
|
86
|
+
examples,
|
|
87
|
+
stats: {
|
|
88
|
+
totalFeedback: merged.length,
|
|
89
|
+
qualified,
|
|
90
|
+
skippedUnmatched,
|
|
91
|
+
skippedEmpty,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** Narrow an arbitrary parsed JSON value to a persisted FewShotExample. */
|
|
96
|
+
export function isFewShotExample(value) {
|
|
97
|
+
if (typeof value !== "object" || value === null)
|
|
98
|
+
return false;
|
|
99
|
+
const v = value;
|
|
100
|
+
if (v["schemaVersion"] !== 1)
|
|
101
|
+
return false;
|
|
102
|
+
if (typeof v["id"] !== "string")
|
|
103
|
+
return false;
|
|
104
|
+
if (typeof v["input"] !== "string" || typeof v["output"] !== "string")
|
|
105
|
+
return false;
|
|
106
|
+
if (typeof v["score"] !== "number" || !Number.isFinite(v["score"]))
|
|
107
|
+
return false;
|
|
108
|
+
const p = v["provenance"];
|
|
109
|
+
if (typeof p !== "object" || p === null)
|
|
110
|
+
return false;
|
|
111
|
+
const pr = p;
|
|
112
|
+
return typeof pr["sessionId"] === "string" && typeof pr["turnNumber"] === "number";
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Merge a freshly-harvested pool into an existing one, deduping by id (the
|
|
116
|
+
* newer example wins so re-runs refresh outputs without duplicating). Returns
|
|
117
|
+
* the deterministically-ordered union.
|
|
118
|
+
*/
|
|
119
|
+
export function mergePools(existing, fresh) {
|
|
120
|
+
const byId = new Map();
|
|
121
|
+
for (const e of existing)
|
|
122
|
+
byId.set(e.id, e);
|
|
123
|
+
for (const e of fresh)
|
|
124
|
+
byId.set(e.id, e);
|
|
125
|
+
return [...byId.values()].sort((a, b) => b.score - a.score || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
126
|
+
}
|
|
127
|
+
/** Serialize a pool to JSONL (one example per line). */
|
|
128
|
+
export function poolToJsonl(examples) {
|
|
129
|
+
return `${examples.map((e) => JSON.stringify(e)).join("\n")}\n`;
|
|
130
|
+
}
|
|
131
|
+
const FEWSHOT_CLOSE_TAG_RE = /<\s*\/\s*few_shot_examples\s*>/gi;
|
|
132
|
+
/**
|
|
133
|
+
* #54 F5 — delimiter safety. An example's input/output is user/model text and
|
|
134
|
+
* can be adversarially shaped, so before interpolating it into the
|
|
135
|
+
* `<few_shot_examples>` block we neutralize any embedded closing tag: a
|
|
136
|
+
* `</few_shot_examples>` inside an example would otherwise let a poisoned
|
|
137
|
+
* example terminate the block early and inject trailing instructions. The
|
|
138
|
+
* closing tag is rewritten to an inert `<\/few_shot_examples>` (content
|
|
139
|
+
* preserved for the model, but no longer parses as the real delimiter).
|
|
140
|
+
* Redaction of secrets/PII already runs upstream — this is purely structural.
|
|
141
|
+
*/
|
|
142
|
+
function escapeFewShotContent(text) {
|
|
143
|
+
return text.replace(FEWSHOT_CLOSE_TAG_RE, "<\\/few_shot_examples>");
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Render the top-K examples as an in-context few-shot block for the system
|
|
147
|
+
* prompt / `agent.instructions`. Empty input → "" so callers append
|
|
148
|
+
* unconditionally. `k` caps how many examples are injected (default 5).
|
|
149
|
+
*/
|
|
150
|
+
export function formatFewShotForPrompt(examples, k = 5) {
|
|
151
|
+
const top = examples.slice(0, Math.max(0, k));
|
|
152
|
+
if (top.length === 0)
|
|
153
|
+
return "";
|
|
154
|
+
const blocks = top
|
|
155
|
+
.map((e, i) => `Example ${i + 1}:\nUser: ${escapeFewShotContent(e.input)}\nAssistant: ${escapeFewShotContent(e.output)}`)
|
|
156
|
+
.join("\n\n");
|
|
157
|
+
return `<few_shot_examples>\nThese are examples of responses users rated highly. Follow their style and quality:\n\n${blocks}\n</few_shot_examples>`;
|
|
158
|
+
}
|
package/dist/fleet.d.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { Manifest } from "@crewhaus/spec-registry";
|
|
2
|
+
/** The spec file that roots a standalone harness. */
|
|
3
|
+
export declare const HARNESS_SPEC_FILENAME = "crewhaus.yaml";
|
|
4
|
+
/** Thrown for operational failures (missing root, empty filter, disallowed
|
|
5
|
+
* bulk subcommand). The CLI entry file catches it and routes the message
|
|
6
|
+
* through `die()`; tests assert on `.message` without the process exiting. */
|
|
7
|
+
export declare class FleetError extends Error {
|
|
8
|
+
readonly name = "FleetError";
|
|
9
|
+
}
|
|
10
|
+
/** A discovered harness: its absolute directory + the spec path within it. */
|
|
11
|
+
export type DiscoveredHarness = {
|
|
12
|
+
readonly dir: string;
|
|
13
|
+
readonly specPath: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Walk `root` for directories carrying a `crewhaus.yaml`, bounded to
|
|
17
|
+
* {@link MAX_DISCOVERY_DEPTH}. Never descends into `.crewhaus`, `node_modules`,
|
|
18
|
+
* `.git`, `dist`, or `.worktrees`. A directory that IS a harness is still
|
|
19
|
+
* descended into (a harness may nest sub-harnesses in a demo tree), just not
|
|
20
|
+
* through its state dir. Results are sorted by directory for stable output.
|
|
21
|
+
*/
|
|
22
|
+
export declare function discoverHarnesses(root: string): DiscoveredHarness[];
|
|
23
|
+
/** The identity fields a fleet row needs, read best-effort. */
|
|
24
|
+
export type SpecHeader = {
|
|
25
|
+
readonly name?: string;
|
|
26
|
+
readonly target?: string;
|
|
27
|
+
readonly model?: string;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Read `name`, `target`, and `model` from a `crewhaus.yaml` WITHOUT a full
|
|
31
|
+
* schema parse. A fleet scan must survive a stale/invalid spec (schema drift,
|
|
32
|
+
* a half-edited file), so this is a tolerant top-level-key scrape rather than
|
|
33
|
+
* `parseSpec`. `name`/`target` are top-level on every target shape; `model`
|
|
34
|
+
* lives at `agent.model` (cli/channel/voice) OR top-level `model`
|
|
35
|
+
* (workflow/graph/pipeline/crew) depending on the shape — the first
|
|
36
|
+
* `model:` at any indentation is taken. Values are unquoted and trimmed.
|
|
37
|
+
*/
|
|
38
|
+
export declare function readSpecHeader(yamlText: string): SpecHeader;
|
|
39
|
+
/** The `.crewhaus/specs`-registered version + env pins for a harness spec. */
|
|
40
|
+
export type RegistryStatus = {
|
|
41
|
+
/** Latest registered version (`v3`), or undefined when nothing is registered. */
|
|
42
|
+
readonly latestVersion?: string;
|
|
43
|
+
/** env → version pins from the registry manifest. */
|
|
44
|
+
readonly pins: Readonly<Record<string, string>>;
|
|
45
|
+
};
|
|
46
|
+
/** The newest recorded eval run for a harness. */
|
|
47
|
+
export type LastEval = {
|
|
48
|
+
readonly datasetName: string;
|
|
49
|
+
readonly passRate: number;
|
|
50
|
+
readonly ts: string;
|
|
51
|
+
};
|
|
52
|
+
/** Minimal shape of an eval index entry the fleet row needs. */
|
|
53
|
+
export type LastEvalEntry = {
|
|
54
|
+
readonly datasetName: string;
|
|
55
|
+
readonly passRate: number;
|
|
56
|
+
readonly ts: string;
|
|
57
|
+
};
|
|
58
|
+
/** One harness's rolled-up inventory row. */
|
|
59
|
+
export type HarnessInventory = {
|
|
60
|
+
readonly dir: string;
|
|
61
|
+
readonly header: SpecHeader;
|
|
62
|
+
/** Best-effort spec name (header name, else the dir basename). */
|
|
63
|
+
readonly specName: string;
|
|
64
|
+
readonly registry: RegistryStatus;
|
|
65
|
+
readonly lastEval?: LastEval;
|
|
66
|
+
readonly sessionCount: number;
|
|
67
|
+
readonly feedbackCount: number;
|
|
68
|
+
/** True when the spec file did not read at all (unreadable/absent). */
|
|
69
|
+
readonly specUnreadable: boolean;
|
|
70
|
+
};
|
|
71
|
+
/** Count `sess_<16hex>.json` files under `.crewhaus/sessions`. */
|
|
72
|
+
export declare function countSessions(harnessDir: string): number;
|
|
73
|
+
/**
|
|
74
|
+
* Distinct feedback a harness carries, counted the way `crewhaus distill`
|
|
75
|
+
* folds it: `user_feedback` events in `sessions/*.jsonl` PLUS bare records in
|
|
76
|
+
* `feedback/*.jsonl` (`extractFeedbackRecords` accepts both encodings), then
|
|
77
|
+
* `mergeFeedback` collapses to one per (sessionId, turnNumber). The row shows
|
|
78
|
+
* distinct rated turns, not raw line count, so a re-rated turn counts once.
|
|
79
|
+
*/
|
|
80
|
+
export declare function countFeedback(harnessDir: string): number;
|
|
81
|
+
/** The newest run in an eval index, or undefined. Newest by `ts` (ISO-8601
|
|
82
|
+
* lexical order is chronological). */
|
|
83
|
+
export declare function lastEvalFor(evalEntries: ReadonlyArray<LastEvalEntry>): LastEval | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Manifest reader seam — production passes a closure over
|
|
86
|
+
* `@crewhaus/spec-registry`; tests pass a stub. Returns undefined when the
|
|
87
|
+
* spec has no registry entry (never registered / compiled).
|
|
88
|
+
*/
|
|
89
|
+
export type ManifestReader = (specName: string, registryRoot: string) => Promise<Manifest | undefined>;
|
|
90
|
+
/** Eval-index reader seam — production passes `readRunIndex`; tests stub. */
|
|
91
|
+
export type EvalIndexReader = (evalsDir: string) => ReadonlyArray<LastEvalEntry>;
|
|
92
|
+
export type BuildInventoryDeps = {
|
|
93
|
+
readonly readManifest: ManifestReader;
|
|
94
|
+
readonly readEvalIndex: EvalIndexReader;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Aggregate one harness directory into an inventory row. All reads are
|
|
98
|
+
* tolerant: a missing state dir, an unregistered spec, or an unreadable spec
|
|
99
|
+
* file each degrade to an empty/absent field rather than throwing, so one
|
|
100
|
+
* broken harness never aborts a `fleet list`.
|
|
101
|
+
*/
|
|
102
|
+
export declare function buildHarnessInventory(harness: DiscoveredHarness, deps: BuildInventoryDeps): Promise<HarnessInventory>;
|
|
103
|
+
/** Build the full fleet inventory for a root. */
|
|
104
|
+
export declare function buildFleetInventory(root: string, deps: BuildInventoryDeps): Promise<HarnessInventory[]>;
|
|
105
|
+
/** Render the inventory as aligned columns for `fleet list`. */
|
|
106
|
+
export declare function formatInventory(rows: ReadonlyArray<HarnessInventory>, root: string): ReadonlyArray<string>;
|
|
107
|
+
export type HarnessHealth = {
|
|
108
|
+
readonly dir: string;
|
|
109
|
+
readonly specName: string;
|
|
110
|
+
/** True when a compiled bundle / registry entry exists (deployable). */
|
|
111
|
+
readonly registered: boolean;
|
|
112
|
+
/** env pins present. */
|
|
113
|
+
readonly pinnedEnvs: ReadonlyArray<string>;
|
|
114
|
+
/** True when a pinned eval baseline exists AND the last run met/held it. */
|
|
115
|
+
readonly evalHealthy: boolean;
|
|
116
|
+
readonly evalNote: string;
|
|
117
|
+
/** Open incidents from the ops-batch incidents dir, if present. */
|
|
118
|
+
readonly openIncidents: number;
|
|
119
|
+
/** Whether `.crewhaus/audit` exists (tamper-evidence available). */
|
|
120
|
+
readonly hasAudit: boolean;
|
|
121
|
+
};
|
|
122
|
+
/** Reader seam: newest pass-rate + whether it dropped vs the pinned baseline. */
|
|
123
|
+
export type EvalHealthReader = (evalsDir: string, specName: string) => {
|
|
124
|
+
healthy: boolean;
|
|
125
|
+
note: string;
|
|
126
|
+
};
|
|
127
|
+
/** Count open incidents under `.crewhaus/incidents` (files not ending
|
|
128
|
+
* `.resolved.json`). Absent dir → 0. The ops batch writes one JSON per
|
|
129
|
+
* incident; a resolved one is renamed `<id>.resolved.json`. */
|
|
130
|
+
export declare function countOpenIncidents(harnessDir: string): number;
|
|
131
|
+
export declare function buildHarnessHealth(inv: HarnessInventory, readEvalHealth: EvalHealthReader): Promise<HarnessHealth>;
|
|
132
|
+
/** Render the health rollup as status lines for `fleet status`. */
|
|
133
|
+
export declare function formatHealth(rows: ReadonlyArray<HarnessHealth>, root: string): ReadonlyArray<string>;
|
|
134
|
+
/** ✓ healthy / ✗ needs attention. Attention = an open incident, an eval
|
|
135
|
+
* regression, or an unregistered harness that nonetheless has env pins
|
|
136
|
+
* (a pin pointing at nothing registered). */
|
|
137
|
+
export declare function healthMark(r: HarnessHealth): "✓" | "✗";
|
|
138
|
+
/** Read-only / idempotent subcommands the bulk runner accepts without an
|
|
139
|
+
* explicit opt-in. Each maps to a `crewhaus <argv...>` invocation. */
|
|
140
|
+
export declare const READ_ONLY_BULK_COMMANDS: Readonly<Record<string, ReadonlyArray<string>>>;
|
|
141
|
+
/** A resolved bulk-run plan: the base argv + whether it is a mutating command. */
|
|
142
|
+
export type BulkCommandPlan = {
|
|
143
|
+
/** The base argv (e.g. ["security","digest"]). */
|
|
144
|
+
readonly argv: ReadonlyArray<string>;
|
|
145
|
+
/** True when the command is off the read-only allow-list. */
|
|
146
|
+
readonly mutating: boolean;
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Resolve a `fleet run` subcommand + trailing args into a plan. Refuses
|
|
150
|
+
* anything off the read-only allow-list unless `allowMutating` is set. The
|
|
151
|
+
* longest allow-listed prefix wins, so `security digest` resolves ahead of a
|
|
152
|
+
* bare `security`.
|
|
153
|
+
*/
|
|
154
|
+
export declare function resolveBulkCommand(subcommandTokens: ReadonlyArray<string>, allowMutating: boolean): BulkCommandPlan;
|
|
155
|
+
/** Match a harness against a `--filter` glob, tested against BOTH the spec
|
|
156
|
+
* name and the directory basename, so `--filter '*bot*'` catches either. */
|
|
157
|
+
export declare function matchesFilter(inv: HarnessInventory, filter: string | undefined): boolean;
|
|
158
|
+
/** Launch seam: run `crewhaus <argv>` in `cwd`, resolve to an exit code + the
|
|
159
|
+
* captured tail of output. Production wires this to `Bun.spawn`; tests inject
|
|
160
|
+
* a deterministic stub so no subprocess runs. */
|
|
161
|
+
export type FleetRunner = (opts: {
|
|
162
|
+
readonly cwd: string;
|
|
163
|
+
readonly argv: ReadonlyArray<string>;
|
|
164
|
+
}) => Promise<{
|
|
165
|
+
readonly exitCode: number;
|
|
166
|
+
readonly tail: string;
|
|
167
|
+
}>;
|
|
168
|
+
/** Per-harness confirm seam for `--allow-mutating` (interactive prompt in the
|
|
169
|
+
* CLI; a predicate in tests). Returning false skips that harness. */
|
|
170
|
+
export type ConfirmMutating = (inv: HarnessInventory, argv: ReadonlyArray<string>) => Promise<boolean>;
|
|
171
|
+
export type BulkRunResult = {
|
|
172
|
+
readonly inv: HarnessInventory;
|
|
173
|
+
readonly ran: boolean;
|
|
174
|
+
/** exit code when ran; undefined when skipped (filter/decline). */
|
|
175
|
+
readonly exitCode?: number;
|
|
176
|
+
readonly tail?: string;
|
|
177
|
+
/** Reason a harness was skipped, if it was. */
|
|
178
|
+
readonly skipReason?: string;
|
|
179
|
+
};
|
|
180
|
+
export type RunFleetBulkOptions = {
|
|
181
|
+
readonly root: string;
|
|
182
|
+
readonly subcommandTokens: ReadonlyArray<string>;
|
|
183
|
+
readonly filter?: string;
|
|
184
|
+
readonly allowMutating: boolean;
|
|
185
|
+
readonly deps: BuildInventoryDeps;
|
|
186
|
+
readonly runner: FleetRunner;
|
|
187
|
+
readonly confirm: ConfirmMutating;
|
|
188
|
+
};
|
|
189
|
+
export type RunFleetBulkReport = {
|
|
190
|
+
readonly plan: BulkCommandPlan;
|
|
191
|
+
readonly results: ReadonlyArray<BulkRunResult>;
|
|
192
|
+
/** Harnesses that ran and exited non-zero. */
|
|
193
|
+
readonly failed: number;
|
|
194
|
+
/** Harnesses that ran and exited zero. */
|
|
195
|
+
readonly passed: number;
|
|
196
|
+
/** Harnesses skipped (filtered out or declined). */
|
|
197
|
+
readonly skipped: number;
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* Run one subcommand across the filtered fleet. Read-only commands run
|
|
201
|
+
* unconditionally on every matched harness; a mutating command (only reachable
|
|
202
|
+
* with `allowMutating`) runs a harness only when `confirm` approves it. The
|
|
203
|
+
* runner is the sole side-effect surface, injected for testability.
|
|
204
|
+
*/
|
|
205
|
+
export declare function runFleetBulk(opts: RunFleetBulkOptions): Promise<RunFleetBulkReport>;
|
|
206
|
+
/** Render a bulk-run report as summary lines for `fleet run`. */
|
|
207
|
+
export declare function formatBulkReport(report: RunFleetBulkReport): ReadonlyArray<string>;
|