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.
Files changed (138) hide show
  1. package/README.md +10 -3
  2. package/dist/advice-apply.d.ts +182 -0
  3. package/dist/advice-apply.js +286 -0
  4. package/dist/advise-rules.d.ts +348 -0
  5. package/dist/advise-rules.js +905 -0
  6. package/dist/alert-sink.d.ts +48 -0
  7. package/dist/alert-sink.js +86 -0
  8. package/dist/approval-gate.d.ts +127 -0
  9. package/dist/approval-gate.js +254 -0
  10. package/dist/audit-verify.d.ts +69 -0
  11. package/dist/audit-verify.js +97 -0
  12. package/dist/autodistill.d.ts +113 -0
  13. package/dist/autodistill.js +256 -0
  14. package/dist/channel-provision.d.ts +360 -0
  15. package/dist/channel-provision.js +881 -0
  16. package/dist/ci-scaffold.d.ts +31 -0
  17. package/dist/ci-scaffold.js +343 -0
  18. package/dist/compile-check.d.ts +90 -0
  19. package/dist/compile-check.js +285 -0
  20. package/dist/compliance-schedule.d.ts +35 -0
  21. package/dist/compliance-schedule.js +36 -0
  22. package/dist/context-pressure.d.ts +80 -0
  23. package/dist/context-pressure.js +166 -0
  24. package/dist/dataset-mine.d.ts +173 -0
  25. package/dist/dataset-mine.js +404 -0
  26. package/dist/datasets.d.ts +124 -0
  27. package/dist/datasets.js +260 -0
  28. package/dist/deploy-canary.d.ts +83 -0
  29. package/dist/deploy-canary.js +87 -0
  30. package/dist/doctor-checks.d.ts +33 -0
  31. package/dist/doctor-checks.js +92 -0
  32. package/dist/doctor-detect.d.ts +108 -0
  33. package/dist/doctor-detect.js +214 -0
  34. package/dist/doctor-fix.d.ts +81 -0
  35. package/dist/doctor-fix.js +164 -0
  36. package/dist/egress-triage.d.ts +121 -0
  37. package/dist/egress-triage.js +261 -0
  38. package/dist/eval-bridge.d.ts +114 -0
  39. package/dist/eval-bridge.js +158 -0
  40. package/dist/eval-coverage.d.ts +140 -0
  41. package/dist/eval-coverage.js +428 -0
  42. package/dist/eval-history.d.ts +48 -0
  43. package/dist/eval-history.js +157 -0
  44. package/dist/eval-matrix.d.ts +80 -0
  45. package/dist/eval-matrix.js +182 -0
  46. package/dist/eval-sentinel.d.ts +65 -0
  47. package/dist/eval-sentinel.js +132 -0
  48. package/dist/faq.d.ts +68 -0
  49. package/dist/faq.js +168 -0
  50. package/dist/feedback.d.ts +9 -2
  51. package/dist/feedback.js +17 -7
  52. package/dist/fewshot.d.ts +83 -0
  53. package/dist/fewshot.js +158 -0
  54. package/dist/fleet.d.ts +207 -0
  55. package/dist/fleet.js +488 -0
  56. package/dist/flywheel.d.ts +193 -0
  57. package/dist/flywheel.js +519 -0
  58. package/dist/graders-suggest.d.ts +186 -0
  59. package/dist/graders-suggest.js +658 -0
  60. package/dist/incident.d.ts +99 -0
  61. package/dist/incident.js +217 -0
  62. package/dist/index.d.ts +9 -1
  63. package/dist/index.js +11664 -963
  64. package/dist/init-interactive.d.ts +105 -0
  65. package/dist/init-interactive.js +208 -0
  66. package/dist/intents.d.ts +105 -0
  67. package/dist/intents.js +292 -0
  68. package/dist/judge-calibrate.d.ts +137 -0
  69. package/dist/judge-calibrate.js +247 -0
  70. package/dist/justification-calibrate.d.ts +150 -0
  71. package/dist/justification-calibrate.js +262 -0
  72. package/dist/justification-gate.d.ts +27 -6
  73. package/dist/justification-gate.js +30 -6
  74. package/dist/knowledge-sync.d.ts +179 -0
  75. package/dist/knowledge-sync.js +551 -0
  76. package/dist/lessons.d.ts +87 -0
  77. package/dist/lessons.js +207 -0
  78. package/dist/lint.d.ts +127 -0
  79. package/dist/lint.js +226 -0
  80. package/dist/loadtest.d.ts +114 -0
  81. package/dist/loadtest.js +196 -0
  82. package/dist/marketplace-cli.d.ts +110 -0
  83. package/dist/marketplace-cli.js +250 -0
  84. package/dist/mcp-doctor.d.ts +121 -0
  85. package/dist/mcp-doctor.js +249 -0
  86. package/dist/model-scan.d.ts +116 -0
  87. package/dist/model-scan.js +226 -0
  88. package/dist/onchain-tune.d.ts +164 -0
  89. package/dist/onchain-tune.js +346 -0
  90. package/dist/permissions-suggest.d.ts +126 -0
  91. package/dist/permissions-suggest.js +333 -0
  92. package/dist/pii-tune.d.ts +107 -0
  93. package/dist/pii-tune.js +122 -0
  94. package/dist/propose.d.ts +117 -0
  95. package/dist/propose.js +184 -0
  96. package/dist/refresh-goldens.d.ts +82 -0
  97. package/dist/refresh-goldens.js +221 -0
  98. package/dist/regression-pin.d.ts +160 -0
  99. package/dist/regression-pin.js +281 -0
  100. package/dist/retention.d.ts +193 -0
  101. package/dist/retention.js +607 -0
  102. package/dist/retire.d.ts +118 -0
  103. package/dist/retire.js +291 -0
  104. package/dist/right-size.d.ts +100 -0
  105. package/dist/right-size.js +123 -0
  106. package/dist/route.d.ts +19 -0
  107. package/dist/route.js +90 -0
  108. package/dist/scaffold-evals.d.ts +138 -0
  109. package/dist/scaffold-evals.js +410 -0
  110. package/dist/scope-audit-drift.d.ts +139 -0
  111. package/dist/scope-audit-drift.js +260 -0
  112. package/dist/security-corpus.d.ts +237 -0
  113. package/dist/security-corpus.js +516 -0
  114. package/dist/security-digest.d.ts +174 -0
  115. package/dist/security-digest.js +656 -0
  116. package/dist/sessions-index.d.ts +27 -0
  117. package/dist/sessions-index.js +51 -0
  118. package/dist/slo-doctor.d.ts +67 -0
  119. package/dist/slo-doctor.js +119 -0
  120. package/dist/slo-sink.d.ts +96 -0
  121. package/dist/slo-sink.js +107 -0
  122. package/dist/spec-changelog.d.ts +102 -0
  123. package/dist/spec-changelog.js +237 -0
  124. package/dist/state-backup.d.ts +223 -0
  125. package/dist/state-backup.js +648 -0
  126. package/dist/tools-cli.d.ts +170 -0
  127. package/dist/tools-cli.js +300 -0
  128. package/dist/triage.d.ts +202 -0
  129. package/dist/triage.js +403 -0
  130. package/dist/upgrade.d.ts +57 -0
  131. package/dist/upgrade.js +113 -0
  132. package/dist/version.d.ts +6 -0
  133. package/dist/version.js +27 -0
  134. package/dist/voice-eval.d.ts +138 -0
  135. package/dist/voice-eval.js +309 -0
  136. package/dist/watch.d.ts +58 -0
  137. package/dist/watch.js +97 -0
  138. package/package.json +90 -65
