crewhaus 0.2.0 → 0.2.2
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/dist/advise-rules.d.ts +55 -1
- package/dist/advise-rules.js +165 -1
- package/dist/dataset-mine.d.ts +3 -2
- package/dist/dataset-mine.js +3 -2
- package/dist/index.js +110 -13
- package/dist/route.d.ts +39 -0
- package/dist/route.js +168 -0
- package/dist/security-digest.d.ts +2 -1
- package/dist/security-digest.js +18 -12
- package/dist/tools-cli.js +2 -0
- package/package.json +89 -88
package/dist/advise-rules.d.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* context slice for each missing kind and simply produce no findings — the
|
|
16
16
|
* rules only fire where the data now exists.
|
|
17
17
|
*/
|
|
18
|
+
import type { ArmStats } from "@crewhaus/routing-store";
|
|
18
19
|
import type { Spec } from "@crewhaus/spec";
|
|
19
20
|
import { type SpecPatch } from "@crewhaus/spec-patch";
|
|
20
21
|
export type AdviceSeverity = "info" | "warn";
|
|
@@ -108,6 +109,13 @@ export type AdviceContext = {
|
|
|
108
109
|
readonly totalToolBytes: number;
|
|
109
110
|
/** `.crewhaus/audit` record kinds → counts (report context + future rules). */
|
|
110
111
|
readonly auditKindCounts: ReadonlyMap<string, number>;
|
|
112
|
+
/**
|
|
113
|
+
* Adaptive model routing — the `model_pool` reward scoreboard arms from
|
|
114
|
+
* `.crewhaus/routing/arms.jsonl` (empty when the harness has never routed a
|
|
115
|
+
* pool). The routing rules mine these into pool-POLICY suggestions; the
|
|
116
|
+
* candidate roster itself stays human-owned.
|
|
117
|
+
*/
|
|
118
|
+
readonly routingArms: ReadonlyArray<ArmStats>;
|
|
111
119
|
};
|
|
112
120
|
export type SessionEvents = {
|
|
113
121
|
readonly sessionId: string;
|
|
@@ -140,7 +148,7 @@ export declare function payloadOf(obj: unknown, kind: string): Record<string, un
|
|
|
140
148
|
* old-vintage logs (no advisor kinds) and future kinds both pass through
|
|
141
149
|
* cleanly.
|
|
142
150
|
*/
|
|
143
|
-
export declare function buildAdviceContext(sessions: ReadonlyArray<SessionEvents>, auditObjects?: ReadonlyArray<unknown>): AdviceContext;
|
|
151
|
+
export declare function buildAdviceContext(sessions: ReadonlyArray<SessionEvents>, auditObjects?: ReadonlyArray<unknown>, routingArms?: ReadonlyArray<ArmStats>): AdviceContext;
|
|
144
152
|
export type AdviceThresholds = {
|
|
145
153
|
/** Minimum calls before a tool's error rate is judged at all. */
|
|
146
154
|
toolFailureMinCalls: number;
|
|
@@ -171,6 +179,13 @@ export type AdviceThresholds = {
|
|
|
171
179
|
/** Item 21 — minimum total tool bytes before dominance is judged at all
|
|
172
180
|
* (a handful of tiny calls shouldn't trip a restructure). */
|
|
173
181
|
toolByteMinTotal: number;
|
|
182
|
+
/** Adaptive routing — samples an arm needs before the routing rules judge
|
|
183
|
+
* it, when the spec doesn't set `model_pool.learning.minSamplesPerArm`.
|
|
184
|
+
* Mirrors the PolicyRouter's own default floor. */
|
|
185
|
+
poolMinSamples: number;
|
|
186
|
+
/** Adaptive routing — mean-reward gap below the band best at/above which a
|
|
187
|
+
* candidate is called consistently losing (demotion advice). */
|
|
188
|
+
poolDemotionGap: number;
|
|
174
189
|
};
|
|
175
190
|
/**
|
|
176
191
|
* Canonical rule thresholds. runtime-core's in-run digest
|
|
@@ -187,6 +202,16 @@ export type RuleOptions = {
|
|
|
187
202
|
/** The cwd spec, when one exists — enables patch suggestions. */
|
|
188
203
|
readonly spec?: Spec;
|
|
189
204
|
readonly thresholds?: Partial<AdviceThresholds>;
|
|
205
|
+
/**
|
|
206
|
+
* Whether the RAW spec YAML textually carries a key at `path` (the driver
|
|
207
|
+
* builds this from `specHasPath` in `@crewhaus/spec-patch`). Zod-defaulted
|
|
208
|
+
* fields are always present on the parsed `spec`, but `applySpecPatch`
|
|
209
|
+
* requires `add` for absent keys and `replace` for present ones — a rule
|
|
210
|
+
* proposing a patch on a defaultable field needs this to pick the op.
|
|
211
|
+
* Absent → such rules assume the key is absent (the common case for
|
|
212
|
+
* defaulted fields) and emit `add`.
|
|
213
|
+
*/
|
|
214
|
+
readonly specHasPath?: (path: ReadonlyArray<string>) => boolean;
|
|
190
215
|
};
|
|
191
216
|
export type AdviceRule = (ctx: AdviceContext, opts?: RuleOptions) => AdviceFinding[];
|
|
192
217
|
/**
|
|
@@ -315,6 +340,35 @@ export declare function subAgentFragment(toolName: string): string;
|
|
|
315
340
|
* rule stays silent, since there's nowhere to paste it.
|
|
316
341
|
*/
|
|
317
342
|
export declare const ruleSubAgentSplit: AdviceRule;
|
|
343
|
+
/**
|
|
344
|
+
* Pool policy upgrade: the spec declares a `model_pool` that is NOT yet
|
|
345
|
+
* `learned`, and the reward scoreboard already has full-coverage data (every
|
|
346
|
+
* candidate past the sample floor in ≥1 difficulty band) — the harness has
|
|
347
|
+
* earned the right to exploit what it measured. Proposes flipping
|
|
348
|
+
* `model_pool.policy` to `learned` (whitelisted; `optimize --from-advice`
|
|
349
|
+
* eval-gates the flip). The candidate ROSTER is never touched.
|
|
350
|
+
*/
|
|
351
|
+
export declare const rulePoolPolicyUpgrade: AdviceRule;
|
|
352
|
+
/**
|
|
353
|
+
* Pool candidate demotion: a candidate that is past the sample floor and
|
|
354
|
+
* loses to the band best by ≥ `poolDemotionGap` mean reward in EVERY band
|
|
355
|
+
* with full coverage is consistently wasted spend. ADVICE-ONLY by design:
|
|
356
|
+
* the candidate roster is human-owned (mirroring the `["agent","model"]`
|
|
357
|
+
* OPTIMIZABLE_PATHS exclusion), so this never emits a patch — it names the
|
|
358
|
+
* loser and lets a human edit `model_pool.candidates`.
|
|
359
|
+
*/
|
|
360
|
+
export declare const rulePoolCandidateDemotion: AdviceRule;
|
|
361
|
+
/**
|
|
362
|
+
* Stale exploitation: a `learned` pool whose scoreboard has full coverage but
|
|
363
|
+
* whose config never explores again — `explorationRate` unset/0 and the
|
|
364
|
+
* bandit is not `thompson` (which self-explores via posterior sampling). Once
|
|
365
|
+
* every arm clears the floor, such a pool hard-commits to the argmax forever
|
|
366
|
+
* and can never notice model drift or a candidate that improved. Proposes a
|
|
367
|
+
* small ε (0.05) via a whole-`learning`-block replace that PRESERVES the
|
|
368
|
+
* spec's other learning fields (whitelisted path; `optimize --from-advice`
|
|
369
|
+
* eval-gates it).
|
|
370
|
+
*/
|
|
371
|
+
export declare const rulePoolStaleExploitation: AdviceRule;
|
|
318
372
|
export declare const ADVICE_RULES: ReadonlyArray<AdviceRule>;
|
|
319
373
|
/**
|
|
320
374
|
* Run every rule and rank the findings: warn before info, then by the
|
package/dist/advise-rules.js
CHANGED
|
@@ -64,7 +64,7 @@ export function payloadOf(obj, kind) {
|
|
|
64
64
|
* old-vintage logs (no advisor kinds) and future kinds both pass through
|
|
65
65
|
* cleanly.
|
|
66
66
|
*/
|
|
67
|
-
export function buildAdviceContext(sessions, auditObjects = []) {
|
|
67
|
+
export function buildAdviceContext(sessions, auditObjects = [], routingArms = []) {
|
|
68
68
|
const toolStats = new Map();
|
|
69
69
|
const recoveriesByAction = new Map();
|
|
70
70
|
const recoveriesByErrorName = new Map();
|
|
@@ -197,6 +197,7 @@ export function buildAdviceContext(sessions, auditObjects = []) {
|
|
|
197
197
|
perToolBytes,
|
|
198
198
|
totalToolBytes,
|
|
199
199
|
auditKindCounts,
|
|
200
|
+
routingArms,
|
|
200
201
|
};
|
|
201
202
|
}
|
|
202
203
|
/**
|
|
@@ -218,6 +219,8 @@ export const DEFAULT_ADVICE_THRESHOLDS = {
|
|
|
218
219
|
chronicCompactionSessions: 2,
|
|
219
220
|
toolByteDominance: 0.6,
|
|
220
221
|
toolByteMinTotal: 50_000,
|
|
222
|
+
poolMinSamples: 25,
|
|
223
|
+
poolDemotionGap: 0.3,
|
|
221
224
|
};
|
|
222
225
|
/** Stop reasons that are part of healthy operation; everything else
|
|
223
226
|
* (max_tokens, refusal, pause_turn, …) counts as anomalous. Mirrored by
|
|
@@ -798,6 +801,164 @@ export const ruleSubAgentSplit = (ctx, opts) => {
|
|
|
798
801
|
},
|
|
799
802
|
];
|
|
800
803
|
};
|
|
804
|
+
function agentModelPool(spec) {
|
|
805
|
+
const agent = specField(spec, "agent");
|
|
806
|
+
const pool = agent?.["model_pool"];
|
|
807
|
+
if (pool === undefined || pool === null || typeof pool !== "object")
|
|
808
|
+
return undefined;
|
|
809
|
+
return pool;
|
|
810
|
+
}
|
|
811
|
+
/** The bands (routeKeys) where EVERY pool candidate has ≥ `floor` samples. */
|
|
812
|
+
function bandsWithFullCoverage(arms, candidates, floor) {
|
|
813
|
+
const bands = [...new Set(arms.map((a) => a.routeKey))];
|
|
814
|
+
return bands.filter((band) => candidates.every((c) => arms.some((a) => a.routeKey === band && a.model === c.model && a.n >= floor)));
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Pool policy upgrade: the spec declares a `model_pool` that is NOT yet
|
|
818
|
+
* `learned`, and the reward scoreboard already has full-coverage data (every
|
|
819
|
+
* candidate past the sample floor in ≥1 difficulty band) — the harness has
|
|
820
|
+
* earned the right to exploit what it measured. Proposes flipping
|
|
821
|
+
* `model_pool.policy` to `learned` (whitelisted; `optimize --from-advice`
|
|
822
|
+
* eval-gates the flip). The candidate ROSTER is never touched.
|
|
823
|
+
*/
|
|
824
|
+
export const rulePoolPolicyUpgrade = (ctx, opts) => {
|
|
825
|
+
const t = resolveThresholds(opts);
|
|
826
|
+
const spec = opts?.spec;
|
|
827
|
+
const pool = agentModelPool(spec);
|
|
828
|
+
if (pool === undefined || pool.policy === "learned")
|
|
829
|
+
return [];
|
|
830
|
+
if (ctx.routingArms.length === 0 || pool.candidates.length < 2)
|
|
831
|
+
return [];
|
|
832
|
+
const floor = pool.learning?.minSamplesPerArm ?? t.poolMinSamples;
|
|
833
|
+
const ready = bandsWithFullCoverage(ctx.routingArms, pool.candidates, floor);
|
|
834
|
+
if (ready.length === 0)
|
|
835
|
+
return [];
|
|
836
|
+
const totalSamples = ctx.routingArms.reduce((acc, a) => acc + a.n, 0);
|
|
837
|
+
const adviceText = `The model_pool reward scoreboard has ≥${floor} samples for every candidate in band(s) ${ready.join(", ")} — set model_pool.policy: learned so routing exploits the measured per-band winners instead of the fixed heuristic.`;
|
|
838
|
+
const policyKeyPresent = opts?.specHasPath?.(["agent", "model_pool", "policy"]) ?? false;
|
|
839
|
+
const patch = {
|
|
840
|
+
target: spec?.target ?? "cli",
|
|
841
|
+
path: ["agent", "model_pool", "policy"],
|
|
842
|
+
op: policyKeyPresent ? "replace" : "add",
|
|
843
|
+
value: "learned",
|
|
844
|
+
rationale: `advise: scoreboard full coverage (every candidate ≥${floor} samples) in band(s) ${ready.join(", ")} across ${totalSamples} routed decisions`,
|
|
845
|
+
};
|
|
846
|
+
return [
|
|
847
|
+
{
|
|
848
|
+
id: "pool-policy-upgrade",
|
|
849
|
+
severity: "info",
|
|
850
|
+
summary: "model_pool scoreboard is ready — flip policy to learned",
|
|
851
|
+
evidence: [
|
|
852
|
+
`bands with every candidate ≥${floor} samples: ${ready.join(", ")}`,
|
|
853
|
+
`${totalSamples} routed decisions recorded across ${ctx.routingArms.length} arm(s)`,
|
|
854
|
+
`current policy: ${pool.policy}`,
|
|
855
|
+
],
|
|
856
|
+
counts: { readyBands: ready.length, totalSamples },
|
|
857
|
+
suggestion: patchOrAdvice(spec, patch, adviceText),
|
|
858
|
+
},
|
|
859
|
+
];
|
|
860
|
+
};
|
|
861
|
+
/**
|
|
862
|
+
* Pool candidate demotion: a candidate that is past the sample floor and
|
|
863
|
+
* loses to the band best by ≥ `poolDemotionGap` mean reward in EVERY band
|
|
864
|
+
* with full coverage is consistently wasted spend. ADVICE-ONLY by design:
|
|
865
|
+
* the candidate roster is human-owned (mirroring the `["agent","model"]`
|
|
866
|
+
* OPTIMIZABLE_PATHS exclusion), so this never emits a patch — it names the
|
|
867
|
+
* loser and lets a human edit `model_pool.candidates`.
|
|
868
|
+
*/
|
|
869
|
+
export const rulePoolCandidateDemotion = (ctx, opts) => {
|
|
870
|
+
const t = resolveThresholds(opts);
|
|
871
|
+
const pool = agentModelPool(opts?.spec);
|
|
872
|
+
if (pool === undefined || ctx.routingArms.length === 0 || pool.candidates.length < 3) {
|
|
873
|
+
// With only 2 candidates, "demote the loser" would collapse the pool —
|
|
874
|
+
// routing a 1-model pool is meaningless; leave that call entirely human.
|
|
875
|
+
return [];
|
|
876
|
+
}
|
|
877
|
+
const floor = pool.learning?.minSamplesPerArm ?? t.poolMinSamples;
|
|
878
|
+
const fullBands = bandsWithFullCoverage(ctx.routingArms, pool.candidates, floor);
|
|
879
|
+
if (fullBands.length === 0)
|
|
880
|
+
return [];
|
|
881
|
+
const losers = pool.candidates.filter((c) => fullBands.every((band) => {
|
|
882
|
+
const bandArms = ctx.routingArms.filter((a) => a.routeKey === band);
|
|
883
|
+
const best = Math.max(...bandArms.map((a) => a.meanReward));
|
|
884
|
+
const own = bandArms.find((a) => a.model === c.model);
|
|
885
|
+
return own !== undefined && own.meanReward <= best - t.poolDemotionGap;
|
|
886
|
+
}));
|
|
887
|
+
return losers.map((loser) => {
|
|
888
|
+
const detail = fullBands
|
|
889
|
+
.map((band) => {
|
|
890
|
+
const bandArms = ctx.routingArms.filter((a) => a.routeKey === band);
|
|
891
|
+
const best = Math.max(...bandArms.map((a) => a.meanReward));
|
|
892
|
+
const own = bandArms.find((a) => a.model === loser.model);
|
|
893
|
+
return `${band}: ${own?.meanReward.toFixed(3)} vs best ${best.toFixed(3)}`;
|
|
894
|
+
})
|
|
895
|
+
.join("; ");
|
|
896
|
+
return {
|
|
897
|
+
id: `pool-candidate-demotion:${loser.model}`,
|
|
898
|
+
severity: "info",
|
|
899
|
+
summary: `model_pool candidate ${loser.model} consistently loses`,
|
|
900
|
+
evidence: [
|
|
901
|
+
`mean reward trails the band best by ≥${t.poolDemotionGap} in every full-coverage band (${detail})`,
|
|
902
|
+
"the candidate roster is human-owned — remove it from model_pool.candidates by hand if the trend holds",
|
|
903
|
+
],
|
|
904
|
+
counts: { fullBands: fullBands.length },
|
|
905
|
+
suggestion: {
|
|
906
|
+
kind: "advice",
|
|
907
|
+
text: `Candidate ${loser.model} has enough samples and still trails the best arm by ≥${t.poolDemotionGap} mean reward in every measured band (${detail}). Consider removing it from model_pool.candidates — fewer arms also means less exploration spend. This is a roster change, so it is never auto-applied.`,
|
|
908
|
+
},
|
|
909
|
+
};
|
|
910
|
+
});
|
|
911
|
+
};
|
|
912
|
+
/**
|
|
913
|
+
* Stale exploitation: a `learned` pool whose scoreboard has full coverage but
|
|
914
|
+
* whose config never explores again — `explorationRate` unset/0 and the
|
|
915
|
+
* bandit is not `thompson` (which self-explores via posterior sampling). Once
|
|
916
|
+
* every arm clears the floor, such a pool hard-commits to the argmax forever
|
|
917
|
+
* and can never notice model drift or a candidate that improved. Proposes a
|
|
918
|
+
* small ε (0.05) via a whole-`learning`-block replace that PRESERVES the
|
|
919
|
+
* spec's other learning fields (whitelisted path; `optimize --from-advice`
|
|
920
|
+
* eval-gates it).
|
|
921
|
+
*/
|
|
922
|
+
export const rulePoolStaleExploitation = (ctx, opts) => {
|
|
923
|
+
const t = resolveThresholds(opts);
|
|
924
|
+
const spec = opts?.spec;
|
|
925
|
+
const pool = agentModelPool(spec);
|
|
926
|
+
if (pool === undefined || pool.policy !== "learned")
|
|
927
|
+
return [];
|
|
928
|
+
if (ctx.routingArms.length === 0 || pool.candidates.length < 2)
|
|
929
|
+
return [];
|
|
930
|
+
const learning = pool.learning ?? {};
|
|
931
|
+
// Already exploring (ε > 0) or self-exploring (thompson) → healthy.
|
|
932
|
+
if ((learning.explorationRate ?? 0) > 0 || learning.bandit === "thompson")
|
|
933
|
+
return [];
|
|
934
|
+
const floor = learning.minSamplesPerArm ?? t.poolMinSamples;
|
|
935
|
+
const ready = bandsWithFullCoverage(ctx.routingArms, pool.candidates, floor);
|
|
936
|
+
if (ready.length === 0)
|
|
937
|
+
return []; // still warming up — the floor explores for it
|
|
938
|
+
const proposedLearning = { ...learning, explorationRate: 0.05 };
|
|
939
|
+
const learningKeyPresent = opts?.specHasPath?.(["agent", "model_pool", "learning"]) ?? false;
|
|
940
|
+
const adviceText = "This learned model_pool has cleared its sample floor in every measured band and now exploits the argmax forever — it can no longer notice model drift or an improved candidate. Set model_pool.learning.explorationRate (e.g. 0.05) to keep sampling occasionally, or switch learning.bandit to thompson, which self-balances exploration.";
|
|
941
|
+
const patch = {
|
|
942
|
+
target: spec?.target ?? "cli",
|
|
943
|
+
path: ["agent", "model_pool", "learning"],
|
|
944
|
+
op: learningKeyPresent ? "replace" : "add",
|
|
945
|
+
value: proposedLearning,
|
|
946
|
+
rationale: `advise: learned pool converged (full coverage in band(s) ${ready.join(", ")}) with explorationRate 0 and a non-thompson bandit — pure exploitation goes stale`,
|
|
947
|
+
};
|
|
948
|
+
return [
|
|
949
|
+
{
|
|
950
|
+
id: "pool-stale-exploitation",
|
|
951
|
+
severity: "info",
|
|
952
|
+
summary: "converged learned model_pool never explores — add explorationRate",
|
|
953
|
+
evidence: [
|
|
954
|
+
`full-coverage band(s) at floor ${floor}: ${ready.join(", ")}`,
|
|
955
|
+
`learning.explorationRate is ${learning.explorationRate ?? "unset"} and bandit is ${learning.bandit ?? "epsilon-greedy"}`,
|
|
956
|
+
],
|
|
957
|
+
counts: { readyBands: ready.length },
|
|
958
|
+
suggestion: patchOrAdvice(spec, patch, adviceText),
|
|
959
|
+
},
|
|
960
|
+
];
|
|
961
|
+
};
|
|
801
962
|
export const ADVICE_RULES = [
|
|
802
963
|
ruleRepeatedToolFailures,
|
|
803
964
|
ruleTruncationPressure,
|
|
@@ -807,6 +968,9 @@ export const ADVICE_RULES = [
|
|
|
807
968
|
ruleFailureTaxonomy,
|
|
808
969
|
ruleLoopBreak,
|
|
809
970
|
ruleSubAgentSplit,
|
|
971
|
+
rulePoolPolicyUpgrade,
|
|
972
|
+
rulePoolCandidateDemotion,
|
|
973
|
+
rulePoolStaleExploitation,
|
|
810
974
|
];
|
|
811
975
|
/**
|
|
812
976
|
* Run every rule and rank the findings: warn before info, then by the
|
package/dist/dataset-mine.d.ts
CHANGED
|
@@ -102,8 +102,9 @@ export type EgressBlock = {
|
|
|
102
102
|
};
|
|
103
103
|
/**
|
|
104
104
|
* Extract egress-block candidates from parsed audit records. The audit log MAY
|
|
105
|
-
* carry no `egress_decision` records at all (
|
|
106
|
-
* in that case. A block is any record whose verdict
|
|
105
|
+
* carry no `egress_decision` records at all (they are written only on warn/block
|
|
106
|
+
* verdicts) — this returns [] in that case. A block is any record whose verdict
|
|
107
|
+
* is not an allow. Because
|
|
107
108
|
* the audit payload does not reliably carry a turn input, these become
|
|
108
109
|
* standalone candidates keyed by a best-effort session id + the lineage
|
|
109
110
|
* summary; the CLI attaches them to the matching session's LAST turn input
|
package/dist/dataset-mine.js
CHANGED
|
@@ -173,8 +173,9 @@ export function mineSession(sessionId, events) {
|
|
|
173
173
|
}
|
|
174
174
|
/**
|
|
175
175
|
* Extract egress-block candidates from parsed audit records. The audit log MAY
|
|
176
|
-
* carry no `egress_decision` records at all (
|
|
177
|
-
* in that case. A block is any record whose verdict
|
|
176
|
+
* carry no `egress_decision` records at all (they are written only on warn/block
|
|
177
|
+
* verdicts) — this returns [] in that case. A block is any record whose verdict
|
|
178
|
+
* is not an allow. Because
|
|
178
179
|
* the audit payload does not reliably carry a turn input, these become
|
|
179
180
|
* standalone candidates keyed by a best-effort session id + the lineage
|
|
180
181
|
* summary; the CLI attaches them to the matching session's LAST turn input
|
package/dist/index.js
CHANGED
|
@@ -23,11 +23,16 @@ import { createLogger } from "@crewhaus/logging";
|
|
|
23
23
|
import { McpHost } from "@crewhaus/mcp-host";
|
|
24
24
|
import { captureFacts, createMemoryStore, deriveMemoryDecision, summarizeDurableFacts, } from "@crewhaus/memory-store";
|
|
25
25
|
import { BUILTIN_DEFAULT_RULES, PermissionConfigError, parsePermissionsConfig, tagRules, } from "@crewhaus/permission-engine";
|
|
26
|
+
// Adaptive model routing — `crewhaus route status|reset` inspects/clears the
|
|
27
|
+
// per-(routeKey, model) reward scoreboard behind `agent.model_pool`; advise
|
|
28
|
+
// mines the same scoreboard into pool-policy suggestions.
|
|
29
|
+
import { openScoreboard } from "@crewhaus/routing-store";
|
|
26
30
|
import { runChatLoop } from "@crewhaus/runtime-core";
|
|
27
31
|
import { createSessionStore, evictExpiredSessions } from "@crewhaus/session-store";
|
|
28
32
|
import { createSkillTool, discoverSkills } from "@crewhaus/skills-registry";
|
|
29
33
|
import { loadCommands } from "@crewhaus/slash-commands";
|
|
30
34
|
import { parseSpec } from "@crewhaus/spec";
|
|
35
|
+
import { specHasPath } from "@crewhaus/spec-patch";
|
|
31
36
|
import { spawnSubAgent } from "@crewhaus/sub-agent-spawner";
|
|
32
37
|
import { ToolCatalog } from "@crewhaus/tool-catalog";
|
|
33
38
|
import { registerMcpServer } from "@crewhaus/tool-mcp";
|
|
@@ -260,6 +265,7 @@ import { RetireError, buildRetirementPlan, formatPlan, formatRetirementResult, r
|
|
|
260
265
|
// Item 25 — model right-sizing downshift search core (pure enumeration + cost
|
|
261
266
|
// projection + $/score ranking); side-effect-free so it is unit-testable.
|
|
262
267
|
import { buildRightSizeReport, enumerateSlotCandidates, } from "./right-size";
|
|
268
|
+
import { runRoute } from "./route";
|
|
263
269
|
// Item 13 — `crewhaus scaffold-evals` + `init --with-evals`: day-one eval
|
|
264
270
|
// assets generated from the spec (deterministic template / one-shot model
|
|
265
271
|
// sample stubs + a single spec-goal llm_judge or floor grader), in a
|
|
@@ -453,6 +459,12 @@ const INIT_SCHEMA = {
|
|
|
453
459
|
// Overwrite an existing scaffolded workflow or eval assets (never the
|
|
454
460
|
// spec).
|
|
455
461
|
{ name: "force", takesValue: false },
|
|
462
|
+
// Item 39 — interactive spec authoring: interview the user (via the
|
|
463
|
+
// resolved model, or a scripted stdin questionnaire when no credentials)
|
|
464
|
+
// and emit a parseSpec-validated crewhaus.yaml. Composes with --detect to
|
|
465
|
+
// prefill the model from what's reachable.
|
|
466
|
+
{ name: "interactive", short: "i", takesValue: false },
|
|
467
|
+
{ name: "detect", takesValue: false },
|
|
456
468
|
{ name: "help", short: "h" },
|
|
457
469
|
],
|
|
458
470
|
};
|
|
@@ -491,12 +503,6 @@ const GRADERS_SUGGEST_SCHEMA = {
|
|
|
491
503
|
{ name: "min-score", takesValue: true },
|
|
492
504
|
// Overwrite an existing review file (default: refuse).
|
|
493
505
|
{ name: "force", takesValue: false },
|
|
494
|
-
// Item 39 — interactive spec authoring: interview the user (via the
|
|
495
|
-
// resolved model, or a scripted stdin questionnaire when no credentials)
|
|
496
|
-
// and emit a parseSpec-validated crewhaus.yaml. Composes with --detect to
|
|
497
|
-
// prefill the model from what's reachable.
|
|
498
|
-
{ name: "interactive", short: "i", takesValue: false },
|
|
499
|
-
{ name: "detect", takesValue: false },
|
|
500
506
|
{ name: "help", short: "h" },
|
|
501
507
|
],
|
|
502
508
|
};
|
|
@@ -529,6 +535,17 @@ const DOCTOR_SCHEMA = {
|
|
|
529
535
|
// Container-HEALTHCHECK exit semantics (0 within/no-data, 1 breach).
|
|
530
536
|
{ name: "slo", takesValue: false },
|
|
531
537
|
{ name: "ttft", takesValue: false },
|
|
538
|
+
// Item 40 — `--detect`: read-only inventory of reachable providers, the
|
|
539
|
+
// local Ollama/vLLM endpoint's models, and MCP servers from
|
|
540
|
+
// .mcp.json / Claude Desktop config. `--no-probe` skips the localhost HTTP
|
|
541
|
+
// probe (offline / CI).
|
|
542
|
+
{ name: "detect", takesValue: false },
|
|
543
|
+
{ name: "no-probe", takesValue: false },
|
|
544
|
+
// Item 40 — `--fix`: apply the mechanical remediations doctor otherwise
|
|
545
|
+
// only prints (scaffold crewhaus.yaml, create .crewhaus/, mark outward
|
|
546
|
+
// tools scope:external, append commented .env stubs). Dry-run is the
|
|
547
|
+
// DEFAULT: without --fix, doctor prints the diff it WOULD apply.
|
|
548
|
+
{ name: "fix", takesValue: false },
|
|
532
549
|
{ name: "help", short: "h" },
|
|
533
550
|
],
|
|
534
551
|
};
|
|
@@ -1343,7 +1360,7 @@ function usageText() {
|
|
|
1343
1360
|
" [--watch] re-validate on change (authoring aid; does not re-launch)",
|
|
1344
1361
|
" [--resume <id>] resume a specific session (cli targets only)",
|
|
1345
1362
|
" [--continue] resume the most-recent session (cli targets only)",
|
|
1346
|
-
" [--prompt <text>]
|
|
1363
|
+
" [--prompt <text>] run one turn and exit, printing the reply (cli: no REPL; browser: the single-turn input, else stdin)",
|
|
1347
1364
|
" [--justification-judge rule-based|claude] Pillar 3 intent-gate judge (FR-004)",
|
|
1348
1365
|
" [--egress-matcher substring|semantic] Pillar 3 sink-side matcher (FR-006)",
|
|
1349
1366
|
" [--egress-embedder <model>] embedder for --egress-matcher semantic",
|
|
@@ -1410,6 +1427,9 @@ function usageText() {
|
|
|
1410
1427
|
" context --bundle [-o <file>] emit a single-markdown orientation manifest",
|
|
1411
1428
|
" [--factory-root <p>] [--docs-root <p>] [--demos-root <p>]",
|
|
1412
1429
|
" cost-summary --session <id> summarize cost_accrual events for a session",
|
|
1430
|
+
" route status [--dir <root>] show the adaptive model_pool reward scoreboard",
|
|
1431
|
+
" route explain <session> [--dir <r>] replay a run's per-turn model_route decisions",
|
|
1432
|
+
" route reset [--dir <root>] wipe the scoreboard (default root .crewhaus)",
|
|
1413
1433
|
" advise [--session <id> | --all] mine session logs for spec advice (item 14)",
|
|
1414
1434
|
" [--json] [-o <dir>] writes suggestions.json + report.html (default .crewhaus/advice)",
|
|
1415
1435
|
" tools list list every builtin tool + its metadata (item 18)",
|
|
@@ -2709,6 +2729,8 @@ async function loadToolMap() {
|
|
|
2709
2729
|
glob: fs.glob,
|
|
2710
2730
|
grep: fs.grep,
|
|
2711
2731
|
bash: bash.bash,
|
|
2732
|
+
bashOutput: bash.bashOutput,
|
|
2733
|
+
killShell: bash.killShell,
|
|
2712
2734
|
todoWrite: todo.todoWrite,
|
|
2713
2735
|
webFetch: web.webFetch,
|
|
2714
2736
|
webSearch: web.webSearch,
|
|
@@ -2887,6 +2909,7 @@ async function resolveEgressMatcher(args, irEgressMatcher) {
|
|
|
2887
2909
|
async function runRun(args) {
|
|
2888
2910
|
if (args.flags["help"]) {
|
|
2889
2911
|
process.stdout.write("usage: crewhaus run <spec.yaml> [--model <model>] [--permission-mode <default|plan|auto|bypass>] [--resume <sessionId> | --continue] [--prompt <text>] [--justification-judge rule-based|claude] [--egress-matcher substring|semantic] [--egress-embedder <model>]\n" +
|
|
2912
|
+
" --prompt <text> runs a single turn non-interactively and prints the reply, then exits (no REPL) — for scripting/CI; composes with --resume/--continue\n" +
|
|
2890
2913
|
" --model accepts the full router grammar: claude-* (Anthropic), openai/<m>, gemini/<m>, bedrock/<id> (geo prefixes tolerated), local/<m>@<url>\n" +
|
|
2891
2914
|
" A spec with a feedback: block asks `rate this session? [g]ood / [b]ad / [enter] skip`\n" +
|
|
2892
2915
|
" on clean REPL exit (one keystroke, 10s timeout, TTY only — never when piped). Opt out\n" +
|
|
@@ -2962,6 +2985,19 @@ async function runRunCli(args, ir, specPath) {
|
|
|
2962
2985
|
resumeId = match.id;
|
|
2963
2986
|
process.stdout.write(`[continue] resuming session ${match.id} (last updated ${match.updatedAt})\n`);
|
|
2964
2987
|
}
|
|
2988
|
+
// `--prompt <text>` runs ONE turn non-interactively and exits (no REPL) —
|
|
2989
|
+
// the cli analogue of the browser target's --prompt and `claude -p`, for
|
|
2990
|
+
// scripting/CI/pipelines. The final assistant message is written to stdout.
|
|
2991
|
+
// Composes with --resume/--continue (one more turn on a resumed session).
|
|
2992
|
+
const promptFlag = args.flags["prompt"];
|
|
2993
|
+
let oneShotPrompt;
|
|
2994
|
+
if (typeof promptFlag === "string") {
|
|
2995
|
+
const trimmed = promptFlag.trim();
|
|
2996
|
+
if (trimmed.length === 0) {
|
|
2997
|
+
die("--prompt requires non-empty text");
|
|
2998
|
+
}
|
|
2999
|
+
oneShotPrompt = trimmed;
|
|
3000
|
+
}
|
|
2965
3001
|
let tools = [];
|
|
2966
3002
|
if (ir.tools.length > 0) {
|
|
2967
3003
|
// Section 14 — apply per-tool config (e.g. registerFetchConfig) before
|
|
@@ -3356,8 +3392,9 @@ async function runRunCli(args, ir, specPath) {
|
|
|
3356
3392
|
process.stdout.write(`[sandbox] backend "${process.env["CREWHAUS_SANDBOX"]}" — python/javascript/shell enabled (still require an alwaysAllow rule)\n`);
|
|
3357
3393
|
}
|
|
3358
3394
|
}
|
|
3395
|
+
let oneShotResult;
|
|
3359
3396
|
try {
|
|
3360
|
-
await runChatLoop({
|
|
3397
|
+
oneShotResult = await runChatLoop({
|
|
3361
3398
|
model,
|
|
3362
3399
|
instructions: mcpQuarantineNotice !== undefined
|
|
3363
3400
|
? `${ir.agent.instructions}\n\n${mcpQuarantineNotice}`
|
|
@@ -3370,11 +3407,24 @@ async function runRunCli(args, ir, specPath) {
|
|
|
3370
3407
|
hooks,
|
|
3371
3408
|
skills,
|
|
3372
3409
|
slashCommands,
|
|
3410
|
+
// `--prompt` → run exactly one turn from the seeded prompt and return
|
|
3411
|
+
// its final message (no interactive REPL). runChatLoop's singleTurn
|
|
3412
|
+
// mode also suppresses the `you>` / `agent ready` chrome.
|
|
3413
|
+
...(oneShotPrompt !== undefined
|
|
3414
|
+
? { singleTurn: true, seedMessages: [{ role: "user", content: oneShotPrompt }] }
|
|
3415
|
+
: {}),
|
|
3373
3416
|
...(hasCodeExecTools ? { sandboxAvailable } : {}),
|
|
3374
3417
|
...(subAgents !== undefined ? { subAgents, spawnSubAgent } : {}),
|
|
3375
3418
|
...(ir.target === "cli" && ir.agent.maxTokens !== undefined
|
|
3376
3419
|
? { maxTokens: ir.agent.maxTokens }
|
|
3377
3420
|
: {}),
|
|
3421
|
+
// Thread `compaction.model` so auto-compaction summarizes on the
|
|
3422
|
+
// spec's chosen (typically cheaper) model instead of the primary.
|
|
3423
|
+
// The compiler already resolves the `cheapest` sentinel to a concrete
|
|
3424
|
+
// id, so this is a raw model string. Mirrors the target-cli emitter.
|
|
3425
|
+
...(ir.target === "cli" && ir.compaction.model !== undefined
|
|
3426
|
+
? { compactionModel: ir.compaction.model }
|
|
3427
|
+
: {}),
|
|
3378
3428
|
// Item 22 — provider failover chain: thread `agent.model_fallbacks` +
|
|
3379
3429
|
// `agent.circuit_breaker` through to the runtime, mirroring the
|
|
3380
3430
|
// target-cli codegen path. Skipped when `--model` overrides the
|
|
@@ -3396,6 +3446,16 @@ async function runRunCli(args, ir, specPath) {
|
|
|
3396
3446
|
ir.agent.modelTiers !== undefined
|
|
3397
3447
|
? { modelTiers: ir.agent.modelTiers }
|
|
3398
3448
|
: {}),
|
|
3449
|
+
// Adaptive model routing — thread `agent.model_pool` so `crewhaus run`
|
|
3450
|
+
// routes (and learns) exactly like the compiled cli bundle, not just the
|
|
3451
|
+
// single primary. Mutually exclusive with tiers/fallbacks in the spec;
|
|
3452
|
+
// a `--model` override forces a single model, so the pool applies only
|
|
3453
|
+
// without an override.
|
|
3454
|
+
...(ir.target === "cli" &&
|
|
3455
|
+
typeof modelOverride !== "string" &&
|
|
3456
|
+
ir.agent.modelPool !== undefined
|
|
3457
|
+
? { modelPool: ir.agent.modelPool }
|
|
3458
|
+
: {}),
|
|
3399
3459
|
// Section 55 / item 23 — thread the spec's failure_taxonomy so
|
|
3400
3460
|
// recovery-engine consults the user's named error classes (including
|
|
3401
3461
|
// the `switch-model` verdict) before its built-in flow.
|
|
@@ -3428,6 +3488,12 @@ async function runRunCli(args, ir, specPath) {
|
|
|
3428
3488
|
if (mcpHost)
|
|
3429
3489
|
await mcpHost.disconnectAll();
|
|
3430
3490
|
}
|
|
3491
|
+
// One-shot (`--prompt`): emit the final assistant message to stdout so it
|
|
3492
|
+
// can be piped/captured. The REPL path streams as it goes and returns the
|
|
3493
|
+
// same trailing text, so only print it in one-shot mode.
|
|
3494
|
+
if (oneShotPrompt !== undefined && oneShotResult !== undefined) {
|
|
3495
|
+
process.stdout.write(`${oneShotResult}\n`);
|
|
3496
|
+
}
|
|
3431
3497
|
// Item 1 — post-session feedback teardown: the one-keystroke exit rating
|
|
3432
3498
|
// prompt (TTY only) and the feedback.autoDistill consumer. Runs only on a
|
|
3433
3499
|
// clean REPL exit (runChatLoop returned; a throw above skips it) and is
|
|
@@ -7616,11 +7682,13 @@ async function runAdvise(args) {
|
|
|
7616
7682
|
if (args.flags["help"]) {
|
|
7617
7683
|
process.stdout.write("usage: crewhaus advise [--session <id> | --all] [--json] [-o <dir>]\n" +
|
|
7618
7684
|
"\n" +
|
|
7619
|
-
"mines .crewhaus/sessions (+ .crewhaus/audit) for spec advice:\n" +
|
|
7685
|
+
"mines .crewhaus/sessions (+ .crewhaus/audit + .crewhaus/routing) for spec advice:\n" +
|
|
7620
7686
|
" repeated tool failures, max_tokens truncation pressure, compaction\n" +
|
|
7621
7687
|
" thrash, permission-ask churn, stop-reason anomalies, learned\n" +
|
|
7622
7688
|
" failure_taxonomy + loop-break rules, sub-agent splits under\n" +
|
|
7623
|
-
" chronic context pressure\n" +
|
|
7689
|
+
" chronic context pressure, and model_pool scoreboard mining\n" +
|
|
7690
|
+
" (flip policy to learned once every candidate has enough samples;\n" +
|
|
7691
|
+
" name consistently-losing candidates — roster edits stay human)\n" +
|
|
7624
7692
|
"\n" +
|
|
7625
7693
|
" --session <id> mine one session (default: all sessions)\n" +
|
|
7626
7694
|
" --json print machine-readable findings to stdout\n" +
|
|
@@ -7670,17 +7738,34 @@ async function runAdvise(args) {
|
|
|
7670
7738
|
// The cwd spec enables patch suggestions; without one (or with a broken
|
|
7671
7739
|
// one) rules fall back to advice text — mining must not block on it.
|
|
7672
7740
|
let spec;
|
|
7741
|
+
let specText;
|
|
7673
7742
|
const specPath = join(process.cwd(), "crewhaus.yaml");
|
|
7674
7743
|
if (existsSync(specPath)) {
|
|
7675
7744
|
try {
|
|
7676
|
-
|
|
7745
|
+
specText = readFileSync(specPath, "utf-8");
|
|
7746
|
+
spec = parseSpec(specText);
|
|
7677
7747
|
}
|
|
7678
7748
|
catch (err) {
|
|
7679
7749
|
process.stderr.write(`[advise] crewhaus.yaml did not parse (${err.message}) — patch suggestions downgraded to advice\n`);
|
|
7680
7750
|
}
|
|
7681
7751
|
}
|
|
7682
|
-
|
|
7683
|
-
|
|
7752
|
+
// Adaptive model routing — the pool reward scoreboard, when the harness has
|
|
7753
|
+
// one. A missing/empty store simply produces no routing findings.
|
|
7754
|
+
let routingArms = [];
|
|
7755
|
+
try {
|
|
7756
|
+
routingArms = openScoreboard(join(process.cwd(), ".crewhaus")).snapshot();
|
|
7757
|
+
}
|
|
7758
|
+
catch {
|
|
7759
|
+
// Corrupt store must not block mining the sessions.
|
|
7760
|
+
}
|
|
7761
|
+
const ctx = buildAdviceContext(sessions, auditObjects, routingArms);
|
|
7762
|
+
const specTextForOps = specText;
|
|
7763
|
+
const findings = runAdviceRules(ctx, {
|
|
7764
|
+
...(spec !== undefined ? { spec } : {}),
|
|
7765
|
+
...(spec !== undefined && specTextForOps !== undefined
|
|
7766
|
+
? { specHasPath: (path) => specHasPath(specTextForOps, path) }
|
|
7767
|
+
: {}),
|
|
7768
|
+
});
|
|
7684
7769
|
const generatedAt = new Date().toISOString();
|
|
7685
7770
|
const suggestions = buildSuggestionsFile(findings, ctx.sessionIds, generatedAt);
|
|
7686
7771
|
const outDir = strFlag(args, "out") ?? join(process.cwd(), ".crewhaus", "advice");
|
|
@@ -12949,6 +13034,18 @@ switch (subcommand) {
|
|
|
12949
13034
|
}
|
|
12950
13035
|
break;
|
|
12951
13036
|
}
|
|
13037
|
+
case "route":
|
|
13038
|
+
// Adaptive model routing — inspect/reset the reward scoreboard. `runRoute`
|
|
13039
|
+
// throws a plain Error on a bad argument; surface it through die().
|
|
13040
|
+
try {
|
|
13041
|
+
process.stdout.write(`${runRoute(rest)}\n`);
|
|
13042
|
+
}
|
|
13043
|
+
catch (err) {
|
|
13044
|
+
if (err instanceof Error)
|
|
13045
|
+
die(err.message);
|
|
13046
|
+
throw err;
|
|
13047
|
+
}
|
|
13048
|
+
break;
|
|
12952
13049
|
case "context":
|
|
12953
13050
|
runContext(parseFor(rest, CONTEXT_SCHEMA));
|
|
12954
13051
|
break;
|
package/dist/route.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type ArmStats } from "@crewhaus/routing-store";
|
|
2
|
+
export type RouteArgs = {
|
|
3
|
+
readonly sub: "status" | "reset" | "explain";
|
|
4
|
+
readonly dir: string;
|
|
5
|
+
/** Session id — required for `explain`. */
|
|
6
|
+
readonly session?: string;
|
|
7
|
+
};
|
|
8
|
+
/** Parse `route <sub> [<session>] [--dir <root>]` argv (everything after `route`). */
|
|
9
|
+
export declare function parseRouteArgs(argv: readonly string[]): RouteArgs;
|
|
10
|
+
/** Snapshot the scoreboard arms (sorted routeKey then model). */
|
|
11
|
+
export declare function loadArms(rootDir: string): ArmStats[];
|
|
12
|
+
/**
|
|
13
|
+
* Render the scoreboard as a human table: grouped by routeKey, best mean-reward
|
|
14
|
+
* arm first within each bucket and starred (the arm a `learned` policy would
|
|
15
|
+
* exploit once every arm clears its sample floor).
|
|
16
|
+
*/
|
|
17
|
+
export declare function formatRouteStatus(arms: readonly ArmStats[]): string;
|
|
18
|
+
/** Wipe the whole scoreboard. Returns the number of arms removed. */
|
|
19
|
+
export declare function resetRouting(rootDir: string): number;
|
|
20
|
+
/** One persisted `model_route` decision from a session log. */
|
|
21
|
+
export type RouteDecision = {
|
|
22
|
+
readonly turnNumber?: number;
|
|
23
|
+
readonly routeKey: string;
|
|
24
|
+
readonly model: string;
|
|
25
|
+
readonly policy: string;
|
|
26
|
+
readonly reason: string;
|
|
27
|
+
readonly explored?: boolean;
|
|
28
|
+
readonly policyVersion?: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Read the `model_route` decisions persisted in a session's event log at
|
|
32
|
+
* `<rootDir>/sessions/<sessionId>.jsonl`, in turn order. A missing log yields
|
|
33
|
+
* `[]`; a malformed line is skipped (the log is best-effort observability).
|
|
34
|
+
*/
|
|
35
|
+
export declare function readRouteDecisions(rootDir: string, sessionId: string): RouteDecision[];
|
|
36
|
+
/** Render a run's per-turn routing decisions as a table (turn order). */
|
|
37
|
+
export declare function formatRouteExplain(sessionId: string, decisions: readonly RouteDecision[]): string;
|
|
38
|
+
/** Run `crewhaus route …`, returning the text to print. */
|
|
39
|
+
export declare function runRoute(argv: readonly string[]): string;
|
package/dist/route.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `crewhaus route` — inspect, explain, and reset adaptive model routing
|
|
3
|
+
* (the `agent.model_pool` reward scoreboard + per-turn routing decisions).
|
|
4
|
+
*
|
|
5
|
+
* route status [--dir <root>] show the learned arms, best-per-bucket first
|
|
6
|
+
* route explain <session> [--dir …] replay a run's per-turn model_route decisions
|
|
7
|
+
* route reset [--dir <root>] wipe the scoreboard (kill switch)
|
|
8
|
+
*
|
|
9
|
+
* `--dir` points at the `.crewhaus` root (default `.crewhaus`); the scoreboard
|
|
10
|
+
* lives at `<root>/routing/arms.jsonl` and session logs at
|
|
11
|
+
* `<root>/sessions/<id>.jsonl`. `status` surfaces the ACCUMULATED learning a
|
|
12
|
+
* `learned` policy exploits; `explain` shows WHY each turn of one run picked
|
|
13
|
+
* the model it did (including ε-greedy exploration draws).
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { openScoreboard } from "@crewhaus/routing-store";
|
|
18
|
+
const DEFAULT_ROOT = ".crewhaus";
|
|
19
|
+
/** Parse `route <sub> [<session>] [--dir <root>]` argv (everything after `route`). */
|
|
20
|
+
export function parseRouteArgs(argv) {
|
|
21
|
+
let dir = DEFAULT_ROOT;
|
|
22
|
+
let sub;
|
|
23
|
+
let session;
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
const a = argv[i];
|
|
26
|
+
if (a === undefined)
|
|
27
|
+
continue;
|
|
28
|
+
if (a === "--dir") {
|
|
29
|
+
const v = argv[i + 1];
|
|
30
|
+
if (v === undefined)
|
|
31
|
+
throw new Error("route: --dir requires a path");
|
|
32
|
+
dir = v;
|
|
33
|
+
i++;
|
|
34
|
+
}
|
|
35
|
+
else if (sub === undefined && (a === "status" || a === "reset" || a === "explain")) {
|
|
36
|
+
// A subcommand keyword is only the subcommand when it comes FIRST; after
|
|
37
|
+
// that the same word is a plain positional (so `route explain status`
|
|
38
|
+
// explains a session literally named "status", not runs `status`).
|
|
39
|
+
sub = a;
|
|
40
|
+
}
|
|
41
|
+
else if (sub === "explain" && session === undefined && !a.startsWith("--")) {
|
|
42
|
+
session = a; // the session id positional (only `explain` takes one)
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
throw new Error(`route: unknown argument "${a}" (expected: status | reset | explain <session> [--dir <root>])`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (sub === undefined) {
|
|
49
|
+
throw new Error("route: expected a subcommand (status | explain <session> | reset)");
|
|
50
|
+
}
|
|
51
|
+
if (sub === "explain" && session === undefined) {
|
|
52
|
+
throw new Error("route explain: a <session> id is required");
|
|
53
|
+
}
|
|
54
|
+
return { sub, dir, ...(session !== undefined ? { session } : {}) };
|
|
55
|
+
}
|
|
56
|
+
/** Snapshot the scoreboard arms (sorted routeKey then model). */
|
|
57
|
+
export function loadArms(rootDir) {
|
|
58
|
+
return openScoreboard(rootDir).snapshot();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Render the scoreboard as a human table: grouped by routeKey, best mean-reward
|
|
62
|
+
* arm first within each bucket and starred (the arm a `learned` policy would
|
|
63
|
+
* exploit once every arm clears its sample floor).
|
|
64
|
+
*/
|
|
65
|
+
export function formatRouteStatus(arms) {
|
|
66
|
+
if (arms.length === 0) {
|
|
67
|
+
return "No routing data yet. Run a harness whose spec declares `agent.model_pool` to accumulate arms.";
|
|
68
|
+
}
|
|
69
|
+
const byKey = new Map();
|
|
70
|
+
for (const a of arms) {
|
|
71
|
+
const g = byKey.get(a.routeKey) ?? [];
|
|
72
|
+
g.push(a);
|
|
73
|
+
byKey.set(a.routeKey, g);
|
|
74
|
+
}
|
|
75
|
+
const lines = [];
|
|
76
|
+
lines.push(`${"routeKey".padEnd(9)} ${"model".padEnd(28)} ${"n".padStart(5)} ${"reward".padStart(7)} ${"latency".padStart(9)} ${"cost".padStart(10)}`);
|
|
77
|
+
for (const key of [...byKey.keys()].sort()) {
|
|
78
|
+
const group = (byKey.get(key) ?? []).slice().sort((x, y) => y.meanReward - x.meanReward);
|
|
79
|
+
group.forEach((a, i) => {
|
|
80
|
+
const star = i === 0 && a.n > 0 ? " *" : "";
|
|
81
|
+
const latency = `${a.meanLatencyMs.toFixed(0)}ms`;
|
|
82
|
+
const cost = a.meanCostUsd > 0 ? `$${a.meanCostUsd.toFixed(5)}` : "-";
|
|
83
|
+
lines.push(`${key.padEnd(9)} ${a.model.padEnd(28)} ${String(a.n).padStart(5)} ${a.meanReward.toFixed(3).padStart(7)} ${latency.padStart(9)} ${cost.padStart(10)}${star}`);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
lines.push("");
|
|
87
|
+
lines.push("* = current best arm in its bucket (what a `learned` policy exploits once every arm clears its sample floor).");
|
|
88
|
+
return lines.join("\n");
|
|
89
|
+
}
|
|
90
|
+
/** Wipe the whole scoreboard. Returns the number of arms removed. */
|
|
91
|
+
export function resetRouting(rootDir) {
|
|
92
|
+
const removed = loadArms(rootDir).length;
|
|
93
|
+
const path = join(rootDir, "routing", "arms.jsonl");
|
|
94
|
+
if (existsSync(path))
|
|
95
|
+
rmSync(path, { force: true });
|
|
96
|
+
return removed;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Read the `model_route` decisions persisted in a session's event log at
|
|
100
|
+
* `<rootDir>/sessions/<sessionId>.jsonl`, in turn order. A missing log yields
|
|
101
|
+
* `[]`; a malformed line is skipped (the log is best-effort observability).
|
|
102
|
+
*/
|
|
103
|
+
export function readRouteDecisions(rootDir, sessionId) {
|
|
104
|
+
// Guard against a hostile session id escaping the sessions dir (e.g.
|
|
105
|
+
// `../../etc/passwd`). Real ids are `sess_<hex>`; anything with a path
|
|
106
|
+
// separator or `..` is rejected outright.
|
|
107
|
+
if (/[/\\]/.test(sessionId) || sessionId.includes("..") || sessionId.length === 0) {
|
|
108
|
+
throw new Error(`route explain: invalid session id "${sessionId}"`);
|
|
109
|
+
}
|
|
110
|
+
const path = join(rootDir, "sessions", `${sessionId}.jsonl`);
|
|
111
|
+
if (!existsSync(path))
|
|
112
|
+
return [];
|
|
113
|
+
const out = [];
|
|
114
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
115
|
+
const trimmed = line.trim();
|
|
116
|
+
if (trimmed.length === 0)
|
|
117
|
+
continue;
|
|
118
|
+
let rec;
|
|
119
|
+
try {
|
|
120
|
+
rec = JSON.parse(trimmed);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (rec.kind !== "model_route" || typeof rec.payload !== "object" || rec.payload === null) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const p = rec.payload;
|
|
129
|
+
if (typeof p["routeKey"] !== "string" || typeof p["model"] !== "string")
|
|
130
|
+
continue;
|
|
131
|
+
out.push({
|
|
132
|
+
...(typeof p["turnNumber"] === "number" ? { turnNumber: p["turnNumber"] } : {}),
|
|
133
|
+
routeKey: p["routeKey"],
|
|
134
|
+
model: p["model"],
|
|
135
|
+
policy: typeof p["policy"] === "string" ? p["policy"] : "?",
|
|
136
|
+
reason: typeof p["reason"] === "string" ? p["reason"] : "",
|
|
137
|
+
...(typeof p["explored"] === "boolean" ? { explored: p["explored"] } : {}),
|
|
138
|
+
...(typeof p["policyVersion"] === "string" ? { policyVersion: p["policyVersion"] } : {}),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
/** Render a run's per-turn routing decisions as a table (turn order). */
|
|
144
|
+
export function formatRouteExplain(sessionId, decisions) {
|
|
145
|
+
if (decisions.length === 0) {
|
|
146
|
+
return `No model_route decisions recorded for session ${sessionId}. (Only runs whose spec declares \`agent.model_pool\` persist routing decisions.)`;
|
|
147
|
+
}
|
|
148
|
+
const lines = [`session ${sessionId} — ${decisions.length} routing decision(s):`, ""];
|
|
149
|
+
lines.push(`${"turn".padStart(4)} ${"band".padEnd(5)} ${"model".padEnd(28)} ${"policy".padEnd(10)} ${"pick".padEnd(9)} reason`);
|
|
150
|
+
for (const d of decisions) {
|
|
151
|
+
const turn = d.turnNumber !== undefined ? String(d.turnNumber) : "-";
|
|
152
|
+
const pick = d.explored ? "explore" : "exploit";
|
|
153
|
+
lines.push(`${turn.padStart(4)} ${d.routeKey.padEnd(5)} ${d.model.padEnd(28)} ${d.policy.padEnd(10)} ${pick.padEnd(9)} ${d.reason}`);
|
|
154
|
+
}
|
|
155
|
+
return lines.join("\n");
|
|
156
|
+
}
|
|
157
|
+
/** Run `crewhaus route …`, returning the text to print. */
|
|
158
|
+
export function runRoute(argv) {
|
|
159
|
+
const args = parseRouteArgs(argv);
|
|
160
|
+
if (args.sub === "status")
|
|
161
|
+
return formatRouteStatus(loadArms(args.dir));
|
|
162
|
+
if (args.sub === "explain") {
|
|
163
|
+
const session = args.session; // parseRouteArgs guarantees it for explain
|
|
164
|
+
return formatRouteExplain(session, readRouteDecisions(args.dir, session));
|
|
165
|
+
}
|
|
166
|
+
const removed = resetRouting(args.dir);
|
|
167
|
+
return `Reset routing scoreboard at ${join(args.dir, "routing", "arms.jsonl")} (${removed} arm${removed === 1 ? "" : "s"} removed).`;
|
|
168
|
+
}
|
|
@@ -73,7 +73,8 @@ export type SecurityDigest = {
|
|
|
73
73
|
readonly malformedLines: number;
|
|
74
74
|
readonly countsByKind: Readonly<Record<string, number>>;
|
|
75
75
|
/** Kinds @crewhaus/audit-log declares that produced 0 window records —
|
|
76
|
-
*
|
|
76
|
+
* always includes the still-writerless kinds, plus any writerful kind
|
|
77
|
+
* (e.g. egress_decision) that simply had no records in the window. */
|
|
77
78
|
readonly absentDeclaredKinds: ReadonlyArray<string>;
|
|
78
79
|
};
|
|
79
80
|
/** Pillar 3 intent gate — from `permission_justification_evaluated`. */
|
package/dist/security-digest.js
CHANGED
|
@@ -25,6 +25,12 @@ import { join, resolve } from "node:path";
|
|
|
25
25
|
* - policy_decision policy-engine's auditPolicyDecision (payload
|
|
26
26
|
* { toolName, sideEffect, decision, reason, tenantId, matchedRule }) —
|
|
27
27
|
* deny/audit-and-allow rows only by default.
|
|
28
|
+
* - egress_decision runtime-core's egress classifier (AUTOMATION-
|
|
29
|
+
* OPPORTUNITIES item 20; payload { sinkId, sinkScope, verdict,
|
|
30
|
+
* originsFound, matchCount }) — appended durably on every NON-PASS
|
|
31
|
+
* (warn/block) verdict when the egress audit sink is wired, so the
|
|
32
|
+
* rollup carries warn/block egress history; pass verdicts stay
|
|
33
|
+
* ephemeral on the trace bus and leave no audit record.
|
|
28
34
|
* - model_call, gateway_request, secrets_rotation, secrets_access,
|
|
29
35
|
* deployment_action, retention_enforcement, alert_raised (item-31
|
|
30
36
|
* alert watchdog's `AlertAuditSink`, see alert-sink.ts; payload
|
|
@@ -33,15 +39,14 @@ import { join, resolve } from "node:path";
|
|
|
33
39
|
* but builds no verdict rollup from them.
|
|
34
40
|
*
|
|
35
41
|
* kinds DECLARED in @crewhaus/audit-log but with NO writer anywhere in the
|
|
36
|
-
* tree today: `
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* reads as "no durable writer yet", not "no egress happened".
|
|
42
|
+
* tree today: `tool_classification`, `session_fork`, `tenancy_context`. The
|
|
43
|
+
* digest still aggregates records of these kinds by their documented payload
|
|
44
|
+
* shape so the rollup lights up the day a writer lands, and reports them
|
|
45
|
+
* under `absentDeclaredKinds` so "0 rows" reads as "no durable writer yet",
|
|
46
|
+
* not "nothing happened". (egress_decision graduated to the writer list
|
|
47
|
+
* above once runtime-core began appending it on warn/block verdicts; a
|
|
48
|
+
* window with no warn/block egress still lists it as absent — meaning "no
|
|
49
|
+
* warn/block egress in the window", not "no writer".)
|
|
45
50
|
*
|
|
46
51
|
* Because block-tier verdicts DO leave durable residue in the session event
|
|
47
52
|
* logs (`.crewhaus/sessions/<id>.jsonl` `tool_result` events carry the
|
|
@@ -210,7 +215,8 @@ export function buildSecurityDigest(opts) {
|
|
|
210
215
|
let jDenied = 0;
|
|
211
216
|
const deniedTools = new Map();
|
|
212
217
|
const judges = new Map();
|
|
213
|
-
// Egress rollup accumulators
|
|
218
|
+
// Egress rollup accumulators — fed by egress_decision records (written on
|
|
219
|
+
// warn/block verdicts; see module header).
|
|
214
220
|
let ePassed = 0;
|
|
215
221
|
let eWarned = 0;
|
|
216
222
|
let eBlocked = 0;
|
|
@@ -480,7 +486,7 @@ export function renderSecurityDigestText(d) {
|
|
|
480
486
|
lines.push(` • origin ${o.name}: ${o.count} hit(s)`);
|
|
481
487
|
}
|
|
482
488
|
else {
|
|
483
|
-
lines.push("egress: no
|
|
489
|
+
lines.push("egress: no egress_decision records in window — records are written on warn/block verdicts; live per-run verdicts via CREWHAUS_SECURITY_DIGEST=1");
|
|
484
490
|
}
|
|
485
491
|
lines.push(`policy engine: ${d.policy.decisions} audited decision(s) — ${d.policy.denied} denied, ${d.policy.auditAndAllow} audit-and-allow`);
|
|
486
492
|
for (const t of d.policy.topDeniedTools)
|
|
@@ -576,7 +582,7 @@ ${tableOf(["Tool", "Denials", "Judges", "Mean confidence", "Last reason"], d.jus
|
|
|
576
582
|
${tableOf(["Judge", "Evaluated", "Denied"], d.justification.byJudge.map((j) => [j.judgeModel, String(j.evaluated), String(j.denied)]))}
|
|
577
583
|
<h2>Egress sinks (warn/block)</h2>
|
|
578
584
|
${d.egress.decisions === 0
|
|
579
|
-
? '<p class="note">no
|
|
585
|
+
? '<p class="note">no egress_decision records in window — records are written on warn/block verdicts; live per-run verdicts via CREWHAUS_SECURITY_DIGEST=1</p>'
|
|
580
586
|
: tableOf(["Sink", "Warned", "Blocked", "Origins"], d.egress.topSinks.map((s) => [
|
|
581
587
|
s.sinkId,
|
|
582
588
|
String(s.warned),
|
package/dist/tools-cli.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crewhaus",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CrewHaus — the meta-harness compiler for AI agents. Compile one crewhaus.yaml spec into a CLI agent, channel bot, RAG pipeline, multi-agent crew, eval harness, voice or browser agent, and more.",
|
|
6
6
|
"keywords": [
|
|
@@ -31,93 +31,94 @@
|
|
|
31
31
|
"test": "bun test src"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@crewhaus/adapter-anthropic": "0.2.
|
|
35
|
-
"@crewhaus/agent-context-isolation": "0.2.
|
|
36
|
-
"@crewhaus/audit-log": "0.2.
|
|
37
|
-
"@crewhaus/eval-optimizer-orchestrator": "0.2.
|
|
38
|
-
"@crewhaus/prompt-optimizer": "0.2.
|
|
39
|
-
"@crewhaus/prompt-optimizer-claude": "0.2.
|
|
40
|
-
"@crewhaus/justification-judge-claude": "0.2.
|
|
41
|
-
"@crewhaus/spec-patch": "0.2.
|
|
42
|
-
"@crewhaus/canary-controller": "0.2.
|
|
43
|
-
"@crewhaus/compiler": "0.2.
|
|
44
|
-
"@crewhaus/computer-use-driver": "0.2.
|
|
45
|
-
"@crewhaus/context-bundle": "0.2.
|
|
46
|
-
"@crewhaus/cost-tracker": "0.2.
|
|
47
|
-
"@crewhaus/data-retention-engine": "0.2.
|
|
48
|
-
"@crewhaus/dataset-registry": "0.2.
|
|
49
|
-
"@crewhaus/deployment-controller": "0.2.
|
|
50
|
-
"@crewhaus/egress-classifier": "0.2.
|
|
51
|
-
"@crewhaus/egress-matcher-semantic": "0.2.
|
|
52
|
-
"@crewhaus/embedder": "0.2.
|
|
53
|
-
"@crewhaus/errors": "0.2.
|
|
54
|
-
"@crewhaus/eval-dataset": "0.2.
|
|
55
|
-
"@crewhaus/eval-grader": "0.2.
|
|
56
|
-
"@crewhaus/eval-judge": "0.2.
|
|
57
|
-
"@crewhaus/eval-report": "0.2.
|
|
58
|
-
"@crewhaus/eval-runner": "0.2.
|
|
59
|
-
"@crewhaus/event-log": "0.2.
|
|
60
|
-
"@crewhaus/gateway-server": "0.2.
|
|
61
|
-
"@crewhaus/grader-safety-classifiers": "0.2.
|
|
62
|
-
"@crewhaus/hooks-engine": "0.2.
|
|
63
|
-
"@crewhaus/infra-utils": "0.2.
|
|
64
|
-
"@crewhaus/ir": "0.2.
|
|
65
|
-
"@crewhaus/ir-passes": "0.2.
|
|
66
|
-
"@crewhaus/logging": "0.2.
|
|
67
|
-
"@crewhaus/mcp-host": "0.2.
|
|
68
|
-
"@crewhaus/memory-store": "0.2.
|
|
69
|
-
"@crewhaus/model-router": "0.2.
|
|
70
|
-
"@crewhaus/module-marketplace-client": "0.2.
|
|
71
|
-
"@crewhaus/migration-engine": "0.2.
|
|
72
|
-
"@crewhaus/migration-runner": "0.2.
|
|
73
|
-
"@crewhaus/plugin-registry": "0.2.
|
|
74
|
-
"@crewhaus/plugin-sdk": "0.2.
|
|
75
|
-
"@crewhaus/permission-engine": "0.2.
|
|
76
|
-
"@crewhaus/pii-redactor": "0.2.
|
|
77
|
-
"@crewhaus/prompt-injection-detector": "0.2.
|
|
78
|
-
"@crewhaus/regression-runner": "0.2.
|
|
79
|
-
"@crewhaus/
|
|
80
|
-
"@crewhaus/
|
|
81
|
-
"@crewhaus/
|
|
82
|
-
"@crewhaus/
|
|
83
|
-
"@crewhaus/
|
|
84
|
-
"@crewhaus/
|
|
85
|
-
"@crewhaus/
|
|
86
|
-
"@crewhaus/
|
|
87
|
-
"@crewhaus/
|
|
88
|
-
"@crewhaus/
|
|
89
|
-
"@crewhaus/
|
|
90
|
-
"@crewhaus/template-
|
|
91
|
-
"@crewhaus/
|
|
92
|
-
"@crewhaus/
|
|
93
|
-
"@crewhaus/
|
|
94
|
-
"@crewhaus/tool-
|
|
95
|
-
"@crewhaus/
|
|
96
|
-
"@crewhaus/target-
|
|
97
|
-
"@crewhaus/
|
|
98
|
-
"@crewhaus/tool-
|
|
99
|
-
"@crewhaus/tool-
|
|
100
|
-
"@crewhaus/tool-
|
|
101
|
-
"@crewhaus/tool-
|
|
102
|
-
"@crewhaus/tool-
|
|
103
|
-
"@crewhaus/tool-image
|
|
104
|
-
"@crewhaus/tool-
|
|
105
|
-
"@crewhaus/tool-
|
|
106
|
-
"@crewhaus/tool-
|
|
107
|
-
"@crewhaus/tool-
|
|
108
|
-
"@crewhaus/tool-
|
|
109
|
-
"@crewhaus/tool-
|
|
110
|
-
"@crewhaus/tool-
|
|
111
|
-
"@crewhaus/tool-
|
|
112
|
-
"@crewhaus/tool-
|
|
113
|
-
"@crewhaus/tool-
|
|
114
|
-
"@crewhaus/tool-
|
|
115
|
-
"@crewhaus/
|
|
116
|
-
"@crewhaus/
|
|
117
|
-
"@crewhaus/
|
|
118
|
-
"@crewhaus/
|
|
119
|
-
"@crewhaus/sandbox
|
|
120
|
-
"@crewhaus/
|
|
34
|
+
"@crewhaus/adapter-anthropic": "0.2.2",
|
|
35
|
+
"@crewhaus/agent-context-isolation": "0.2.2",
|
|
36
|
+
"@crewhaus/audit-log": "0.2.2",
|
|
37
|
+
"@crewhaus/eval-optimizer-orchestrator": "0.2.2",
|
|
38
|
+
"@crewhaus/prompt-optimizer": "0.2.2",
|
|
39
|
+
"@crewhaus/prompt-optimizer-claude": "0.2.2",
|
|
40
|
+
"@crewhaus/justification-judge-claude": "0.2.2",
|
|
41
|
+
"@crewhaus/spec-patch": "0.2.2",
|
|
42
|
+
"@crewhaus/canary-controller": "0.2.2",
|
|
43
|
+
"@crewhaus/compiler": "0.2.2",
|
|
44
|
+
"@crewhaus/computer-use-driver": "0.2.2",
|
|
45
|
+
"@crewhaus/context-bundle": "0.2.2",
|
|
46
|
+
"@crewhaus/cost-tracker": "0.2.2",
|
|
47
|
+
"@crewhaus/data-retention-engine": "0.2.2",
|
|
48
|
+
"@crewhaus/dataset-registry": "0.2.2",
|
|
49
|
+
"@crewhaus/deployment-controller": "0.2.2",
|
|
50
|
+
"@crewhaus/egress-classifier": "0.2.2",
|
|
51
|
+
"@crewhaus/egress-matcher-semantic": "0.2.2",
|
|
52
|
+
"@crewhaus/embedder": "0.2.2",
|
|
53
|
+
"@crewhaus/errors": "0.2.2",
|
|
54
|
+
"@crewhaus/eval-dataset": "0.2.2",
|
|
55
|
+
"@crewhaus/eval-grader": "0.2.2",
|
|
56
|
+
"@crewhaus/eval-judge": "0.2.2",
|
|
57
|
+
"@crewhaus/eval-report": "0.2.2",
|
|
58
|
+
"@crewhaus/eval-runner": "0.2.2",
|
|
59
|
+
"@crewhaus/event-log": "0.2.2",
|
|
60
|
+
"@crewhaus/gateway-server": "0.2.2",
|
|
61
|
+
"@crewhaus/grader-safety-classifiers": "0.2.2",
|
|
62
|
+
"@crewhaus/hooks-engine": "0.2.2",
|
|
63
|
+
"@crewhaus/infra-utils": "0.2.2",
|
|
64
|
+
"@crewhaus/ir": "0.2.2",
|
|
65
|
+
"@crewhaus/ir-passes": "0.2.2",
|
|
66
|
+
"@crewhaus/logging": "0.2.2",
|
|
67
|
+
"@crewhaus/mcp-host": "0.2.2",
|
|
68
|
+
"@crewhaus/memory-store": "0.2.2",
|
|
69
|
+
"@crewhaus/model-router": "0.2.2",
|
|
70
|
+
"@crewhaus/module-marketplace-client": "0.2.2",
|
|
71
|
+
"@crewhaus/migration-engine": "0.2.2",
|
|
72
|
+
"@crewhaus/migration-runner": "0.2.2",
|
|
73
|
+
"@crewhaus/plugin-registry": "0.2.2",
|
|
74
|
+
"@crewhaus/plugin-sdk": "0.2.2",
|
|
75
|
+
"@crewhaus/permission-engine": "0.2.2",
|
|
76
|
+
"@crewhaus/pii-redactor": "0.2.2",
|
|
77
|
+
"@crewhaus/prompt-injection-detector": "0.2.2",
|
|
78
|
+
"@crewhaus/regression-runner": "0.2.2",
|
|
79
|
+
"@crewhaus/routing-store": "0.2.2",
|
|
80
|
+
"@crewhaus/run-context": "0.2.2",
|
|
81
|
+
"@crewhaus/runtime-core": "0.2.2",
|
|
82
|
+
"@crewhaus/secrets-manager": "0.2.2",
|
|
83
|
+
"@crewhaus/session-store": "0.2.2",
|
|
84
|
+
"@crewhaus/spec-registry": "0.2.2",
|
|
85
|
+
"@crewhaus/skills-registry": "0.2.2",
|
|
86
|
+
"@crewhaus/slash-commands": "0.2.2",
|
|
87
|
+
"@crewhaus/smoke-harness": "0.2.2",
|
|
88
|
+
"@crewhaus/spec": "0.2.2",
|
|
89
|
+
"@crewhaus/sub-agent-spawner": "0.2.2",
|
|
90
|
+
"@crewhaus/template-marketplace-client": "0.2.2",
|
|
91
|
+
"@crewhaus/template-registry": "0.2.2",
|
|
92
|
+
"@crewhaus/tenancy": "0.2.2",
|
|
93
|
+
"@crewhaus/trace-event-bus": "0.2.2",
|
|
94
|
+
"@crewhaus/tool-bash": "0.2.2",
|
|
95
|
+
"@crewhaus/tool-builder": "0.2.2",
|
|
96
|
+
"@crewhaus/target-cli": "0.2.2",
|
|
97
|
+
"@crewhaus/target-eval-bundle": "0.2.2",
|
|
98
|
+
"@crewhaus/tool-catalog": "0.2.2",
|
|
99
|
+
"@crewhaus/tool-code-execution": "0.2.2",
|
|
100
|
+
"@crewhaus/tool-codegraph": "0.2.2",
|
|
101
|
+
"@crewhaus/tool-fetch": "0.2.2",
|
|
102
|
+
"@crewhaus/tool-fs": "0.2.2",
|
|
103
|
+
"@crewhaus/tool-image": "0.2.2",
|
|
104
|
+
"@crewhaus/tool-image-generation": "0.2.2",
|
|
105
|
+
"@crewhaus/tool-document-ingest": "0.2.2",
|
|
106
|
+
"@crewhaus/tool-mcp": "0.2.2",
|
|
107
|
+
"@crewhaus/tool-memory": "0.2.2",
|
|
108
|
+
"@crewhaus/tool-mouse-keyboard": "0.2.2",
|
|
109
|
+
"@crewhaus/tool-navigate": "0.2.2",
|
|
110
|
+
"@crewhaus/tool-permission-matcher": "0.2.2",
|
|
111
|
+
"@crewhaus/tool-screen-capture": "0.2.2",
|
|
112
|
+
"@crewhaus/tool-task": "0.2.2",
|
|
113
|
+
"@crewhaus/tool-todo": "0.2.2",
|
|
114
|
+
"@crewhaus/tool-vision-grounding": "0.2.2",
|
|
115
|
+
"@crewhaus/tool-web": "0.2.2",
|
|
116
|
+
"@crewhaus/docker-images": "0.2.2",
|
|
117
|
+
"@crewhaus/crewhaus-cloud": "0.2.2",
|
|
118
|
+
"@crewhaus/federation-discovery": "0.2.2",
|
|
119
|
+
"@crewhaus/sandbox": "0.2.2",
|
|
120
|
+
"@crewhaus/sandbox-image-registry": "0.2.2",
|
|
121
|
+
"@crewhaus/compliance-controls": "0.2.2",
|
|
121
122
|
"yaml": "^2.6.0"
|
|
122
123
|
},
|
|
123
124
|
"devDependencies": {
|