crewhaus 0.1.8 → 0.2.1
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 +173 -0
- package/dist/dataset-mine.js +404 -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 +11664 -963
- 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/route.d.ts +19 -0
- package/dist/route.js +90 -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 +174 -0
- package/dist/security-digest.js +656 -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 +300 -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 +90 -65
package/dist/fleet.js
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Item 58 — `crewhaus fleet`: the cross-harness view every other single-cwd
|
|
3
|
+
* command lacks. A team runs many harnesses (the Slack bot, the CLI
|
|
4
|
+
* concierge, the nightly optimizer) each in its own directory per the
|
|
5
|
+
* standalone-harness convention; `fleet` discovers them under a root and
|
|
6
|
+
* rolls their state up into one inventory / health table, and runs a safe
|
|
7
|
+
* read-only subcommand across the filtered set.
|
|
8
|
+
*
|
|
9
|
+
* Factored out of the entry file `index.ts` (which runs a top-level argv
|
|
10
|
+
* switch and so cannot be imported by a test without executing the CLI).
|
|
11
|
+
* Side-effect-free on import and directly unit-testable, mirroring
|
|
12
|
+
* `retention.ts` / `state-backup.ts` / `datasets.ts`:
|
|
13
|
+
* - discovery + inventory aggregation are pure reads over seeded harness
|
|
14
|
+
* dirs (no network, no subprocess);
|
|
15
|
+
* - the bulk-run path launches per-harness `crewhaus` invocations through an
|
|
16
|
+
* INJECTED runner seam (`FleetRunner`), so tests drive it without a real
|
|
17
|
+
* CLI.
|
|
18
|
+
*
|
|
19
|
+
* DISCOVERY MODEL — a "harness" is any directory that carries a
|
|
20
|
+
* `crewhaus.yaml` (the spec file the standalone-harness convention roots a
|
|
21
|
+
* harness on). `discoverHarnesses(root)` walks `root` up to a bounded depth,
|
|
22
|
+
* records every dir with a `crewhaus.yaml`, and does NOT descend into a
|
|
23
|
+
* harness's own `.crewhaus/` state dir or into `node_modules` (a bundled
|
|
24
|
+
* template spec under `node_modules` is not a fleet member). The root itself
|
|
25
|
+
* counts when it carries a spec, so `fleet list` run from inside a single
|
|
26
|
+
* harness reports just that one.
|
|
27
|
+
*
|
|
28
|
+
* INVENTORY — per harness, aggregated purely from on-disk state the other
|
|
29
|
+
* commands already write:
|
|
30
|
+
* - name / target / model: a lenient scan of `crewhaus.yaml` (a full
|
|
31
|
+
* `parseSpec` can fail on schema drift, and a fleet scan must not die on
|
|
32
|
+
* one stale spec — so name/target/model are read best-effort);
|
|
33
|
+
* - registry version + env pins: `@crewhaus/spec-registry` manifest for the
|
|
34
|
+
* spec name under `.crewhaus/specs`;
|
|
35
|
+
* - last eval pass-rate: newest entry in `.crewhaus/evals/index.jsonl`
|
|
36
|
+
* (`@crewhaus/eval-report`'s `readRunIndex`);
|
|
37
|
+
* - session count: `sess_*.json` files under `.crewhaus/sessions`;
|
|
38
|
+
* - feedback count: distinct records via `extractFeedbackRecords` over the
|
|
39
|
+
* same sinks `crewhaus distill` reads (sessions + feedback jsonl).
|
|
40
|
+
*
|
|
41
|
+
* BULK OPS SAFETY — `fleet run <sub>` only accepts commands off an explicit
|
|
42
|
+
* READ-ONLY allow-list (`eval`, `doctor`, `security digest`, `audit verify`).
|
|
43
|
+
* A mutating command is refused unless `--allow-mutating` is passed AND a
|
|
44
|
+
* per-harness confirm callback approves each one — the CLI wires that to an
|
|
45
|
+
* interactive prompt, tests to a predicate. The bulk runner never itself
|
|
46
|
+
* mutates: it launches one `crewhaus <sub>` invocation per harness (cwd = the
|
|
47
|
+
* harness dir, the convention every subcommand resolves state from) and
|
|
48
|
+
* aggregates exit codes.
|
|
49
|
+
*/
|
|
50
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
51
|
+
import { join, resolve } from "node:path";
|
|
52
|
+
import { extractFeedbackRecords, mergeFeedback } from "./feedback";
|
|
53
|
+
/** The spec file that roots a standalone harness. */
|
|
54
|
+
export const HARNESS_SPEC_FILENAME = "crewhaus.yaml";
|
|
55
|
+
/** Depth cap for the discovery walk (root is depth 0). Deep monorepos still
|
|
56
|
+
* resolve; a pathological tree can't hang the scan. */
|
|
57
|
+
const MAX_DISCOVERY_DEPTH = 6;
|
|
58
|
+
/** Directory names never descended into during discovery. */
|
|
59
|
+
const DISCOVERY_SKIP_DIRS = new Set([
|
|
60
|
+
".crewhaus",
|
|
61
|
+
"node_modules",
|
|
62
|
+
".git",
|
|
63
|
+
"dist",
|
|
64
|
+
".worktrees",
|
|
65
|
+
]);
|
|
66
|
+
/** Thrown for operational failures (missing root, empty filter, disallowed
|
|
67
|
+
* bulk subcommand). The CLI entry file catches it and routes the message
|
|
68
|
+
* through `die()`; tests assert on `.message` without the process exiting. */
|
|
69
|
+
export class FleetError extends Error {
|
|
70
|
+
name = "FleetError";
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Walk `root` for directories carrying a `crewhaus.yaml`, bounded to
|
|
74
|
+
* {@link MAX_DISCOVERY_DEPTH}. Never descends into `.crewhaus`, `node_modules`,
|
|
75
|
+
* `.git`, `dist`, or `.worktrees`. A directory that IS a harness is still
|
|
76
|
+
* descended into (a harness may nest sub-harnesses in a demo tree), just not
|
|
77
|
+
* through its state dir. Results are sorted by directory for stable output.
|
|
78
|
+
*/
|
|
79
|
+
export function discoverHarnesses(root) {
|
|
80
|
+
const absRoot = resolve(root);
|
|
81
|
+
if (!existsSync(absRoot) || !statSync(absRoot).isDirectory()) {
|
|
82
|
+
throw new FleetError(`no directory at ${absRoot} — nothing to scan for harnesses`);
|
|
83
|
+
}
|
|
84
|
+
const found = [];
|
|
85
|
+
const visit = (dir, depth) => {
|
|
86
|
+
const specPath = join(dir, HARNESS_SPEC_FILENAME);
|
|
87
|
+
if (existsSync(specPath) && statSync(specPath).isFile()) {
|
|
88
|
+
found.push({ dir, specPath });
|
|
89
|
+
}
|
|
90
|
+
if (depth >= MAX_DISCOVERY_DEPTH)
|
|
91
|
+
return;
|
|
92
|
+
let entries;
|
|
93
|
+
try {
|
|
94
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return; // unreadable dir — skip, don't abort the whole scan
|
|
98
|
+
}
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
if (!entry.isDirectory())
|
|
101
|
+
continue;
|
|
102
|
+
if (DISCOVERY_SKIP_DIRS.has(entry.name))
|
|
103
|
+
continue;
|
|
104
|
+
visit(join(dir, entry.name), depth + 1);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
visit(absRoot, 0);
|
|
108
|
+
found.sort((a, b) => (a.dir < b.dir ? -1 : a.dir > b.dir ? 1 : 0));
|
|
109
|
+
return found;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Read `name`, `target`, and `model` from a `crewhaus.yaml` WITHOUT a full
|
|
113
|
+
* schema parse. A fleet scan must survive a stale/invalid spec (schema drift,
|
|
114
|
+
* a half-edited file), so this is a tolerant top-level-key scrape rather than
|
|
115
|
+
* `parseSpec`. `name`/`target` are top-level on every target shape; `model`
|
|
116
|
+
* lives at `agent.model` (cli/channel/voice) OR top-level `model`
|
|
117
|
+
* (workflow/graph/pipeline/crew) depending on the shape — the first
|
|
118
|
+
* `model:` at any indentation is taken. Values are unquoted and trimmed.
|
|
119
|
+
*/
|
|
120
|
+
export function readSpecHeader(yamlText) {
|
|
121
|
+
const topLevel = (key) => {
|
|
122
|
+
// `key:` at column 0 (top-level YAML mapping key).
|
|
123
|
+
const re = new RegExp(`^${key}:[ \\t]*(.*)$`, "m");
|
|
124
|
+
const m = re.exec(yamlText);
|
|
125
|
+
if (m === null)
|
|
126
|
+
return undefined;
|
|
127
|
+
return cleanScalar(m[1] ?? "");
|
|
128
|
+
};
|
|
129
|
+
const anyLevel = (key) => {
|
|
130
|
+
// `key:` at any indentation — for `model` under `agent:`.
|
|
131
|
+
const re = new RegExp(`^[ \\t]*${key}:[ \\t]*(.*)$`, "m");
|
|
132
|
+
const m = re.exec(yamlText);
|
|
133
|
+
if (m === null)
|
|
134
|
+
return undefined;
|
|
135
|
+
return cleanScalar(m[1] ?? "");
|
|
136
|
+
};
|
|
137
|
+
const name = topLevel("name");
|
|
138
|
+
const target = topLevel("target");
|
|
139
|
+
const model = topLevel("model") ?? anyLevel("model");
|
|
140
|
+
return {
|
|
141
|
+
...(name !== undefined ? { name } : {}),
|
|
142
|
+
...(target !== undefined ? { target } : {}),
|
|
143
|
+
...(model !== undefined ? { model } : {}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/** Strip surrounding quotes, an inline `# comment`, and whitespace from a
|
|
147
|
+
* scalar. Returns undefined for an empty/block-scalar-only value. */
|
|
148
|
+
function cleanScalar(raw) {
|
|
149
|
+
let s = raw.trim();
|
|
150
|
+
if (s === "" || s === "|" || s === ">" || s === "|-" || s === ">-")
|
|
151
|
+
return undefined;
|
|
152
|
+
const quote = s.startsWith('"') ? '"' : s.startsWith("'") ? "'" : undefined;
|
|
153
|
+
if (quote !== undefined) {
|
|
154
|
+
// Quoted value: take everything up to the closing quote (a `#` inside is
|
|
155
|
+
// data); anything after the close (e.g. ` # comment`) is dropped.
|
|
156
|
+
const close = s.indexOf(quote, 1);
|
|
157
|
+
if (close !== -1)
|
|
158
|
+
return s.slice(1, close);
|
|
159
|
+
// Unterminated quote — fall through to the unquoted handling.
|
|
160
|
+
}
|
|
161
|
+
const hash = s.indexOf(" #");
|
|
162
|
+
if (hash !== -1)
|
|
163
|
+
s = s.slice(0, hash).trim();
|
|
164
|
+
return s === "" ? undefined : s;
|
|
165
|
+
}
|
|
166
|
+
const SESSION_JSON_REGEX = /^sess_[0-9a-f]{16}\.json$/;
|
|
167
|
+
/** Count `sess_<16hex>.json` files under `.crewhaus/sessions`. */
|
|
168
|
+
export function countSessions(harnessDir) {
|
|
169
|
+
const dir = join(harnessDir, ".crewhaus", "sessions");
|
|
170
|
+
if (!existsSync(dir))
|
|
171
|
+
return 0;
|
|
172
|
+
let n = 0;
|
|
173
|
+
for (const f of readdirSync(dir)) {
|
|
174
|
+
if (SESSION_JSON_REGEX.test(f))
|
|
175
|
+
n += 1;
|
|
176
|
+
}
|
|
177
|
+
return n;
|
|
178
|
+
}
|
|
179
|
+
/** Parse a JSONL blob into objects, skipping blank/malformed lines. */
|
|
180
|
+
function parseJsonlObjects(text) {
|
|
181
|
+
const out = [];
|
|
182
|
+
for (const line of text.split("\n")) {
|
|
183
|
+
if (line.trim() === "")
|
|
184
|
+
continue;
|
|
185
|
+
try {
|
|
186
|
+
out.push(JSON.parse(line));
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// tolerated — one torn line must not hide the rest
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Distinct feedback a harness carries, counted the way `crewhaus distill`
|
|
196
|
+
* folds it: `user_feedback` events in `sessions/*.jsonl` PLUS bare records in
|
|
197
|
+
* `feedback/*.jsonl` (`extractFeedbackRecords` accepts both encodings), then
|
|
198
|
+
* `mergeFeedback` collapses to one per (sessionId, turnNumber). The row shows
|
|
199
|
+
* distinct rated turns, not raw line count, so a re-rated turn counts once.
|
|
200
|
+
*/
|
|
201
|
+
export function countFeedback(harnessDir) {
|
|
202
|
+
const objects = [];
|
|
203
|
+
for (const sub of ["sessions", "feedback"]) {
|
|
204
|
+
const dir = join(harnessDir, ".crewhaus", sub);
|
|
205
|
+
if (!existsSync(dir))
|
|
206
|
+
continue;
|
|
207
|
+
for (const f of readdirSync(dir)) {
|
|
208
|
+
if (!f.endsWith(".jsonl"))
|
|
209
|
+
continue;
|
|
210
|
+
objects.push(...parseJsonlObjects(readFileSync(join(dir, f), "utf-8")));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return mergeFeedback(extractFeedbackRecords(objects)).length;
|
|
214
|
+
}
|
|
215
|
+
/** The newest run in an eval index, or undefined. Newest by `ts` (ISO-8601
|
|
216
|
+
* lexical order is chronological). */
|
|
217
|
+
export function lastEvalFor(evalEntries) {
|
|
218
|
+
let best;
|
|
219
|
+
for (const e of evalEntries) {
|
|
220
|
+
if (best === undefined || e.ts > best.ts)
|
|
221
|
+
best = e;
|
|
222
|
+
}
|
|
223
|
+
if (best === undefined)
|
|
224
|
+
return undefined;
|
|
225
|
+
return { datasetName: best.datasetName, passRate: best.passRate, ts: best.ts };
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Aggregate one harness directory into an inventory row. All reads are
|
|
229
|
+
* tolerant: a missing state dir, an unregistered spec, or an unreadable spec
|
|
230
|
+
* file each degrade to an empty/absent field rather than throwing, so one
|
|
231
|
+
* broken harness never aborts a `fleet list`.
|
|
232
|
+
*/
|
|
233
|
+
export async function buildHarnessInventory(harness, deps) {
|
|
234
|
+
let header = {};
|
|
235
|
+
let specUnreadable = false;
|
|
236
|
+
try {
|
|
237
|
+
header = readSpecHeader(readFileSync(harness.specPath, "utf-8"));
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
specUnreadable = true;
|
|
241
|
+
}
|
|
242
|
+
const specName = header.name ?? basenameOf(harness.dir);
|
|
243
|
+
let registry = { pins: {} };
|
|
244
|
+
const manifest = await deps.readManifest(specName, join(harness.dir, ".crewhaus", "specs"));
|
|
245
|
+
if (manifest !== undefined) {
|
|
246
|
+
const latest = manifest.versions[manifest.versions.length - 1];
|
|
247
|
+
registry = {
|
|
248
|
+
...(latest !== undefined ? { latestVersion: latest } : {}),
|
|
249
|
+
pins: manifest.pins,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const lastEval = lastEvalFor(deps.readEvalIndex(join(harness.dir, ".crewhaus", "evals")));
|
|
253
|
+
return {
|
|
254
|
+
dir: harness.dir,
|
|
255
|
+
header,
|
|
256
|
+
specName,
|
|
257
|
+
registry,
|
|
258
|
+
...(lastEval !== undefined ? { lastEval } : {}),
|
|
259
|
+
sessionCount: countSessions(harness.dir),
|
|
260
|
+
feedbackCount: countFeedback(harness.dir),
|
|
261
|
+
specUnreadable,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function basenameOf(dir) {
|
|
265
|
+
const parts = dir.split(/[\\/]/).filter((p) => p !== "");
|
|
266
|
+
return parts[parts.length - 1] ?? dir;
|
|
267
|
+
}
|
|
268
|
+
/** Build the full fleet inventory for a root. */
|
|
269
|
+
export async function buildFleetInventory(root, deps) {
|
|
270
|
+
const harnesses = discoverHarnesses(root);
|
|
271
|
+
const rows = [];
|
|
272
|
+
for (const h of harnesses) {
|
|
273
|
+
rows.push(await buildHarnessInventory(h, deps));
|
|
274
|
+
}
|
|
275
|
+
return rows;
|
|
276
|
+
}
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
// Rendering
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
/** Render the inventory as aligned columns for `fleet list`. */
|
|
281
|
+
export function formatInventory(rows, root) {
|
|
282
|
+
if (rows.length === 0) {
|
|
283
|
+
return [`no harnesses (no ${HARNESS_SPEC_FILENAME}) found under ${resolve(root)}`];
|
|
284
|
+
}
|
|
285
|
+
const lines = [`${rows.length} harness(es) under ${resolve(root)}:`, ""];
|
|
286
|
+
for (const r of rows) {
|
|
287
|
+
const rel = relTo(root, r.dir);
|
|
288
|
+
lines.push(`• ${r.specName}${rel !== "" ? ` (${rel})` : ""}`);
|
|
289
|
+
const shape = r.header.target ?? "?";
|
|
290
|
+
const model = r.header.model ?? "?";
|
|
291
|
+
lines.push(` shape=${shape} model=${model}`);
|
|
292
|
+
const ver = r.registry.latestVersion ?? "unregistered";
|
|
293
|
+
const pins = Object.entries(r.registry.pins);
|
|
294
|
+
const pinStr = pins.length === 0 ? "no pins" : pins.map(([e, v]) => `${e}→${v}`).join(", ");
|
|
295
|
+
lines.push(` registry=${ver} pins: ${pinStr}`);
|
|
296
|
+
if (r.lastEval !== undefined) {
|
|
297
|
+
lines.push(` last eval: ${(r.lastEval.passRate * 100).toFixed(1)}% pass (${r.lastEval.datasetName}, ${r.lastEval.ts})`);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
lines.push(" last eval: none recorded");
|
|
301
|
+
}
|
|
302
|
+
lines.push(` sessions=${r.sessionCount} feedback=${r.feedbackCount}`);
|
|
303
|
+
if (r.specUnreadable)
|
|
304
|
+
lines.push(" ⚠ spec file unreadable");
|
|
305
|
+
}
|
|
306
|
+
return lines;
|
|
307
|
+
}
|
|
308
|
+
function relTo(root, dir) {
|
|
309
|
+
const absRoot = resolve(root);
|
|
310
|
+
const absDir = resolve(dir);
|
|
311
|
+
if (absDir === absRoot)
|
|
312
|
+
return ".";
|
|
313
|
+
if (absDir.startsWith(`${absRoot}/`))
|
|
314
|
+
return absDir.slice(absRoot.length + 1);
|
|
315
|
+
return absDir;
|
|
316
|
+
}
|
|
317
|
+
/** Count open incidents under `.crewhaus/incidents` (files not ending
|
|
318
|
+
* `.resolved.json`). Absent dir → 0. The ops batch writes one JSON per
|
|
319
|
+
* incident; a resolved one is renamed `<id>.resolved.json`. */
|
|
320
|
+
export function countOpenIncidents(harnessDir) {
|
|
321
|
+
const dir = join(harnessDir, ".crewhaus", "incidents");
|
|
322
|
+
if (!existsSync(dir))
|
|
323
|
+
return 0;
|
|
324
|
+
let n = 0;
|
|
325
|
+
for (const f of readdirSync(dir)) {
|
|
326
|
+
if (f.endsWith(".json") && !f.endsWith(".resolved.json"))
|
|
327
|
+
n += 1;
|
|
328
|
+
}
|
|
329
|
+
return n;
|
|
330
|
+
}
|
|
331
|
+
export async function buildHarnessHealth(inv, readEvalHealth) {
|
|
332
|
+
const evalHealth = readEvalHealth(join(inv.dir, ".crewhaus", "evals"), inv.specName);
|
|
333
|
+
return {
|
|
334
|
+
dir: inv.dir,
|
|
335
|
+
specName: inv.specName,
|
|
336
|
+
registered: inv.registry.latestVersion !== undefined,
|
|
337
|
+
pinnedEnvs: Object.keys(inv.registry.pins).sort(),
|
|
338
|
+
evalHealthy: evalHealth.healthy,
|
|
339
|
+
evalNote: evalHealth.note,
|
|
340
|
+
openIncidents: countOpenIncidents(inv.dir),
|
|
341
|
+
hasAudit: existsSync(join(inv.dir, ".crewhaus", "audit")),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
/** Render the health rollup as status lines for `fleet status`. */
|
|
345
|
+
export function formatHealth(rows, root) {
|
|
346
|
+
if (rows.length === 0) {
|
|
347
|
+
return [`no harnesses found under ${resolve(root)}`];
|
|
348
|
+
}
|
|
349
|
+
const lines = [`fleet health — ${rows.length} harness(es) under ${resolve(root)}:`, ""];
|
|
350
|
+
for (const r of rows) {
|
|
351
|
+
const flags = [];
|
|
352
|
+
flags.push(r.registered ? "registered" : "unregistered");
|
|
353
|
+
flags.push(r.evalHealthy ? "eval:ok" : "eval:attention");
|
|
354
|
+
if (r.openIncidents > 0)
|
|
355
|
+
flags.push(`incidents:${r.openIncidents}`);
|
|
356
|
+
if (r.hasAudit)
|
|
357
|
+
flags.push("audit");
|
|
358
|
+
const mark = healthMark(r);
|
|
359
|
+
lines.push(`${mark} ${r.specName}`);
|
|
360
|
+
lines.push(` ${flags.join(" ")}`);
|
|
361
|
+
lines.push(` eval: ${r.evalNote}`);
|
|
362
|
+
if (r.pinnedEnvs.length > 0)
|
|
363
|
+
lines.push(` pinned envs: ${r.pinnedEnvs.join(", ")}`);
|
|
364
|
+
}
|
|
365
|
+
const attention = rows.filter((r) => healthMark(r) !== "✓").length;
|
|
366
|
+
lines.push("");
|
|
367
|
+
lines.push(attention === 0
|
|
368
|
+
? "summary: all harnesses healthy"
|
|
369
|
+
: `summary: ${attention} harness(es) need attention`);
|
|
370
|
+
return lines;
|
|
371
|
+
}
|
|
372
|
+
/** ✓ healthy / ✗ needs attention. Attention = an open incident, an eval
|
|
373
|
+
* regression, or an unregistered harness that nonetheless has env pins
|
|
374
|
+
* (a pin pointing at nothing registered). */
|
|
375
|
+
export function healthMark(r) {
|
|
376
|
+
if (r.openIncidents > 0)
|
|
377
|
+
return "✗";
|
|
378
|
+
if (!r.evalHealthy)
|
|
379
|
+
return "✗";
|
|
380
|
+
if (!r.registered && r.pinnedEnvs.length > 0)
|
|
381
|
+
return "✗";
|
|
382
|
+
return "✓";
|
|
383
|
+
}
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
// Bulk run (fleet run <sub> --filter <glob>)
|
|
386
|
+
// ---------------------------------------------------------------------------
|
|
387
|
+
/** Read-only / idempotent subcommands the bulk runner accepts without an
|
|
388
|
+
* explicit opt-in. Each maps to a `crewhaus <argv...>` invocation. */
|
|
389
|
+
export const READ_ONLY_BULK_COMMANDS = {
|
|
390
|
+
eval: ["eval"],
|
|
391
|
+
doctor: ["doctor"],
|
|
392
|
+
"security digest": ["security", "digest"],
|
|
393
|
+
"audit verify": ["audit", "verify"],
|
|
394
|
+
};
|
|
395
|
+
/**
|
|
396
|
+
* Resolve a `fleet run` subcommand + trailing args into a plan. Refuses
|
|
397
|
+
* anything off the read-only allow-list unless `allowMutating` is set. The
|
|
398
|
+
* longest allow-listed prefix wins, so `security digest` resolves ahead of a
|
|
399
|
+
* bare `security`.
|
|
400
|
+
*/
|
|
401
|
+
export function resolveBulkCommand(subcommandTokens, allowMutating) {
|
|
402
|
+
if (subcommandTokens.length === 0) {
|
|
403
|
+
throw new FleetError("missing subcommand — usage: crewhaus fleet run <eval|doctor|security digest|audit verify> [--filter <glob>]");
|
|
404
|
+
}
|
|
405
|
+
const joined2 = subcommandTokens.slice(0, 2).join(" ");
|
|
406
|
+
const joined1 = subcommandTokens[0];
|
|
407
|
+
const two = READ_ONLY_BULK_COMMANDS[joined2];
|
|
408
|
+
if (two !== undefined) {
|
|
409
|
+
return { argv: [...two, ...subcommandTokens.slice(2)], mutating: false };
|
|
410
|
+
}
|
|
411
|
+
const one = READ_ONLY_BULK_COMMANDS[joined1];
|
|
412
|
+
if (one !== undefined) {
|
|
413
|
+
return { argv: [...one, ...subcommandTokens.slice(1)], mutating: false };
|
|
414
|
+
}
|
|
415
|
+
if (!allowMutating) {
|
|
416
|
+
const allowed = Object.keys(READ_ONLY_BULK_COMMANDS).join(", ");
|
|
417
|
+
throw new FleetError(`"${joined1}" is not a read-only fleet subcommand (allowed: ${allowed}). Pass --allow-mutating to run a mutating command across the fleet (each harness will require confirmation).`);
|
|
418
|
+
}
|
|
419
|
+
return { argv: [...subcommandTokens], mutating: true };
|
|
420
|
+
}
|
|
421
|
+
/** Match a harness against a `--filter` glob, tested against BOTH the spec
|
|
422
|
+
* name and the directory basename, so `--filter '*bot*'` catches either. */
|
|
423
|
+
export function matchesFilter(inv, filter) {
|
|
424
|
+
if (filter === undefined)
|
|
425
|
+
return true;
|
|
426
|
+
const glob = new Bun.Glob(filter);
|
|
427
|
+
return glob.match(inv.specName) || glob.match(basenameOf(inv.dir));
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Run one subcommand across the filtered fleet. Read-only commands run
|
|
431
|
+
* unconditionally on every matched harness; a mutating command (only reachable
|
|
432
|
+
* with `allowMutating`) runs a harness only when `confirm` approves it. The
|
|
433
|
+
* runner is the sole side-effect surface, injected for testability.
|
|
434
|
+
*/
|
|
435
|
+
export async function runFleetBulk(opts) {
|
|
436
|
+
const plan = resolveBulkCommand(opts.subcommandTokens, opts.allowMutating);
|
|
437
|
+
const inventory = await buildFleetInventory(opts.root, opts.deps);
|
|
438
|
+
const results = [];
|
|
439
|
+
let failed = 0;
|
|
440
|
+
let passed = 0;
|
|
441
|
+
let skipped = 0;
|
|
442
|
+
for (const inv of inventory) {
|
|
443
|
+
if (!matchesFilter(inv, opts.filter)) {
|
|
444
|
+
results.push({ inv, ran: false, skipReason: "filtered out" });
|
|
445
|
+
skipped += 1;
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (plan.mutating) {
|
|
449
|
+
const ok = await opts.confirm(inv, plan.argv);
|
|
450
|
+
if (!ok) {
|
|
451
|
+
results.push({ inv, ran: false, skipReason: "declined at confirm" });
|
|
452
|
+
skipped += 1;
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const { exitCode, tail } = await opts.runner({ cwd: inv.dir, argv: plan.argv });
|
|
457
|
+
results.push({ inv, ran: true, exitCode, tail });
|
|
458
|
+
if (exitCode === 0)
|
|
459
|
+
passed += 1;
|
|
460
|
+
else
|
|
461
|
+
failed += 1;
|
|
462
|
+
}
|
|
463
|
+
return { plan, results, failed, passed, skipped };
|
|
464
|
+
}
|
|
465
|
+
/** Render a bulk-run report as summary lines for `fleet run`. */
|
|
466
|
+
export function formatBulkReport(report) {
|
|
467
|
+
const cmd = report.plan.argv.join(" ");
|
|
468
|
+
const lines = [
|
|
469
|
+
`fleet run: crewhaus ${cmd}${report.plan.mutating ? " (mutating — confirmed per harness)" : ""}`,
|
|
470
|
+
"",
|
|
471
|
+
];
|
|
472
|
+
for (const r of report.results) {
|
|
473
|
+
if (!r.ran) {
|
|
474
|
+
lines.push(`~ ${r.inv.specName} — skipped (${r.skipReason})`);
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
const mark = r.exitCode === 0 ? "✓" : "✗";
|
|
478
|
+
lines.push(`${mark} ${r.inv.specName} — exit ${r.exitCode}`);
|
|
479
|
+
const tail = (r.tail ?? "").trim();
|
|
480
|
+
if (tail !== "") {
|
|
481
|
+
for (const line of tail.split("\n").slice(-3))
|
|
482
|
+
lines.push(` ${line}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
lines.push("");
|
|
486
|
+
lines.push(`summary: ${report.passed} passed, ${report.failed} failed, ${report.skipped} skipped`);
|
|
487
|
+
return lines;
|
|
488
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { EvalRunSummary } from "@crewhaus/eval-runner";
|
|
2
|
+
/** Thrown on invalid knobs/defaults and scaffold refusals. The CLI entry
|
|
3
|
+
* file catches it and routes the message through `die()`; tests assert on
|
|
4
|
+
* `.message` without the process exiting (mirrors DatasetRefError). */
|
|
5
|
+
export declare class FlywheelConfigError extends Error {
|
|
6
|
+
readonly name = "FlywheelConfigError";
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The demo's env knob names, kept verbatim for continuity — a repo that ran
|
|
10
|
+
* the EP01 `nightly-flywheel.sh` can point its existing workflow env at
|
|
11
|
+
* `crewhaus flywheel run` without renaming a secret or a variable.
|
|
12
|
+
*/
|
|
13
|
+
export declare const FLYWHEEL_ENV_KNOBS: {
|
|
14
|
+
readonly budgetUsd: "FLYWHEEL_BUDGET_USD";
|
|
15
|
+
readonly iterations: "FLYWHEEL_ITERATIONS";
|
|
16
|
+
readonly seed: "FLYWHEEL_SEED";
|
|
17
|
+
readonly concurrency: "FLYWHEEL_CONCURRENCY";
|
|
18
|
+
};
|
|
19
|
+
export type FlywheelKnobs = {
|
|
20
|
+
/** Hard USD ceiling for the optimizer's model-driven mutation calls. */
|
|
21
|
+
readonly budgetUsd: number;
|
|
22
|
+
readonly iterations: number;
|
|
23
|
+
readonly seed: number;
|
|
24
|
+
/** Eval fan-out. Default 1 keeps the loop under a low provider TPM tier
|
|
25
|
+
* (the 30k-TPM default tier) — unattended jobs favour reliability. */
|
|
26
|
+
readonly concurrency: number;
|
|
27
|
+
};
|
|
28
|
+
/** Demo defaults: $2 cap, 3 iterations, seed 1 (reproducible PRs),
|
|
29
|
+
* concurrency 1 (rate-limit-tier safe). */
|
|
30
|
+
export declare const FLYWHEEL_DEFAULT_KNOBS: FlywheelKnobs;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the run knobs with flag > FLYWHEEL_* env > default precedence.
|
|
33
|
+
* The env layer exists so the scaffolded workflow (and the EP01 demo's)
|
|
34
|
+
* can steer a run purely through repository variables/dispatch inputs.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveFlywheelKnobs(opts: {
|
|
37
|
+
readonly flags: Readonly<Record<string, string | boolean>>;
|
|
38
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
39
|
+
}): FlywheelKnobs;
|
|
40
|
+
export type FlywheelDataResolution = {
|
|
41
|
+
/** `--dataset` value to use (file path or `registry:` ref). */
|
|
42
|
+
readonly dataset: string;
|
|
43
|
+
/** `--graders` file path to use. */
|
|
44
|
+
readonly graders: string;
|
|
45
|
+
readonly datasetSource: "flag" | "convention" | "ratings-registry";
|
|
46
|
+
readonly gradersSource: "flag" | "convention";
|
|
47
|
+
};
|
|
48
|
+
/** The standalone-harness convention paths the flywheel defaults to. */
|
|
49
|
+
export declare const CONVENTIONAL_DATASET: string;
|
|
50
|
+
export declare const CONVENTIONAL_GRADERS: string;
|
|
51
|
+
/**
|
|
52
|
+
* Resolve where the flywheel's dataset + graders come from when the flags
|
|
53
|
+
* are omitted: the standalone-harness convention (`eval/dataset.jsonl` /
|
|
54
|
+
* `eval/graders.yaml` beside the SPEC — resolved from the spec's directory,
|
|
55
|
+
* matching the dirty-check's spec-dir behavior, so `flywheel run
|
|
56
|
+
* path/to/crewhaus.yaml` from a sibling dir still finds the harness's own
|
|
57
|
+
* files; flag paths stay cwd-relative), and — when the spec declares a
|
|
58
|
+
* `feedback:` block and ratings have been distilled into the registry —
|
|
59
|
+
* the `registry:<spec>-ratings` dataset the autoDistill flywheel feeds.
|
|
60
|
+
*/
|
|
61
|
+
export declare function resolveFlywheelData(opts: {
|
|
62
|
+
readonly datasetFlag?: string;
|
|
63
|
+
readonly gradersFlag?: string;
|
|
64
|
+
readonly specName: string;
|
|
65
|
+
/** Directory the spec lives in — the base for the conventional paths. */
|
|
66
|
+
readonly specDir: string;
|
|
67
|
+
readonly hasConventionalDataset: boolean;
|
|
68
|
+
readonly hasConventionalGraders: boolean;
|
|
69
|
+
/** `registry:<specName>-ratings` has at least one version. */
|
|
70
|
+
readonly ratingsRegistered: boolean;
|
|
71
|
+
}): FlywheelDataResolution;
|
|
72
|
+
/** True when `git status --porcelain -- <spec>` reported any change. */
|
|
73
|
+
export declare function specIsDirty(porcelain: string): boolean;
|
|
74
|
+
export type FlywheelVerdict = {
|
|
75
|
+
readonly accepted: boolean;
|
|
76
|
+
readonly reason: string;
|
|
77
|
+
readonly passRateBefore: number;
|
|
78
|
+
readonly passRateAfter: number;
|
|
79
|
+
/** Sample-level pass→fail flips between the before and after runs. */
|
|
80
|
+
readonly regressions: number;
|
|
81
|
+
/** Sample-level fail→pass flips (what the accepted patch bought). */
|
|
82
|
+
readonly recoveries: number;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* The flywheel's acceptance bar: BOTH conditions must hold —
|
|
86
|
+
* (a) the strict run-history gate passes (`gateRuns`: any pass-rate drop
|
|
87
|
+
* or sample-level pass→fail flip fails, even when recoveries cancel
|
|
88
|
+
* it out in the aggregates), and
|
|
89
|
+
* (b) pass_rate STRICTLY improved (a flat run passes the gate but is not
|
|
90
|
+
* worth a write-back/PR — taste over averages, per the demo).
|
|
91
|
+
*/
|
|
92
|
+
export declare function evaluateFlywheelAcceptance(before: EvalRunSummary, after: EvalRunSummary): FlywheelVerdict;
|
|
93
|
+
export declare function formatPassRate(rate: number): string;
|
|
94
|
+
/** What the optimize step reports back (the orchestrator's result, narrowed
|
|
95
|
+
* to what the flywheel needs). `applied` mirrors OptimizeSpecResult.applied:
|
|
96
|
+
* improvement ≥ threshold — NOT the flywheel acceptance verdict. */
|
|
97
|
+
export type FlywheelOptimizeOutcome = {
|
|
98
|
+
readonly applied: boolean;
|
|
99
|
+
/** The patched YAML, NOT yet written anywhere (accept-then-write). */
|
|
100
|
+
readonly patchedYaml: string;
|
|
101
|
+
readonly runId: string;
|
|
102
|
+
/** The optimize artifacts dir (patch.json / report.json / evals/). */
|
|
103
|
+
readonly outDir: string;
|
|
104
|
+
readonly scoreBefore: number;
|
|
105
|
+
readonly scoreAfter: number;
|
|
106
|
+
readonly mutatorName: string;
|
|
107
|
+
readonly iterations: number;
|
|
108
|
+
/** Formatted `$0.0000` spend total from the budget meter. */
|
|
109
|
+
readonly spendUsd: string;
|
|
110
|
+
};
|
|
111
|
+
export type FlywheelHooks = {
|
|
112
|
+
/** Offline parse→lower gate. Throws to fail the phase. */
|
|
113
|
+
readonly compileCheck: (yaml: string, phase: "pre-optimize" | "post-optimize") => void;
|
|
114
|
+
/** One full eval pass of `yaml` over the flywheel's dataset. */
|
|
115
|
+
readonly evalRun: (label: "before" | "after", yaml: string) => Promise<EvalRunSummary>;
|
|
116
|
+
/** The optimize search, run WITHOUT write-back. */
|
|
117
|
+
readonly optimize: () => Promise<FlywheelOptimizeOutcome>;
|
|
118
|
+
/** Apply the accepted patch: write-back + auto-register + regression pin. */
|
|
119
|
+
readonly applyAccepted: (outcome: FlywheelOptimizeOutcome) => Promise<void>;
|
|
120
|
+
};
|
|
121
|
+
export type FlywheelOutcome = "accepted" | "accepted-dry-run" | "rejected" | "no-improvement" | "patch-compile-failed";
|
|
122
|
+
export type FlywheelRunResult = {
|
|
123
|
+
readonly outcome: FlywheelOutcome;
|
|
124
|
+
readonly reason: string;
|
|
125
|
+
readonly verdict?: FlywheelVerdict;
|
|
126
|
+
readonly before?: EvalRunSummary;
|
|
127
|
+
readonly after?: EvalRunSummary;
|
|
128
|
+
readonly optimize?: FlywheelOptimizeOutcome;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* The complete loop with accept-then-write semantics. The source spec is
|
|
132
|
+
* only ever touched by `hooks.applyAccepted`, which runs exactly when the
|
|
133
|
+
* acceptance gate passed AND `dryRun` is false — every other path leaves
|
|
134
|
+
* the working tree byte-identical, so there is nothing to revert. A
|
|
135
|
+
* pre-optimize compile failure propagates (nothing was spent yet); a
|
|
136
|
+
* post-optimize compile failure maps to `"patch-compile-failed"` (the
|
|
137
|
+
* optimizer produced YAML the compiler rejects — an error, but the source
|
|
138
|
+
* is untouched).
|
|
139
|
+
*/
|
|
140
|
+
export declare function runFlywheelLoop(opts: {
|
|
141
|
+
readonly sourceYaml: string;
|
|
142
|
+
readonly dryRun: boolean;
|
|
143
|
+
readonly hooks: FlywheelHooks;
|
|
144
|
+
}): Promise<FlywheelRunResult>;
|
|
145
|
+
/** One-screen flywheel report, printed after every run. */
|
|
146
|
+
export declare function formatFlywheelReport(result: FlywheelRunResult, extras: {
|
|
147
|
+
readonly specPath: string;
|
|
148
|
+
readonly datasetName: string;
|
|
149
|
+
readonly sampleCount: number;
|
|
150
|
+
readonly budgetUsd: number;
|
|
151
|
+
readonly artifactsDir: string;
|
|
152
|
+
}): string[];
|
|
153
|
+
export declare const FLYWHEEL_WORKFLOW_RELPATH: string;
|
|
154
|
+
export type ScaffoldResult = {
|
|
155
|
+
readonly path: string;
|
|
156
|
+
readonly action: "wrote";
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* Write a workflow scaffold, refusing to overwrite an existing file unless
|
|
160
|
+
* `force` (shared by `flywheel init` and `init --ci`). Throws
|
|
161
|
+
* FlywheelConfigError on refusal so the CLI maps it to a clean `die()`.
|
|
162
|
+
*/
|
|
163
|
+
export declare function scaffoldWorkflowFile(opts: {
|
|
164
|
+
readonly rootDir: string;
|
|
165
|
+
readonly relPath: string;
|
|
166
|
+
readonly content: string;
|
|
167
|
+
readonly force: boolean;
|
|
168
|
+
}): ScaffoldResult;
|
|
169
|
+
/**
|
|
170
|
+
* Normalize a repo-root-relative harness dir for embedding in a scaffolded
|
|
171
|
+
* workflow: POSIX separators, no leading `./`, no trailing slash; "" means
|
|
172
|
+
* the harness IS the repo root. Shared by both workflow builders.
|
|
173
|
+
*/
|
|
174
|
+
export declare function normalizeHarnessDir(dir: string | undefined): string;
|
|
175
|
+
/**
|
|
176
|
+
* The GitHub Actions schedule, adapted from the EP01 demo's
|
|
177
|
+
* `concierge-flywheel.yml`: nightly cron + manual dispatch, budget knobs as
|
|
178
|
+
* FLYWHEEL_* env, PR creation via `gh` for HUMAN review. It never merges on
|
|
179
|
+
* its own, and `crewhaus flywheel run` never writes an unaccepted patch, so
|
|
180
|
+
* every change that reaches the default branch passed both the acceptance
|
|
181
|
+
* gate and a human.
|
|
182
|
+
*
|
|
183
|
+
* `harnessDir` (repo-root-relative, "" = root) points the job's
|
|
184
|
+
* working-directory at a nested harness AND prefixes the artifact upload
|
|
185
|
+
* `path:` — actions paths resolve from the repo root and do NOT honor
|
|
186
|
+
* `defaults.run.working-directory` (finding 7).
|
|
187
|
+
*/
|
|
188
|
+
export declare function buildFlywheelWorkflowYaml(opts?: {
|
|
189
|
+
readonly harnessDir?: string;
|
|
190
|
+
}): string;
|
|
191
|
+
/** Printed after `flywheel init` (and folded into `flywheel --help`): the
|
|
192
|
+
* env knobs the scaffolded workflow steers the run with. */
|
|
193
|
+
export declare function formatFlywheelKnobsGuide(): string[];
|