@@ -0,0 +1,31 @@
1
+ export declare const EVAL_CI_WORKFLOW_RELPATH: string;
2
+ export declare const SENTINEL_WORKFLOW_RELPATH: string;
3
+ /**
4
+ * Item 30 — the nightly model-drift sentinel workflow, scaffolded by
5
+ * `crewhaus init --sentinel`. Mirrors the shipped
6
+ * `@crewhaus/eval-report/templates/sentinel-drift.yml` template but adapts the
7
+ * paths to a nested harness (finding 7: GitHub only reads `.github/workflows`
8
+ * at the repo root, so a nested harness's spec/dataset paths are prefixed).
9
+ *
10
+ * It re-runs a seed-pinned sentinel dataset against the UNCHANGED spec and
11
+ * diffs against a FROZEN baseline run committed at `<harness>/eval/
12
+ * sentinel-baseline`. `crewhaus eval --sentinel` exits non-zero on provider
13
+ * drift (or a not-comparable spec/dataset-hash mismatch), failing this job so
14
+ * GitHub's scheduled-workflow-failure notification alerts.
15
+ */
16
+ export declare function buildSentinelDriftWorkflowYaml(opts?: {
17
+ readonly harnessDir?: string;
18
+ }): string;
19
+ /**
20
+ * The eval-gated-PR workflow. Model-budget-conscious by default:
21
+ * `--concurrency 1` (the 30k-TPM default provider tier) and a pinned seed
22
+ * for determinism.
23
+ *
24
+ * `harnessDir` (repo-root-relative, "" = root) adapts the scaffold to a
25
+ * nested harness: the job's working-directory, the `paths:` trigger filter,
26
+ * and the base-branch checkout `path:` are all repo-root-anchored, so every
27
+ * one of them needs the prefix (finding 7).
28
+ */
29
+ export declare function buildEvalCiWorkflowYaml(opts?: {
30
+ readonly harnessDir?: string;
31
+ }): string;
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Item 44 — `crewhaus init --ci`: the eval-on-PR CI gate as a scaffold.
3
+ * Emits `.github/workflows/crewhaus-eval.yml`, which turns the shipped
4
+ * `eval --gate` exit-code plumbing into a required PR check: every pull
5
+ * request touching the spec, dataset, or graders is compiled, evaled, and
6
+ * gated against the base branch — with the score-delta table posted as a
7
+ * PR comment.
8
+ *
9
+ * Baseline strategy (the design decision, also documented in the workflow
10
+ * comments): the job runs TWO fresh evals — the base branch's spec and the
11
+ * PR's spec, both on the PR's dataset+graders, same pinned seed — instead
12
+ * of diffing against a baseline persisted in the repo. Committing
13
+ * `.crewhaus/evals/baselines.json` plus the pinned run directory would
14
+ * save one eval per PR, but the stored transcripts go stale against every
15
+ * provider/model change, force contributors to re-commit run artifacts on
16
+ * every accepted change, and make the gate's "before" unreproducible. The
17
+ * two-run flow needs no persisted state and isolates exactly the PR's
18
+ * change (only the spec differs between the runs); the trade-off is one
19
+ * extra eval per PR and exposure to judge nondeterminism, which the pinned
20
+ * `--seed` and `--concurrency 1` bound as far as the graders allow.
21
+ *
22
+ * Mechanically the gate reuses the item-3 run-history machinery inside the
23
+ * job's FRESH workspace: the first `crewhaus eval` (base spec) pins the
24
+ * (spec, dataset) baseline; the second (PR spec) runs with `--gate`, which
25
+ * diffs against that pin and exits non-zero on any pass-rate drop or
26
+ * sample-level pass→fail flip. A PR that renames the spec or changes the
27
+ * dataset keyset starts a new lineage — the gate passes vacuously and the
28
+ * comment says so.
29
+ *
30
+ * Kept in a side-effect-free module (the CLI entry file runs an argv
31
+ * switch on import) mirroring `flywheel.ts`, which owns the shared
32
+ * `scaffoldWorkflowFile` writer.
33
+ */
34
+ import { join } from "node:path";
35
+ import { normalizeHarnessDir } from "./flywheel";
36
+ export const EVAL_CI_WORKFLOW_RELPATH = join(".github", "workflows", "crewhaus-eval.yml");
37
+ export const SENTINEL_WORKFLOW_RELPATH = join(".github", "workflows", "sentinel-drift.yml");
38
+ /**
39
+ * Item 30 — the nightly model-drift sentinel workflow, scaffolded by
40
+ * `crewhaus init --sentinel`. Mirrors the shipped
41
+ * `@crewhaus/eval-report/templates/sentinel-drift.yml` template but adapts the
42
+ * paths to a nested harness (finding 7: GitHub only reads `.github/workflows`
43
+ * at the repo root, so a nested harness's spec/dataset paths are prefixed).
44
+ *
45
+ * It re-runs a seed-pinned sentinel dataset against the UNCHANGED spec and
46
+ * diffs against a FROZEN baseline run committed at `<harness>/eval/
47
+ * sentinel-baseline`. `crewhaus eval --sentinel` exits non-zero on provider
48
+ * drift (or a not-comparable spec/dataset-hash mismatch), failing this job so
49
+ * GitHub's scheduled-workflow-failure notification alerts.
50
+ */
51
+ export function buildSentinelDriftWorkflowYaml(opts = {}) {
52
+ const sub = normalizeHarnessDir(opts.harnessDir);
53
+ const prefix = sub === "" ? "" : `${sub}/`;
54
+ const workingDirLine = sub === "" ? "" : `\n working-directory: ${sub}`;
55
+ const subdirNote = sub === ""
56
+ ? ""
57
+ : `# - Scaffolded for the harness at ${sub}/ — the job's working-directory is
58
+ # set so the spec/dataset/baseline paths resolve inside it.\n`;
59
+ return `# sentinel-drift.yml — scaffolded by \`crewhaus init --sentinel\` (item 30).
60
+ #
61
+ # Nightly model-drift sentinel: re-runs a SEED-PINNED sentinel dataset against
62
+ # your UNCHANGED spec and diffs the fresh run against a FROZEN baseline run
63
+ # committed to the repo. When the spec's specHash AND the sentinel dataset's
64
+ # content hash are BOTH identical to the baseline's, any pass/fail flip or
65
+ # score shift can ONLY be the provider silently changing model behaviour.
66
+ # \`crewhaus eval --sentinel\` exits non-zero on drift (and on a not-comparable
67
+ # spec/dataset-hash mismatch — a mis-pointed sentinel is loud, never silently
68
+ # green), so this job goes red and GitHub's scheduled-failure email alerts.
69
+ #
70
+ # WHY A COMMITTED BASELINE (unlike the eval-on-PR gate's two fresh runs): a
71
+ # drift probe needs a fixed "before" that survives across nights and is NOT
72
+ # re-derived from the current provider — that is the whole point.
73
+ #
74
+ # BEFORE THIS RUNS, freeze the baseline once against the current spec+dataset:
75
+ # crewhaus eval ${prefix}crewhaus.yaml \\
76
+ # --dataset ${prefix}eval/sentinel.jsonl --graders ${prefix}eval/graders.yaml \\
77
+ # --seed 1 -o ${prefix}eval/sentinel-baseline
78
+ # git add ${prefix}eval/sentinel-baseline && git commit
79
+ # Re-freeze it (same command) whenever you INTENTIONALLY change the spec or
80
+ # sentinel data, or knowingly adopt a new provider baseline.
81
+ ${subdirNote}#
82
+ # Required repo secret: ANTHROPIC_API_KEY (billed by the nightly eval run).
83
+
84
+ name: sentinel-drift
85
+
86
+ on:
87
+ schedule:
88
+ # Nightly at 03:17 UTC (off the hour to dodge cron scheduling backlog).
89
+ - cron: "17 3 * * *"
90
+ workflow_dispatch: {}
91
+
92
+ permissions:
93
+ contents: read
94
+
95
+ env:
96
+ SENTINEL_SEED: "1" # pin the sample order so only the PROVIDER can vary
97
+
98
+ jobs:
99
+ sentinel:
100
+ runs-on: ubuntu-latest
101
+ timeout-minutes: 20
102
+ defaults:
103
+ run:
104
+ shell: bash${workingDirLine}
105
+ steps:
106
+ - name: Checkout
107
+ uses: actions/checkout@v4
108
+
109
+ - name: Install Bun
110
+ uses: oven-sh/setup-bun@v2
111
+
112
+ - name: Install crewhaus CLI
113
+ run: |
114
+ bun add -g crewhaus
115
+ echo "$HOME/.bun/bin" >> "$GITHUB_PATH"
116
+
117
+ - name: Run the drift sentinel
118
+ env:
119
+ ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
120
+ run: |
121
+ crewhaus eval crewhaus.yaml \\
122
+ --dataset eval/sentinel.jsonl \\
123
+ --graders eval/graders.yaml \\
124
+ --seed "$SENTINEL_SEED" \\
125
+ --sentinel --baseline eval/sentinel-baseline \\
126
+ -o .crewhaus/evals/sentinel-\$(date -u +%Y%m%d)
127
+ `;
128
+ }
129
+ /**
130
+ * The eval-gated-PR workflow. Model-budget-conscious by default:
131
+ * `--concurrency 1` (the 30k-TPM default provider tier) and a pinned seed
132
+ * for determinism.
133
+ *
134
+ * `harnessDir` (repo-root-relative, "" = root) adapts the scaffold to a
135
+ * nested harness: the job's working-directory, the `paths:` trigger filter,
136
+ * and the base-branch checkout `path:` are all repo-root-anchored, so every
137
+ * one of them needs the prefix (finding 7).
138
+ */
139
+ export function buildEvalCiWorkflowYaml(opts = {}) {
140
+ const sub = normalizeHarnessDir(opts.harnessDir);
141
+ const prefix = sub === "" ? "" : `${sub}/`;
142
+ const workingDirLine = sub === "" ? "" : `\n working-directory: ${sub}`;
143
+ // Working-dir-relative path of the base branch's spec: the base checkout
144
+ // lands at <harness>/.crewhaus-ci/base (a FULL repo copy), so a nested
145
+ // harness's spec sits at .crewhaus-ci/base/<harness>/crewhaus.yaml.
146
+ const baseSpec = `.crewhaus-ci/base/${prefix}crewhaus.yaml`;
147
+ const subdirNote = sub === ""
148
+ ? `# - If your harness lives in a subdirectory, re-run \`crewhaus init --ci\`
149
+ # from the repo (the CLI detects the git root and prefixes everything),
150
+ # or hand-set jobs.eval-gate.defaults.run.working-directory AND prefix
151
+ # the paths: filter and the base-checkout path: — both resolve from the
152
+ # repo root, not the working directory.`
153
+ : `# - Scaffolded for the harness at ${sub}/ — the job's
154
+ # working-directory, the paths: filter, and the base-checkout path are
155
+ # already prefixed.`;
156
+ return `# crewhaus-eval.yml — scaffolded by \`crewhaus init --ci\`.
157
+ #
158
+ # Eval-gated spec PRs (the exact chain EP01 proves by hand): on every pull
159
+ # request touching the spec, dataset, or graders, this workflow
160
+ # 1. compiles the PR's spec (offline gate — a spec that doesn't compile
161
+ # never spends eval budget),
162
+ # 2. evals the BASE branch's spec on the PR's dataset+graders (this pins
163
+ # the run-history baseline in the job's fresh workspace),
164
+ # 3. evals the PR's spec with --gate, which diffs against that baseline
165
+ # and exits non-zero on any pass-rate drop or per-sample pass→fail
166
+ # flip (the strict gate from apps/cli/src/eval-history.ts),
167
+ # 4. posts the score-delta table as a PR comment, and
168
+ # 5. fails the check when the gate failed.
169
+ #
170
+ # BASELINE STRATEGY — why two fresh runs instead of a persisted baseline:
171
+ # committing .crewhaus/evals/baselines.json + the pinned run dir to the repo
172
+ # would save one eval per PR, but stored run artifacts go stale against
173
+ # every provider/model change, must be re-committed after every accepted
174
+ # PR, and make the "before" side unreproducible. Two fresh runs need no
175
+ # persisted state and isolate exactly the PR's change — only the SPEC
176
+ # differs between the runs (both use the PR's dataset+graders). Trade-off:
177
+ # one extra eval per PR + judge nondeterminism, bounded by the pinned
178
+ # EVAL_SEED and EVAL_CONCURRENCY=1 below.
179
+ #
180
+ # BRANCH-PROTECTION CAVEAT — read before marking this check REQUIRED:
181
+ # this workflow only runs on PRs that touch the paths: filter below. A PR
182
+ # that does NOT touch those paths never starts the workflow, so a REQUIRED
183
+ # crewhaus-eval check deadlocks it — stuck forever on "Expected — waiting
184
+ # for status". Leave the check optional, or enforce it only through
185
+ # paths-aware tooling (e.g. a merge queue that understands path filters).
186
+ # COST: every push to a matching PR re-runs the job and bills TWO evals
187
+ # (base spec + PR spec) against ANTHROPIC_API_KEY.
188
+ #
189
+ # Notes:
190
+ # - First PR introducing the spec: the base branch has no crewhaus.yaml,
191
+ # so there is no baseline — the base eval and the gate are SKIPPED, the
192
+ # comment says so, and the check goes green; future PRs will gate.
193
+ # - Fork PRs: the whole job is skipped (see jobs.eval-gate.if) — GitHub
194
+ # does not expose the ANTHROPIC_API_KEY secret to fork-triggered runs.
195
+ # - A PR that renames the spec or changes the dataset's sample keyset
196
+ # starts a NEW baseline lineage: the gate passes vacuously (nothing
197
+ # comparable to regress against) and the diff step says so.
198
+ # - EVAL_CONCURRENCY=1 keeps the job under a low provider rate-limit
199
+ # tier (the 30k-TPM default tier); raise it if your key allows.
200
+ ${subdirNote}
201
+ #
202
+ # Required repo secret: ANTHROPIC_API_KEY (billed by the two eval runs).
203
+
204
+ name: crewhaus-eval
205
+
206
+ on:
207
+ pull_request:
208
+ paths:
209
+ - "${prefix}crewhaus.yaml"
210
+ - "${prefix}eval/**"
211
+
212
+ permissions:
213
+ contents: read
214
+ pull-requests: write # the score-delta comment
215
+
216
+ # One eval gate per PR head; a force-push cancels the stale run.
217
+ concurrency:
218
+ group: crewhaus-eval-\${{ github.ref }}
219
+ cancel-in-progress: true
220
+
221
+ env:
222
+ EVAL_SEED: "1" # pinned for determinism across the base/PR runs
223
+ EVAL_CONCURRENCY: "1" # 30k-TPM-tier default; raise if your key allows
224
+
225
+ jobs:
226
+ eval-gate:
227
+ # Fork PRs cannot read secrets.ANTHROPIC_API_KEY (GitHub withholds
228
+ # secrets from fork-triggered runs), so the evals would fail red at
229
+ # model-client construction — skip the whole job instead (a skipped
230
+ # check is neutral, not a red X). Maintainers can gate a fork's change
231
+ # by pushing its commits to an in-repo branch (same-repo PRs get
232
+ # secrets), by re-running the eval after merge, or via a manual
233
+ # workflow_dispatch wrapper of their own.
234
+ if: github.event.pull_request.head.repo.full_name == github.repository
235
+ runs-on: ubuntu-latest
236
+ timeout-minutes: 30
237
+ defaults:
238
+ run:
239
+ shell: bash${workingDirLine}
240
+ steps:
241
+ - name: Checkout PR
242
+ uses: actions/checkout@v4
243
+
244
+ - name: Checkout base branch (the gate's "before" spec)
245
+ uses: actions/checkout@v4
246
+ with:
247
+ ref: \${{ github.base_ref }}
248
+ # checkout paths resolve from the repo root (working-directory
249
+ # only applies to run steps).
250
+ path: ${prefix}.crewhaus-ci/base
251
+
252
+ - name: Install Bun
253
+ uses: oven-sh/setup-bun@v2
254
+
255
+ - name: Install crewhaus CLI
256
+ run: |
257
+ bun add -g crewhaus
258
+ echo "$HOME/.bun/bin" >> "$GITHUB_PATH"
259
+
260
+ - name: Compile gate (offline)
261
+ run: crewhaus compile crewhaus.yaml --emit-ir > /dev/null
262
+
263
+ - name: Detect the baseline spec
264
+ id: baseline
265
+ run: |
266
+ # First PR introducing the spec: the base branch has nothing to
267
+ # gate against. Skip the base eval + the gate (green check) — the
268
+ # comment step reports it; future PRs will gate.
269
+ if [ -f "${baseSpec}" ]; then
270
+ echo "exists=true" >> "$GITHUB_OUTPUT"
271
+ else
272
+ echo "exists=false" >> "$GITHUB_OUTPUT"
273
+ echo "first PR introducing the spec — no baseline; future PRs will gate"
274
+ fi
275
+
276
+ - name: Eval base spec (pins the baseline)
277
+ if: steps.baseline.outputs.exists == 'true'
278
+ env:
279
+ ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
280
+ run: |
281
+ # Base SPEC, PR dataset+graders: only the spec differs between the
282
+ # two runs, so the gate isolates exactly the PR's spec change.
283
+ crewhaus eval ${baseSpec} \\
284
+ --dataset eval/dataset.jsonl \\
285
+ --graders eval/graders.yaml \\
286
+ --concurrency "$EVAL_CONCURRENCY" --seed "$EVAL_SEED" \\
287
+ -o .crewhaus-ci/runs/base
288
+
289
+ - name: Eval PR spec (gated against the base run)
290
+ id: gate
291
+ if: steps.baseline.outputs.exists == 'true'
292
+ # The comment must post either way; the last step re-fails the check.
293
+ continue-on-error: true
294
+ env:
295
+ ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
296
+ run: |
297
+ crewhaus eval crewhaus.yaml \\
298
+ --dataset eval/dataset.jsonl \\
299
+ --graders eval/graders.yaml \\
300
+ --concurrency "$EVAL_CONCURRENCY" --seed "$EVAL_SEED" \\
301
+ --gate \\
302
+ -o .crewhaus-ci/runs/pr
303
+
304
+ - name: Comment the score delta on the PR
305
+ if: always()
306
+ env:
307
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
308
+ run: |
309
+ read_row() { # $1 = run dir → "pass_rate | mean_score" (or n/a)
310
+ bun -e "const r=JSON.parse(require('fs').readFileSync('$1/results.json','utf8'));process.stdout.write((r.aggregates.passRate*100).toFixed(1)+'% | '+r.aggregates.meanScore.toFixed(3))" 2>/dev/null \\
311
+ || printf 'n/a | n/a'
312
+ }
313
+ BASE_ROW="$(read_row .crewhaus-ci/runs/base)"
314
+ PR_ROW="$(read_row .crewhaus-ci/runs/pr)"
315
+ DIFF_LINE="$(crewhaus eval-report diff .crewhaus-ci/runs/base .crewhaus-ci/runs/pr -o .crewhaus-ci/runs/diff 2>/dev/null | tail -n 1 \\
316
+ || echo 'diff unavailable — new baseline lineage (spec renamed / dataset keyset changed) or a run failed')"
317
+ # Only a FAILING GATE STEP is a regression. A gate that never ran
318
+ # (no baseline, or an earlier step blew up) must not be labelled
319
+ # one — its outcome is "skipped", not "failure".
320
+ BASELINE="\${{ steps.baseline.outputs.exists }}"
321
+ GATE_STATE="\${{ steps.gate.outcome }}"
322
+ if [ "$BASELINE" = "false" ]; then
323
+ VERDICT="NO BASELINE ✅ — first PR introducing the spec; future PRs will gate"
324
+ elif [ "$GATE_STATE" = "success" ]; then
325
+ VERDICT="PASS ✅"
326
+ elif [ "$GATE_STATE" = "failure" ]; then
327
+ VERDICT="FAIL ❌ (regression vs base)"
328
+ else
329
+ VERDICT="NOT RUN ⚠️ — an earlier step failed before the gate (infra, not a regression)"
330
+ fi
331
+ BODY="$(printf '## crewhaus eval gate: %s\\n\\n| run | pass_rate | mean_score |\\n| --- | --- | --- |\\n| base (%s) | %s |\\n| PR | %s |\\n\\n%s\\n\\n_seed %s · concurrency %s · gate: any pass-rate drop or per-sample pass→fail flip fails_\\n' \\
332
+ "$VERDICT" "\${{ github.base_ref }}" "$BASE_ROW" "$PR_ROW" "$DIFF_LINE" "$EVAL_SEED" "$EVAL_CONCURRENCY")"
333
+ gh api "repos/\${{ github.repository }}/issues/\${{ github.event.pull_request.number }}/comments" \\
334
+ -f body="$BODY" \\
335
+ || echo "comment skipped (read-only token — fork PR?)"
336
+
337
+ - name: Fail the check on gate failure
338
+ if: steps.gate.outcome == 'failure'
339
+ run: |
340
+ echo "eval gate failed — the PR spec regressed against \${{ github.base_ref }}"
341
+ exit 1
342
+ `;
343
+ }
@@ -0,0 +1,90 @@
1
+ import type { Bundle } from "@crewhaus/ir";
2
+ import { type ShapeAssertion } from "@crewhaus/smoke-harness";
3
+ export type CheckStepName = "assertion" | "install" | "boot";
4
+ export type CheckStepStatus = "ok" | "gated" | "skipped" | "failed";
5
+ export type CheckStep = {
6
+ readonly step: CheckStepName;
7
+ readonly status: CheckStepStatus;
8
+ readonly detail?: string;
9
+ };
10
+ export type CheckRunResult = {
11
+ readonly exitCode: number;
12
+ /** True when the liveness window elapsed and we killed the process (daemons). */
13
+ readonly timedOut: boolean;
14
+ readonly stdout: string;
15
+ readonly stderr: string;
16
+ };
17
+ export type CheckStepRunner = (opts: {
18
+ readonly argv: readonly string[];
19
+ readonly cwd: string;
20
+ readonly env?: Readonly<Record<string, string>>;
21
+ readonly timeoutMs?: number;
22
+ }) => Promise<CheckRunResult>;
23
+ /** Credential-free child env: only {@link SCRUB_PASSTHROUGH_VARS} survive. */
24
+ export declare function scrubbedEnv(baseEnv: NodeJS.ProcessEnv): Record<string, string>;
25
+ export declare function emptyEnvFile(): string;
26
+ /**
27
+ * The exact argv `compile --check` boots a bundle entrypoint with. Exported
28
+ * so the F4 regression tests exercise the REAL construction (flag presence
29
+ * with an injected runner, and .env invisibility with a live subprocess).
30
+ */
31
+ export declare function bootArgvFor(entry: string): readonly string[];
32
+ /** Collect the @crewhaus/* packages the emitted bundle imports (sorted, deduped). */
33
+ export declare function collectCrewhausDeps(files: Bundle["files"]): readonly string[];
34
+ /**
35
+ * Minimal manifest for bundles whose emitter ships no package.json.
36
+ * Versions pin to the CLI's OWN version: the @crewhaus/* packages publish in
37
+ * lockstep (scripts/publish-workspace.ts stamps them all with one version),
38
+ * so `<cliVersion>` is exactly the published contract this CLI's emitters
39
+ * were released against — where "latest" would silently verify against
40
+ * whatever shipped since, making the check's verdict depend on the day it
41
+ * runs. "latest" remains only as the fallback when no version is resolvable
42
+ * (a broken installation). (In a dev checkout the pinned publish can lag
43
+ * unreleased emitter features; the boot step then reports the mismatch,
44
+ * which is a real signal, not a false positive.)
45
+ */
46
+ export declare function buildBundlePackageJson(deps: readonly string[], version?: string): string;
47
+ /** daemon.ts (daemon shapes) beats agent.ts; undefined = nothing bootable here. */
48
+ export declare function resolveBootEntry(files: Bundle["files"]): string | undefined;
49
+ /**
50
+ * Known first-boot gates: the message a shape prints when it boots far
51
+ * enough to demand its live inputs. Reaching a gate is the liveness signal
52
+ * (`doctor --liveness` semantics — "booting far enough to parse argv IS the
53
+ * signal"). Derived empirically from booting every fixture bundle with a
54
+ * scrubbed env; keep patterns narrow so structural breakage stays red.
55
+ */
56
+ export declare const BOOT_GATE_PATTERNS: ReadonlyArray<{
57
+ readonly pattern: RegExp;
58
+ readonly gate: string;
59
+ }>;
60
+ export declare function classifyBootOutcome(result: CheckRunResult): {
61
+ status: "ok" | "gated" | "failed";
62
+ detail: string;
63
+ };
64
+ /** Fold the steps into the single green/red verdict line. Only "failed" is red. */
65
+ export declare function buildVerdict(steps: readonly CheckStep[]): {
66
+ green: boolean;
67
+ line: string;
68
+ };
69
+ export declare const defaultCheckRunner: CheckStepRunner;
70
+ export type CompileCheckOptions = {
71
+ readonly target: string;
72
+ readonly bundle: Bundle;
73
+ /** Absolute out-dir the bundle was just written to. */
74
+ readonly outDir: string;
75
+ /** Test seam — defaults to the shipped SHAPE_ASSERTIONS. */
76
+ readonly assertions?: readonly ShapeAssertion[];
77
+ /** Test seam — defaults to a real Bun.spawn runner. */
78
+ readonly runner?: CheckStepRunner;
79
+ readonly bootTimeoutMs?: number;
80
+ /** Upper bound for the `bun install` step (default 120s) — see F5a. */
81
+ readonly installTimeoutMs?: number;
82
+ /** Base env the scrubbed child env (PATH/HOME + proxy/CA vars) is drawn from. */
83
+ readonly env?: NodeJS.ProcessEnv;
84
+ };
85
+ export type CompileCheckResult = {
86
+ readonly green: boolean;
87
+ readonly line: string;
88
+ readonly steps: readonly CheckStep[];
89
+ };
90
+ export declare function runCompileCheck(opts: CompileCheckOptions): Promise<CompileCheckResult>;