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/README.md
CHANGED
|
@@ -58,9 +58,9 @@ Run `crewhaus` with no arguments for the full usage text, or any subcommand with
|
|
|
58
58
|
|---|---|
|
|
59
59
|
| `compile <spec.yaml> -o <out-dir>` | Compile a spec to a runnable bundle; `--emit-ir` prints the lowered IR as JSON instead |
|
|
60
60
|
| `run <spec.yaml>` | Compile in-memory and execute the agent (`--model`, `--resume <id>` / `--continue`, `--prompt <text>`, …) |
|
|
61
|
-
| `eval <spec.yaml> --dataset <data> --graders <graders.yaml>` | Run the agent against a dataset and grade (deterministic graders + LLM-as-judge) |
|
|
62
|
-
| `eval-report diff <prev> <new>` | Compare two eval runs and emit a diff report |
|
|
63
|
-
| `optimize <spec.yaml> --dataset <data> --graders <graders.yaml>` | Active eval-driven optimization; `--write-back` rewrites the spec in place |
|
|
61
|
+
| `eval <spec.yaml> --dataset <data> --graders <graders.yaml>` | Run the agent against a dataset and grade (deterministic graders + LLM-as-judge); every run is indexed and auto-diffed against the pinned baseline (`--gate` exits non-zero on regression, `--no-promote` keeps the pin); the per-spec `<specName>-regressions` suite is unioned in by default (`--no-regressions` skips) |
|
|
62
|
+
| `eval-report diff <prev> <new>` | Compare two eval runs and emit a diff report; `history` lists recorded runs, `baseline show\|set <runId>` inspects or pins baselines |
|
|
63
|
+
| `optimize <spec.yaml> --dataset <data> --graders <graders.yaml>` | Active eval-driven optimization; `--write-back` rewrites the spec in place; accepted patches pin their fail→pass recoveries into `<specName>-regressions` (`--no-pin-regressions` skips) |
|
|
64
64
|
| `init [name]` | Scaffold a new `crewhaus.yaml` |
|
|
65
65
|
| `doctor` | Check environment health |
|
|
66
66
|
| `context --bundle` | Emit a single-markdown orientation manifest |
|
|
@@ -76,6 +76,13 @@ Run `crewhaus` with no arguments for the full usage text, or any subcommand with
|
|
|
76
76
|
| `compliance evidence` | Collect SOC 2 / ISO 27001 / HIPAA evidence |
|
|
77
77
|
| `version` | Print the CLI version (also `--version`, `-v`) |
|
|
78
78
|
|
|
79
|
+
## Notes
|
|
80
|
+
|
|
81
|
+
Known limitations of the eval run-history features:
|
|
82
|
+
|
|
83
|
+
- **Baseline keys ignore spec identity beyond the name.** Run-index entries and baselines key on `(specName, datasetName)`; two different specs sharing a `name:` (or a spec whose instructions changed) share a lineage. The `specHash` is recorded in every index entry for forensics but does not partition baselines yet.
|
|
84
|
+
- **1-sample matrix cells can misclassify a crash.** `eval --models` flags a cell as "never really ran" when *all* of its samples errored; with a 1-sample dataset a single transient provider error is indistinguishable from a bad credential, so the cell is reported as crashed rather than as a result row.
|
|
85
|
+
|
|
79
86
|
## Docs
|
|
80
87
|
|
|
81
88
|
- [Documentation](https://crewhaus.ai/docs) — start with the [quickstart](https://crewhaus.ai/docs/quickstart) and the [CLI target guide](https://crewhaus.ai/docs/targets/cli)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Item 15 — `crewhaus optimize --from-advice <suggestions.json>`: the
|
|
3
|
+
* eval-gated apply path for the SpecPatches `crewhaus advise` emits
|
|
4
|
+
* (agent.max_tokens, compaction.curate, …). The optimize search only ever
|
|
5
|
+
* constructs `agent.instructions` patches; this module closes the other
|
|
6
|
+
* half of the telemetry loop by taking the advisor's pre-validated config
|
|
7
|
+
* patches and accepting each one only after a real eval proves it safe.
|
|
8
|
+
*
|
|
9
|
+
* The loop mirrors the flywheel's accept-then-write shape (baseline eval →
|
|
10
|
+
* per-candidate compile gate → candidate eval → acceptance gate), with two
|
|
11
|
+
* deliberate differences:
|
|
12
|
+
*
|
|
13
|
+
* 1. ONE baseline. The unpatched spec is evaluated exactly once; every
|
|
14
|
+
* candidate (accumulated accepted spec + patch k) is judged against
|
|
15
|
+
* that same baseline, so N patches cost N+1 evals, not 2N.
|
|
16
|
+
* 2. ACCEPTANCE: gate-pass only — the strict run-history gate must hold
|
|
17
|
+
* (`gateRuns`: any pass-rate drop or per-sample pass→fail flip
|
|
18
|
+
* rejects, fail-closed on incomparable rates), but strict improvement
|
|
19
|
+
* is NOT required. Advisor patches tune config for latency/cost/
|
|
20
|
+
* robustness, not accuracy — an equal pass rate with zero regressions
|
|
21
|
+
* is exactly the "didn't break anything" bar they need to clear. The
|
|
22
|
+
* delta is printed and persisted either way. (Contrast the flywheel,
|
|
23
|
+
* which additionally demands pass_rate strictly up: a rewritten
|
|
24
|
+
* prompt that buys nothing is not worth a PR; a max_tokens bump that
|
|
25
|
+
* holds the pass rate still fixes truncations.)
|
|
26
|
+
*
|
|
27
|
+
* Accepted patches COMPOSE: patch k+1 applies on top of the accumulated
|
|
28
|
+
* accepted YAML, so the final write-back is one coherent spec. Rejected
|
|
29
|
+
* patches are reported with their eval delta and never touch the
|
|
30
|
+
* accumulation. Every patch re-runs `validatePatch` against the CURRENT
|
|
31
|
+
* accumulated spec — the OPTIMIZABLE_PATHS whitelist stays the safety
|
|
32
|
+
* floor even for a hand-edited suggestions file.
|
|
33
|
+
*
|
|
34
|
+
* Kept in a side-effect-free module (the CLI entry file runs an argv
|
|
35
|
+
* switch on import) mirroring `eval-history.ts` / `advise-rules.ts`: the
|
|
36
|
+
* loop takes injected compile/eval hooks so accept/reject/compose
|
|
37
|
+
* semantics are unit-testable with synthetic summaries — no credentials.
|
|
38
|
+
*/
|
|
39
|
+
import type { EvalRunSummary } from "@crewhaus/eval-runner";
|
|
40
|
+
import { type SpecPatch } from "@crewhaus/spec-patch";
|
|
41
|
+
/** Thrown on invalid flag combos and suggestions-file validation failures.
|
|
42
|
+
* The CLI entry file catches it and routes the message through `die()`;
|
|
43
|
+
* tests assert on `.message` without the process exiting. */
|
|
44
|
+
export declare class AdviceApplyError extends Error {
|
|
45
|
+
readonly name = "AdviceApplyError";
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `--from-advice` replaces the mutation search entirely — the patches come
|
|
49
|
+
* from the suggestions file, so the mutator/iteration knobs have nothing to
|
|
50
|
+
* steer. Reject the combination loudly instead of silently ignoring flags
|
|
51
|
+
* the user believed were in effect (mirrors eval-matrix's
|
|
52
|
+
* `assertMatrixFlagsCompatible`). --dataset/--graders/--ratings still
|
|
53
|
+
* resolve as usual: the apply path needs an eval.
|
|
54
|
+
*/
|
|
55
|
+
export declare function assertFromAdviceFlagsCompatible(flags: {
|
|
56
|
+
readonly mutator: boolean;
|
|
57
|
+
readonly iterations: boolean;
|
|
58
|
+
}): void;
|
|
59
|
+
/** One patch lifted out of a validated suggestions.json entry. */
|
|
60
|
+
export type ParsedAdvicePatch = {
|
|
61
|
+
readonly findingId?: string;
|
|
62
|
+
readonly summary?: string;
|
|
63
|
+
readonly patch: SpecPatch;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Parse and schema-validate a suggestions.json produced by `crewhaus
|
|
67
|
+
* advise` (`buildSuggestionsFile` in ./advise-rules). Unknown or invalid
|
|
68
|
+
* patches are REJECTED with a clear per-entry error — an apply path must
|
|
69
|
+
* never guess at a malformed mutation. Advice-only findings never reach
|
|
70
|
+
* the file (they are report-only), so every entry must carry a patch.
|
|
71
|
+
*/
|
|
72
|
+
export declare function parseSuggestionsFile(text: string): ParsedAdvicePatch[];
|
|
73
|
+
export type AdvicePatchVerdict = {
|
|
74
|
+
readonly accepted: boolean;
|
|
75
|
+
readonly reason: string;
|
|
76
|
+
readonly passRateBefore: number;
|
|
77
|
+
readonly passRateAfter: number;
|
|
78
|
+
/** Sample-level pass→fail flips vs. the baseline run. */
|
|
79
|
+
readonly regressions: number;
|
|
80
|
+
/** Sample-level fail→pass flips vs. the baseline run. */
|
|
81
|
+
readonly recoveries: number;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* The from-advice acceptance bar (see the module header for the full
|
|
85
|
+
* rationale): the strict run-history gate must pass (`gateRuns`, the same
|
|
86
|
+
* gate the flywheel and `eval --gate` use: any pass-rate drop or
|
|
87
|
+
* per-sample pass→fail flip fails), but — unlike the flywheel — strict
|
|
88
|
+
* improvement is NOT required. Config patches fix latency/cost/robustness,
|
|
89
|
+
* not accuracy, so equal pass rate with zero regressions ACCEPTS; the
|
|
90
|
+
* delta is reported either way. Fail-closed on incomparable pass rates
|
|
91
|
+
* (a NaN rate satisfies no comparison, so it must reject explicitly).
|
|
92
|
+
*/
|
|
93
|
+
export declare function evaluateAdvicePatchAcceptance(before: EvalRunSummary, after: EvalRunSummary): AdvicePatchVerdict;
|
|
94
|
+
export type AdviceApplyHooks = {
|
|
95
|
+
/** Offline parse→lower gate. Throws when the compiler rejects the YAML.
|
|
96
|
+
* `label` is `"baseline"` or the candidate's `patch-NNN` label. */
|
|
97
|
+
readonly compileCheck: (yaml: string, label: string) => void;
|
|
98
|
+
/** One full eval pass of `yaml` over the dev split. The CLI persists the
|
|
99
|
+
* run under `<out>/advice/<label>/`. */
|
|
100
|
+
readonly evalRun: (label: string, yaml: string) => Promise<EvalRunSummary>;
|
|
101
|
+
};
|
|
102
|
+
export type AdvicePatchDecision = {
|
|
103
|
+
/** 1-based position in the suggestions file; matches the `patch-NNN`
|
|
104
|
+
* eval-dir label. */
|
|
105
|
+
readonly index: number;
|
|
106
|
+
readonly findingId?: string;
|
|
107
|
+
readonly summary?: string;
|
|
108
|
+
readonly patch: SpecPatch;
|
|
109
|
+
readonly status: "accepted" | "rejected";
|
|
110
|
+
readonly reason: string;
|
|
111
|
+
/** Present when the patch reached its eval (absent for patches rejected
|
|
112
|
+
* by validation / apply / compile — no eval was spent on those). */
|
|
113
|
+
readonly passRateBefore?: number;
|
|
114
|
+
readonly passRateAfter?: number;
|
|
115
|
+
readonly regressions?: number;
|
|
116
|
+
readonly recoveries?: number;
|
|
117
|
+
readonly evalDir?: string;
|
|
118
|
+
};
|
|
119
|
+
export type ApplyAdvicePatchesResult = {
|
|
120
|
+
readonly baseline: EvalRunSummary;
|
|
121
|
+
readonly decisions: ReadonlyArray<AdvicePatchDecision>;
|
|
122
|
+
readonly accepted: number;
|
|
123
|
+
/** The accumulated accepted YAML (identical to the source when nothing
|
|
124
|
+
* was accepted). NOT yet written anywhere — accept-then-write. */
|
|
125
|
+
readonly finalYaml: string;
|
|
126
|
+
/** The last ACCEPTED candidate's eval summary — the "after" side for the
|
|
127
|
+
* write-back header and regression pinning. Absent when 0 accepted. */
|
|
128
|
+
readonly finalSummary?: EvalRunSummary;
|
|
129
|
+
};
|
|
130
|
+
/** Format a candidate's eval-dir label from its 1-based index. */
|
|
131
|
+
export declare function patchLabel(index: number): string;
|
|
132
|
+
/**
|
|
133
|
+
* The complete eval-gated apply loop. The source is never written by this
|
|
134
|
+
* function — the caller owns write-back gating (`--write-back`). Baseline
|
|
135
|
+
* runs ONCE; each candidate is the accumulated accepted YAML plus patch k,
|
|
136
|
+
* compared against that one baseline. A baseline compile/eval failure
|
|
137
|
+
* propagates (nothing useful can happen without it); every per-patch
|
|
138
|
+
* failure (whitelist, apply conflict, compile, gate) records a rejection
|
|
139
|
+
* and the loop continues with the remaining patches.
|
|
140
|
+
*/
|
|
141
|
+
export declare function applyAdvicePatches(opts: {
|
|
142
|
+
readonly sourceYaml: string;
|
|
143
|
+
readonly patches: ReadonlyArray<ParsedAdvicePatch>;
|
|
144
|
+
readonly hooks: AdviceApplyHooks;
|
|
145
|
+
}): Promise<ApplyAdvicePatchesResult>;
|
|
146
|
+
export type AdviceDecisionsFile = {
|
|
147
|
+
readonly runId: string;
|
|
148
|
+
readonly generatedAt: string;
|
|
149
|
+
/** The suggestions file the patches came from. */
|
|
150
|
+
readonly source: string;
|
|
151
|
+
readonly baseline: {
|
|
152
|
+
readonly passRate: number;
|
|
153
|
+
readonly evalDir: string;
|
|
154
|
+
};
|
|
155
|
+
readonly evaluated: number;
|
|
156
|
+
readonly accepted: number;
|
|
157
|
+
readonly decisions: ReadonlyArray<AdvicePatchDecision>;
|
|
158
|
+
};
|
|
159
|
+
/** The `decisions.json` payload persisted under `<out>/advice/`. */
|
|
160
|
+
export declare function buildAdviceDecisionsFile(opts: {
|
|
161
|
+
readonly runId: string;
|
|
162
|
+
readonly generatedAt: string;
|
|
163
|
+
readonly source: string;
|
|
164
|
+
readonly baseline: EvalRunSummary;
|
|
165
|
+
readonly decisions: ReadonlyArray<AdvicePatchDecision>;
|
|
166
|
+
}): AdviceDecisionsFile;
|
|
167
|
+
/** One line per decision for the CLI's report. */
|
|
168
|
+
export declare function formatAdviceDecisionLine(d: AdvicePatchDecision): string;
|
|
169
|
+
/**
|
|
170
|
+
* Stamp the accumulated accepted YAML with the same provenance header a
|
|
171
|
+
* successful `optimize --write-back` produces (`formatWriteBackHeader`, the
|
|
172
|
+
* stamp `spec log` distills), with `mutator: advisor` naming the patch
|
|
173
|
+
* source and `iterations` carrying the number of patches evaluated.
|
|
174
|
+
*/
|
|
175
|
+
export declare function stampAdviceWriteBack(opts: {
|
|
176
|
+
readonly runId: string;
|
|
177
|
+
readonly yaml: string;
|
|
178
|
+
readonly passRateBefore: number;
|
|
179
|
+
readonly passRateAfter: number;
|
|
180
|
+
readonly patchesEvaluated: number;
|
|
181
|
+
readonly timestamp?: string;
|
|
182
|
+
}): string;
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { parseSpec } from "@crewhaus/spec";
|
|
2
|
+
import { applySpecPatch, formatWriteBackHeader, validatePatch, } from "@crewhaus/spec-patch";
|
|
3
|
+
import { gateRuns } from "./eval-history";
|
|
4
|
+
/** Thrown on invalid flag combos and suggestions-file validation failures.
|
|
5
|
+
* The CLI entry file catches it and routes the message through `die()`;
|
|
6
|
+
* tests assert on `.message` without the process exiting. */
|
|
7
|
+
export class AdviceApplyError extends Error {
|
|
8
|
+
name = "AdviceApplyError";
|
|
9
|
+
}
|
|
10
|
+
// -------- flag validation --------
|
|
11
|
+
/**
|
|
12
|
+
* `--from-advice` replaces the mutation search entirely — the patches come
|
|
13
|
+
* from the suggestions file, so the mutator/iteration knobs have nothing to
|
|
14
|
+
* steer. Reject the combination loudly instead of silently ignoring flags
|
|
15
|
+
* the user believed were in effect (mirrors eval-matrix's
|
|
16
|
+
* `assertMatrixFlagsCompatible`). --dataset/--graders/--ratings still
|
|
17
|
+
* resolve as usual: the apply path needs an eval.
|
|
18
|
+
*/
|
|
19
|
+
export function assertFromAdviceFlagsCompatible(flags) {
|
|
20
|
+
if (flags.mutator) {
|
|
21
|
+
throw new AdviceApplyError("--from-advice is mutually exclusive with --mutator — the patches come from the suggestions file; no mutation search runs");
|
|
22
|
+
}
|
|
23
|
+
if (flags.iterations) {
|
|
24
|
+
throw new AdviceApplyError("--from-advice is mutually exclusive with --iterations — every patch in the suggestions file is evaluated exactly once");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const PATCH_OPS = new Set(["replace", "add", "remove"]);
|
|
28
|
+
function isPlainObject(v) {
|
|
29
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
30
|
+
}
|
|
31
|
+
/** Structural validation of one entry's `patch` — shape only; the
|
|
32
|
+
* spec-aware checks (target match, OPTIMIZABLE_PATHS) run per-patch in
|
|
33
|
+
* the loop via `validatePatch`. Returns an error string or undefined. */
|
|
34
|
+
function patchShapeError(patch) {
|
|
35
|
+
if (!isPlainObject(patch))
|
|
36
|
+
return "patch must be an object";
|
|
37
|
+
if (typeof patch["target"] !== "string" || patch["target"] === "") {
|
|
38
|
+
return "patch.target must be a non-empty string";
|
|
39
|
+
}
|
|
40
|
+
const path = patch["path"];
|
|
41
|
+
if (!Array.isArray(path) ||
|
|
42
|
+
path.length === 0 ||
|
|
43
|
+
path.some((p) => typeof p !== "string" || p === "")) {
|
|
44
|
+
return "patch.path must be a non-empty array of non-empty strings";
|
|
45
|
+
}
|
|
46
|
+
const op = patch["op"];
|
|
47
|
+
if (typeof op !== "string" || !PATCH_OPS.has(op)) {
|
|
48
|
+
return `patch.op must be one of replace|add|remove (got ${JSON.stringify(op)})`;
|
|
49
|
+
}
|
|
50
|
+
if (op !== "remove" && patch["value"] === undefined) {
|
|
51
|
+
return `patch.value is required for op "${op}"`;
|
|
52
|
+
}
|
|
53
|
+
if (patch["rationale"] !== undefined && typeof patch["rationale"] !== "string") {
|
|
54
|
+
return "patch.rationale must be a string when present";
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Parse and schema-validate a suggestions.json produced by `crewhaus
|
|
60
|
+
* advise` (`buildSuggestionsFile` in ./advise-rules). Unknown or invalid
|
|
61
|
+
* patches are REJECTED with a clear per-entry error — an apply path must
|
|
62
|
+
* never guess at a malformed mutation. Advice-only findings never reach
|
|
63
|
+
* the file (they are report-only), so every entry must carry a patch.
|
|
64
|
+
*/
|
|
65
|
+
export function parseSuggestionsFile(text) {
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(text);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
throw new AdviceApplyError(`suggestions file is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
72
|
+
}
|
|
73
|
+
if (!isPlainObject(parsed) || !Array.isArray(parsed["suggestions"])) {
|
|
74
|
+
throw new AdviceApplyError('suggestions file must be an object with a "suggestions" array (the `crewhaus advise` suggestions.json format)');
|
|
75
|
+
}
|
|
76
|
+
const out = [];
|
|
77
|
+
for (const [i, entry] of parsed["suggestions"].entries()) {
|
|
78
|
+
if (!isPlainObject(entry)) {
|
|
79
|
+
throw new AdviceApplyError(`suggestions[${i}] must be an object`);
|
|
80
|
+
}
|
|
81
|
+
const findingId = typeof entry["findingId"] === "string" ? entry["findingId"] : undefined;
|
|
82
|
+
const label = findingId !== undefined ? `suggestions[${i}] (${findingId})` : `suggestions[${i}]`;
|
|
83
|
+
const shapeError = patchShapeError(entry["patch"]);
|
|
84
|
+
if (shapeError !== undefined) {
|
|
85
|
+
throw new AdviceApplyError(`${label}: ${shapeError}`);
|
|
86
|
+
}
|
|
87
|
+
const raw = entry["patch"];
|
|
88
|
+
const patch = {
|
|
89
|
+
target: raw["target"],
|
|
90
|
+
path: raw["path"],
|
|
91
|
+
op: raw["op"],
|
|
92
|
+
...(raw["value"] !== undefined ? { value: raw["value"] } : {}),
|
|
93
|
+
...(typeof raw["rationale"] === "string" ? { rationale: raw["rationale"] } : {}),
|
|
94
|
+
};
|
|
95
|
+
out.push({
|
|
96
|
+
...(findingId !== undefined ? { findingId } : {}),
|
|
97
|
+
...(typeof entry["summary"] === "string" ? { summary: entry["summary"] } : {}),
|
|
98
|
+
patch,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
function pct(rate) {
|
|
104
|
+
return `${(rate * 100).toFixed(1)}%`;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The from-advice acceptance bar (see the module header for the full
|
|
108
|
+
* rationale): the strict run-history gate must pass (`gateRuns`, the same
|
|
109
|
+
* gate the flywheel and `eval --gate` use: any pass-rate drop or
|
|
110
|
+
* per-sample pass→fail flip fails), but — unlike the flywheel — strict
|
|
111
|
+
* improvement is NOT required. Config patches fix latency/cost/robustness,
|
|
112
|
+
* not accuracy, so equal pass rate with zero regressions ACCEPTS; the
|
|
113
|
+
* delta is reported either way. Fail-closed on incomparable pass rates
|
|
114
|
+
* (a NaN rate satisfies no comparison, so it must reject explicitly).
|
|
115
|
+
*/
|
|
116
|
+
export function evaluateAdvicePatchAcceptance(before, after) {
|
|
117
|
+
const passRateBefore = before.aggregates.passRate;
|
|
118
|
+
const passRateAfter = after.aggregates.passRate;
|
|
119
|
+
const verdict = gateRuns(before, after);
|
|
120
|
+
const base = {
|
|
121
|
+
passRateBefore,
|
|
122
|
+
passRateAfter,
|
|
123
|
+
regressions: verdict.report.regressions.length,
|
|
124
|
+
recoveries: verdict.report.recoveries.length,
|
|
125
|
+
};
|
|
126
|
+
if (!Number.isFinite(passRateBefore) || !Number.isFinite(passRateAfter)) {
|
|
127
|
+
return {
|
|
128
|
+
accepted: false,
|
|
129
|
+
reason: `pass_rate not comparable (before=${String(passRateBefore)}, ` +
|
|
130
|
+
`after=${String(passRateAfter)}) — rejecting fail-closed`,
|
|
131
|
+
...base,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (verdict.verdict === "fail") {
|
|
135
|
+
return { accepted: false, reason: verdict.reason ?? "regression gate failed", ...base };
|
|
136
|
+
}
|
|
137
|
+
// Gate passed ⇒ no drop and no flips. Equal-or-better both accept; word
|
|
138
|
+
// the reason so the printed delta documents which case this was.
|
|
139
|
+
if (passRateAfter > passRateBefore) {
|
|
140
|
+
return {
|
|
141
|
+
accepted: true,
|
|
142
|
+
reason: `pass_rate ${pct(passRateBefore)} → ${pct(passRateAfter)} with zero regressions`,
|
|
143
|
+
...base,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
accepted: true,
|
|
148
|
+
reason: `pass_rate held at ${pct(passRateBefore)} with zero regressions (config patch: gate-pass accepts; strict improvement not required)`,
|
|
149
|
+
...base,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/** Format a candidate's eval-dir label from its 1-based index. */
|
|
153
|
+
export function patchLabel(index) {
|
|
154
|
+
return `patch-${String(index).padStart(3, "0")}`;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The complete eval-gated apply loop. The source is never written by this
|
|
158
|
+
* function — the caller owns write-back gating (`--write-back`). Baseline
|
|
159
|
+
* runs ONCE; each candidate is the accumulated accepted YAML plus patch k,
|
|
160
|
+
* compared against that one baseline. A baseline compile/eval failure
|
|
161
|
+
* propagates (nothing useful can happen without it); every per-patch
|
|
162
|
+
* failure (whitelist, apply conflict, compile, gate) records a rejection
|
|
163
|
+
* and the loop continues with the remaining patches.
|
|
164
|
+
*/
|
|
165
|
+
export async function applyAdvicePatches(opts) {
|
|
166
|
+
const { hooks } = opts;
|
|
167
|
+
// 1. Offline sanity gate — a spec that doesn't compile must never reach
|
|
168
|
+
// the (paid) baseline eval.
|
|
169
|
+
hooks.compileCheck(opts.sourceYaml, "baseline");
|
|
170
|
+
// 2. Baseline eval — once, on the unpatched spec.
|
|
171
|
+
const baseline = await hooks.evalRun("baseline", opts.sourceYaml);
|
|
172
|
+
const decisions = [];
|
|
173
|
+
let accumulatedYaml = opts.sourceYaml;
|
|
174
|
+
let accepted = 0;
|
|
175
|
+
let finalSummary;
|
|
176
|
+
for (const [i, entry] of opts.patches.entries()) {
|
|
177
|
+
const index = i + 1;
|
|
178
|
+
const label = patchLabel(index);
|
|
179
|
+
const meta = {
|
|
180
|
+
index,
|
|
181
|
+
...(entry.findingId !== undefined ? { findingId: entry.findingId } : {}),
|
|
182
|
+
...(entry.summary !== undefined ? { summary: entry.summary } : {}),
|
|
183
|
+
patch: entry.patch,
|
|
184
|
+
};
|
|
185
|
+
// 3a. Spec-aware validation against the CURRENT accumulated spec: the
|
|
186
|
+
// OPTIMIZABLE_PATHS whitelist + target match stay the safety floor
|
|
187
|
+
// even for a hand-edited suggestions file.
|
|
188
|
+
try {
|
|
189
|
+
validatePatch(parseSpec(accumulatedYaml), entry.patch);
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
decisions.push({
|
|
193
|
+
...meta,
|
|
194
|
+
status: "rejected",
|
|
195
|
+
reason: `patch invalid: ${err instanceof Error ? err.message : String(err)}`,
|
|
196
|
+
});
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
// 3b. Apply in-memory on top of the accumulated accepted YAML.
|
|
200
|
+
let candidateYaml;
|
|
201
|
+
try {
|
|
202
|
+
candidateYaml = applySpecPatch(accumulatedYaml, entry.patch).yaml;
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
decisions.push({
|
|
206
|
+
...meta,
|
|
207
|
+
status: "rejected",
|
|
208
|
+
reason: `patch failed to apply: ${err instanceof Error ? err.message : String(err)}`,
|
|
209
|
+
});
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
// 3c. Compile gate before the paid eval.
|
|
213
|
+
try {
|
|
214
|
+
hooks.compileCheck(candidateYaml, label);
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
decisions.push({
|
|
218
|
+
...meta,
|
|
219
|
+
status: "rejected",
|
|
220
|
+
reason: `patched spec failed to compile: ${err instanceof Error ? err.message : String(err)}`,
|
|
221
|
+
});
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
// 3d. Candidate eval + acceptance gate vs. the ONE baseline.
|
|
225
|
+
const candidate = await hooks.evalRun(label, candidateYaml);
|
|
226
|
+
const verdict = evaluateAdvicePatchAcceptance(baseline, candidate);
|
|
227
|
+
const evalFields = {
|
|
228
|
+
passRateBefore: verdict.passRateBefore,
|
|
229
|
+
passRateAfter: verdict.passRateAfter,
|
|
230
|
+
regressions: verdict.regressions,
|
|
231
|
+
recoveries: verdict.recoveries,
|
|
232
|
+
evalDir: candidate.outDir,
|
|
233
|
+
};
|
|
234
|
+
if (!verdict.accepted) {
|
|
235
|
+
decisions.push({ ...meta, status: "rejected", reason: verdict.reason, ...evalFields });
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
accumulatedYaml = candidateYaml;
|
|
239
|
+
finalSummary = candidate;
|
|
240
|
+
accepted += 1;
|
|
241
|
+
decisions.push({ ...meta, status: "accepted", reason: verdict.reason, ...evalFields });
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
baseline,
|
|
245
|
+
decisions,
|
|
246
|
+
accepted,
|
|
247
|
+
finalYaml: accumulatedYaml,
|
|
248
|
+
...(finalSummary !== undefined ? { finalSummary } : {}),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/** The `decisions.json` payload persisted under `<out>/advice/`. */
|
|
252
|
+
export function buildAdviceDecisionsFile(opts) {
|
|
253
|
+
return {
|
|
254
|
+
runId: opts.runId,
|
|
255
|
+
generatedAt: opts.generatedAt,
|
|
256
|
+
source: opts.source,
|
|
257
|
+
baseline: { passRate: opts.baseline.aggregates.passRate, evalDir: opts.baseline.outDir },
|
|
258
|
+
evaluated: opts.decisions.length,
|
|
259
|
+
accepted: opts.decisions.filter((d) => d.status === "accepted").length,
|
|
260
|
+
decisions: opts.decisions,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/** One line per decision for the CLI's report. */
|
|
264
|
+
export function formatAdviceDecisionLine(d) {
|
|
265
|
+
const id = d.findingId !== undefined ? ` ${d.findingId}` : "";
|
|
266
|
+
const value = d.patch.op === "remove" ? "" : ` → ${JSON.stringify(d.patch.value)}`;
|
|
267
|
+
const head = `${patchLabel(d.index)}${id}: ${d.patch.op} ${d.patch.path.join(".")}${value}`;
|
|
268
|
+
return `${head} — ${d.status.toUpperCase()}: ${d.reason}`;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Stamp the accumulated accepted YAML with the same provenance header a
|
|
272
|
+
* successful `optimize --write-back` produces (`formatWriteBackHeader`, the
|
|
273
|
+
* stamp `spec log` distills), with `mutator: advisor` naming the patch
|
|
274
|
+
* source and `iterations` carrying the number of patches evaluated.
|
|
275
|
+
*/
|
|
276
|
+
export function stampAdviceWriteBack(opts) {
|
|
277
|
+
const header = formatWriteBackHeader({
|
|
278
|
+
runId: opts.runId,
|
|
279
|
+
mutator: "advisor",
|
|
280
|
+
scoreBefore: opts.passRateBefore,
|
|
281
|
+
scoreAfter: opts.passRateAfter,
|
|
282
|
+
iterations: opts.patchesEvaluated,
|
|
283
|
+
...(opts.timestamp !== undefined ? { timestamp: opts.timestamp } : {}),
|
|
284
|
+
});
|
|
285
|
+
return `${header}${opts.yaml}`;
|
|
286
|
+
}
|