@crewhaus/spec 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1253 -60
- package/dist/index.js +257 -11
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1110,7 +1110,8 @@ const continuityBlock = z.union([z.boolean(), continuityObject]).optional();
|
|
|
1110
1110
|
/**
|
|
1111
1111
|
* v0.3.0 Goal 3 (§4.1) — the top-level `thredz:` block: ONE knob that flips
|
|
1112
1112
|
* the memory fabric's wiki backend to a hosted Thredz wiki over the published
|
|
1113
|
-
* `thredz-mcp` stdio server (npm, v0.
|
|
1113
|
+
* `thredz-mcp` stdio server (npm, v0.3.0 — 27 tools incl. `goal_*`/`task_*`
|
|
1114
|
+
* and the `wiki_space_*` pair).
|
|
1114
1115
|
*
|
|
1115
1116
|
* Forms:
|
|
1116
1117
|
* - boolean shorthand: `thredz: true` ≡ `{ api_key: "$THREDZ_API_KEY" }`
|
|
@@ -1142,6 +1143,22 @@ const thredzObject = z
|
|
|
1142
1143
|
api_key: z.string().min(1),
|
|
1143
1144
|
base_url: z.string().url().optional(),
|
|
1144
1145
|
visibility: z.enum(["private", "shared"]).optional(),
|
|
1146
|
+
/**
|
|
1147
|
+
* 0.5.0 — a Thredz **wiki space** (Pro/Scale) to scope this agent's memory
|
|
1148
|
+
* to; becomes the synthesized server's `THREDZ_DEFAULT_SPACE`. A `shared`
|
|
1149
|
+
* space is readable by every wiki-enabled key on the account; an
|
|
1150
|
+
* `individual` space only by the key that owns it.
|
|
1151
|
+
*
|
|
1152
|
+
* The space TYPE is chosen when the space is created (over the API, or via
|
|
1153
|
+
* the model-callable `wiki_space_create`), not here — so this cannot be
|
|
1154
|
+
* validated at compile time and `visibility` is not cross-checked against
|
|
1155
|
+
* it. Inside a space the space's type decides visibility outright.
|
|
1156
|
+
*
|
|
1157
|
+
* ONE individual space per API KEY is a hard Thredz limit, so per-agent
|
|
1158
|
+
* private memory means a per-agent `api_key`. That is one `thredz:` block
|
|
1159
|
+
* per spec today; a crew gives each role its own (see the role-keyed form).
|
|
1160
|
+
*/
|
|
1161
|
+
space: z.string().min(1).optional(),
|
|
1145
1162
|
goals: z.boolean().optional(),
|
|
1146
1163
|
agents: z
|
|
1147
1164
|
.union([
|
|
@@ -1162,6 +1179,48 @@ const thredzObject = z
|
|
|
1162
1179
|
})
|
|
1163
1180
|
.strict();
|
|
1164
1181
|
const thredzBlock = z.union([z.boolean(), z.string().min(1), thredzObject]).optional();
|
|
1182
|
+
/**
|
|
1183
|
+
* 0.5.0 — the CREW-ONLY superset of {@link thredzObject}. `roles` fans the
|
|
1184
|
+
* block out per role so each role can carry its OWN `api_key` and its own
|
|
1185
|
+
* `space`; every other field at this level is the DEFAULT a role inherits and
|
|
1186
|
+
* may override.
|
|
1187
|
+
*
|
|
1188
|
+
* WHY THE MAP LIVES HERE, under `thredz.roles.<role>`, and NOT on the role as
|
|
1189
|
+
* `roles.<role>.thredz` — this is load-bearing, not taste. Two security
|
|
1190
|
+
* surfaces prefix-match on `["thredz"]`:
|
|
1191
|
+
*
|
|
1192
|
+
* - `@crewhaus/spec-patch`'s `OPTIMIZABLE_PATHS.crew` allows `["roles"]`
|
|
1193
|
+
* (whole-role replacement) and matches by PREFIX. Under `roles.*`, a
|
|
1194
|
+
* role's `api_key` would become optimizer-reachable and
|
|
1195
|
+
* `optimize --write-back` could rewrite a credential.
|
|
1196
|
+
* - the hangar's spec editor denies the `["thredz"]` prefix outright
|
|
1197
|
+
* ("thredz crosses the harness boundary to a hosted wiki"). Under
|
|
1198
|
+
* `roles.*`, `api_key` would be editable from a browser.
|
|
1199
|
+
*
|
|
1200
|
+
* Keeping the fan-out under `thredz.` inherits both protections with zero
|
|
1201
|
+
* code change. The `^thredz:` header/badge regexes keep matching too.
|
|
1202
|
+
*
|
|
1203
|
+
* `api_key` is optional ONLY here: a pure fan-out crew gives every role its
|
|
1204
|
+
* own key and needs no crew-wide one. The refinement below enforces that at
|
|
1205
|
+
* least one of the two exists.
|
|
1206
|
+
*/
|
|
1207
|
+
const crewThredzObject = thredzObject
|
|
1208
|
+
.extend({
|
|
1209
|
+
api_key: z.string().min(1).optional(),
|
|
1210
|
+
roles: z.record(safeName, thredzObject.partial({ api_key: true })).optional(),
|
|
1211
|
+
})
|
|
1212
|
+
.strict()
|
|
1213
|
+
.superRefine((t, ctx) => {
|
|
1214
|
+
const fanOut = t.roles !== undefined && Object.keys(t.roles).length > 0;
|
|
1215
|
+
if (!fanOut && t.api_key === undefined) {
|
|
1216
|
+
ctx.addIssue({
|
|
1217
|
+
code: "custom",
|
|
1218
|
+
message: "thredz.api_key is required unless thredz.roles gives each role its own key (one individual wiki space per API key is a hard Thredz limit, so per-role private memory needs per-role keys)",
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1222
|
+
/** The crew mount: the shorthands still work, plus the role-keyed superset. */
|
|
1223
|
+
const crewThredzBlock = z.union([z.boolean(), z.string().min(1), crewThredzObject]).optional();
|
|
1165
1224
|
/**
|
|
1166
1225
|
* v0.3.0 Goal 2 (§3.3, PR 17) — the top-level `learning:` block: continual
|
|
1167
1226
|
* learning as a first-class capability. Presence (with `enabled` not `false`)
|
|
@@ -1325,6 +1384,57 @@ const observabilityBlock = z
|
|
|
1325
1384
|
})
|
|
1326
1385
|
.strict()
|
|
1327
1386
|
.optional();
|
|
1387
|
+
/**
|
|
1388
|
+
* "Watch me" — the sampled phase-2 judge pass of `crewhaus watchme report`,
|
|
1389
|
+
* the ONE model-spending analysis phase. Absent ⇒ deterministic-only reports
|
|
1390
|
+
* (the field defaults below still resolve at lower time, so a budgeted judge
|
|
1391
|
+
* always names a model).
|
|
1392
|
+
*/
|
|
1393
|
+
const watchmeJudgeBlock = z
|
|
1394
|
+
.object({
|
|
1395
|
+
/** Judge model for the sampled phase-2 quality pass. Refused at runtime
|
|
1396
|
+
* if unpriced (dream-engine pattern) — the budget cap must be
|
|
1397
|
+
* enforceable. */
|
|
1398
|
+
model: z.string().min(1).default("claude-haiku-4-5"),
|
|
1399
|
+
/** Fraction of ungraded turns escalated to the judge. */
|
|
1400
|
+
sample_rate: z.number().min(0).max(1).default(0.15),
|
|
1401
|
+
/** Per-report spend cap. 0 (default) = deterministic-only reports. */
|
|
1402
|
+
budget_usd: z.number().min(0).default(0),
|
|
1403
|
+
})
|
|
1404
|
+
.strict();
|
|
1405
|
+
/**
|
|
1406
|
+
* "Watch me" — observe this harness's interactions and learn from them
|
|
1407
|
+
* (design/watch-me.md). Presence turns on the live capture tap; `crewhaus
|
|
1408
|
+
* watchme report` distills the watched sessions post-hoc. Carried on the
|
|
1409
|
+
* three interactive-loop shapes (cli, channel, managed); the strict unions
|
|
1410
|
+
* reject the key loudly elsewhere (research/crew are a named deferral,
|
|
1411
|
+
* design/watch-me.md §13.1).
|
|
1412
|
+
*
|
|
1413
|
+
* Deliberately a SIBLING of `observability:`, not a sub-key of it:
|
|
1414
|
+
* observability controls the generic telemetry subscribers (ring buffer,
|
|
1415
|
+
* printers, metrics, cost, alerts, otel) while watchme is a learning feature
|
|
1416
|
+
* with its own durable store and spec-synthesis outputs. Capture is
|
|
1417
|
+
* INDEPENDENT of `observability.trace.level` — that knob controls the ring
|
|
1418
|
+
* buffer + printers only, never the watchme tap.
|
|
1419
|
+
*
|
|
1420
|
+
* Every knob defaults, so a bare `watchme: {}` is a complete declaration.
|
|
1421
|
+
* NO `watchme.*` path is optimizer-tunable — see the exclusion note beside
|
|
1422
|
+
* OPTIMIZABLE_PATHS in `@crewhaus/spec-patch`.
|
|
1423
|
+
*/
|
|
1424
|
+
const watchmeBlock = z
|
|
1425
|
+
.object({
|
|
1426
|
+
enabled: z.boolean().default(true),
|
|
1427
|
+
/** "full" = write the .events.jsonl trace sibling; "mirrors" = rely on the
|
|
1428
|
+
* default-on advisor mirrors only (retro-analysis grade, no extra file). */
|
|
1429
|
+
capture: z.enum(["full", "mirrors"]).default("full"),
|
|
1430
|
+
judge: watchmeJudgeBlock.optional(),
|
|
1431
|
+
/** "user" additionally registers this harness in the global registry at run time. */
|
|
1432
|
+
scope: z.enum(["harness", "user"]).default("harness"),
|
|
1433
|
+
/** Publish redacted distilled findings to the wiki/Thredz at report time. */
|
|
1434
|
+
share: z.boolean().default(false),
|
|
1435
|
+
})
|
|
1436
|
+
.strict()
|
|
1437
|
+
.optional();
|
|
1328
1438
|
/**
|
|
1329
1439
|
* Section 47 — blockchain subsystem blocks (cross-cutting). Any shape may
|
|
1330
1440
|
* declare any subset of `chains` / `wallets` / `contracts` /
|
|
@@ -1396,10 +1506,13 @@ const contractsBlock = z.array(contractBindingSchema).optional();
|
|
|
1396
1506
|
const transactionPolicyBlock = transactionPolicySchema.optional();
|
|
1397
1507
|
/**
|
|
1398
1508
|
* Phase 3 §3.3 — CLI banner with optional tagline rotation. When set,
|
|
1399
|
-
*
|
|
1400
|
-
*
|
|
1401
|
-
*
|
|
1402
|
-
*
|
|
1509
|
+
* BOTH cli surfaces print this banner on cold start — the compiled
|
|
1510
|
+
* bundle and `crewhaus run` (which used to ignore the block entirely,
|
|
1511
|
+
* making an authored banner invisible to anyone who ran the spec
|
|
1512
|
+
* directly). Suppressed under `--resume` / `--continue` so resumed
|
|
1513
|
+
* sessions don't re-banner, and under `CREWHAUS_RESUMED=1` for a
|
|
1514
|
+
* wrapper re-invoking a compiled bundle. Static mode picks the first
|
|
1515
|
+
* tagline; random mode picks one uniformly per startup.
|
|
1403
1516
|
*/
|
|
1404
1517
|
const cliBannerBlock = z
|
|
1405
1518
|
.object({
|
|
@@ -1602,6 +1715,8 @@ const cliSchema = z
|
|
|
1602
1715
|
thredz: thredzBlock,
|
|
1603
1716
|
learning: learningBlock,
|
|
1604
1717
|
observability: observabilityBlock,
|
|
1718
|
+
// "Watch me" — observe-and-learn (sibling of observability, see watchmeBlock).
|
|
1719
|
+
watchme: watchmeBlock,
|
|
1605
1720
|
cli: cliOptionsBlock,
|
|
1606
1721
|
chains: chainsBlock,
|
|
1607
1722
|
wallets: walletsBlock,
|
|
@@ -1710,6 +1825,15 @@ const whatsappChannelSchema = z
|
|
|
1710
1825
|
phoneNumberId: z.string().min(1),
|
|
1711
1826
|
accessToken: z.string().min(1),
|
|
1712
1827
|
appSecret: z.string().min(1),
|
|
1828
|
+
// The token Meta presents on the GET callback-URL verification handshake
|
|
1829
|
+
// (`hub.verify_token`). Optional: a daemon serving an already-verified
|
|
1830
|
+
// subscription does not need it, and without it the handshake fails
|
|
1831
|
+
// closed rather than echoing an unauthenticated challenge.
|
|
1832
|
+
verifyToken: z
|
|
1833
|
+
.string()
|
|
1834
|
+
.min(1)
|
|
1835
|
+
.optional()
|
|
1836
|
+
.describe("shared token echoed back on Meta's GET callback-URL verification handshake; required to verify a new webhook subscription"),
|
|
1713
1837
|
})
|
|
1714
1838
|
.strict();
|
|
1715
1839
|
const imessageChannelSchema = z
|
|
@@ -1792,6 +1916,8 @@ const channelSchema = z
|
|
|
1792
1916
|
thredz: thredzBlock,
|
|
1793
1917
|
learning: learningBlock,
|
|
1794
1918
|
observability: observabilityBlock,
|
|
1919
|
+
// "Watch me" — observe-and-learn (sibling of observability, see watchmeBlock).
|
|
1920
|
+
watchme: watchmeBlock,
|
|
1795
1921
|
heartbeat: heartbeatBlock,
|
|
1796
1922
|
// Loop contract 0.4 (Batch F) — cron/interval wake trigger (the general
|
|
1797
1923
|
// temporal surface beside the interval-only `heartbeat`).
|
|
@@ -1818,9 +1944,28 @@ const graphNodeSchema = z
|
|
|
1818
1944
|
tools: z.array(z.string().min(1)).optional(),
|
|
1819
1945
|
tool_config: toolConfigBlock,
|
|
1820
1946
|
/**
|
|
1821
|
-
*
|
|
1822
|
-
*
|
|
1823
|
-
*
|
|
1947
|
+
* A human approval gate on this node, and a PRE-condition: the node
|
|
1948
|
+
* calls `ctx.requestApproval(prompt)` BEFORE its model turn, so the
|
|
1949
|
+
* prompt is answered against the UPSTREAM state (which the `hitl_pause`
|
|
1950
|
+
* event and the bundle's pause report both print) and no tokens are
|
|
1951
|
+
* spent until the human answers. The engine pauses, persists a
|
|
1952
|
+
* checkpoint, and waits for `resume(checkpointId, decision)` from the
|
|
1953
|
+
* operator/CLI; the resumed run replays this node from the top and
|
|
1954
|
+
* makes its FIRST model call.
|
|
1955
|
+
*
|
|
1956
|
+
* The decision string is recorded at `state["<node>_decision"]`, which
|
|
1957
|
+
* every downstream node reads as part of the upstream state. (NOTE:
|
|
1958
|
+
* `edges[].when.key` cannot name it yet — that key must name a declared
|
|
1959
|
+
* node; see the `graphEdgeWhenSchema` note below.) A rejecting decision
|
|
1960
|
+
* — `reject`, `no`, `deny`, `decline`, `abort`, `cancel`, `stop`, `veto`
|
|
1961
|
+
* (trimmed, case-insensitive) — cancels this node's turn entirely, so
|
|
1962
|
+
* the node records only its decision and no output; any other string,
|
|
1963
|
+
* including free text, approves it. To halt the run on a rejection,
|
|
1964
|
+
* guard the node's outgoing edge with `when: { key: <node>, exists:
|
|
1965
|
+
* true }` — a cancelled node records no output, so no edge matches.
|
|
1966
|
+
*
|
|
1967
|
+
* To have a human approve a node's OWN output, put the gate on the
|
|
1968
|
+
* DOWNSTREAM node: its upstream state is exactly that output.
|
|
1824
1969
|
*/
|
|
1825
1970
|
hitl: z
|
|
1826
1971
|
.object({
|
|
@@ -1857,8 +2002,16 @@ const graphAnyNodeSchema = z.union([graphNodeSchema, graphJudgeNodeSchema]);
|
|
|
1857
2002
|
* - `equals` — take the edge when `state[key] === equals` (string/number/
|
|
1858
2003
|
* boolean strict equality).
|
|
1859
2004
|
* - `exists: true` — take the edge when `state[key] !== undefined` (the
|
|
1860
|
-
* node has produced output
|
|
1861
|
-
*
|
|
2005
|
+
* node has produced output — which, for a `hitl:` node, is FALSE when
|
|
2006
|
+
* the operator rejected the gate, since a rejected node records only
|
|
2007
|
+
* `state["<node>_decision"]`).
|
|
2008
|
+
*
|
|
2009
|
+
* GAP (unchanged by the pre-condition HITL fix): `key` may not yet name a
|
|
2010
|
+
* hitl node's `<node>_decision` record — the cross-check below pins it to a
|
|
2011
|
+
* declared node name, so a rejection can be observed via `exists` on the
|
|
2012
|
+
* node itself but not matched on the decision string. Widening it means
|
|
2013
|
+
* touching the three mirrored checks (this one, ir-passes' graph
|
|
2014
|
+
* wellformedness, target-graph's validateGraph).
|
|
1862
2015
|
*
|
|
1863
2016
|
* Lowered to `IrGraphEdge.when` and emitted as a graph-engine
|
|
1864
2017
|
* `EdgeCondition` (`(state) => state[key] === equals` / `!== undefined`).
|
|
@@ -1979,6 +2132,23 @@ const managedSchema = z
|
|
|
1979
2132
|
expose: exposeBlock,
|
|
1980
2133
|
// Loop contract 0.4 (Batch B, G02) — in-loop output evaluation.
|
|
1981
2134
|
evaluation: evaluationBlock,
|
|
2135
|
+
// NEW-inloop-coverage — human-rating capture on the GATEWAY shape.
|
|
2136
|
+
//
|
|
2137
|
+
// WHAT MANAGED SUPPORTS: the daemon serves a `feedback.submit` JSON-RPC
|
|
2138
|
+
// method (params = the user-supplied FeedbackRecord subset) that appends
|
|
2139
|
+
// a standard record to `.crewhaus/feedback/<tenant>.jsonl` — the exact
|
|
2140
|
+
// sink `crewhaus distill` / `optimize --ratings` / `judge calibrate`
|
|
2141
|
+
// already read; and `autoDistill: true` registers the janitor step that
|
|
2142
|
+
// turns those ratings into versioned `<name>-ratings` registry datasets
|
|
2143
|
+
// on the daemon's own clock (D39), because a gateway daemon never runs a
|
|
2144
|
+
// `crewhaus run` teardown.
|
|
2145
|
+
//
|
|
2146
|
+
// WHAT IT CANNOT SUPPORT: `exitPrompt` is meaningless here (there is no
|
|
2147
|
+
// REPL to exit — the compiler warns when it is set), and
|
|
2148
|
+
// `channelReactions` is the channel shape's own inbound-reaction gate
|
|
2149
|
+
// (also warned). Both parse for schema uniformity across shapes rather
|
|
2150
|
+
// than being silently honoured.
|
|
2151
|
+
feedback: feedbackBlock,
|
|
1982
2152
|
memory: memoryBlock,
|
|
1983
2153
|
// Loop contract 0.4 (Batch E, G22) — agent-shape RAG over doc sources.
|
|
1984
2154
|
knowledge: knowledgeBlock,
|
|
@@ -1986,6 +2156,9 @@ const managedSchema = z
|
|
|
1986
2156
|
thredz: thredzBlock,
|
|
1987
2157
|
learning: learningBlock,
|
|
1988
2158
|
observability: observabilityBlock,
|
|
2159
|
+
// "Watch me" — observe-and-learn (sibling of observability, see watchmeBlock).
|
|
2160
|
+
// Parse + lower ONLY on this shape in v1: compile() warns accepted-but-unwired.
|
|
2161
|
+
watchme: watchmeBlock,
|
|
1989
2162
|
// Loop contract 0.4 (Batch F) — cron/interval wake trigger.
|
|
1990
2163
|
schedule: scheduleBlock,
|
|
1991
2164
|
})
|
|
@@ -2136,7 +2309,7 @@ const crewSchema = z
|
|
|
2136
2309
|
// surface, §2.7).
|
|
2137
2310
|
memory: memoryBlock,
|
|
2138
2311
|
continuity: continuityBlock,
|
|
2139
|
-
thredz:
|
|
2312
|
+
thredz: crewThredzBlock,
|
|
2140
2313
|
learning: learningBlock,
|
|
2141
2314
|
// Loop contract 0.4 (Batch C, G26) — crew joins the observability-carrying
|
|
2142
2315
|
// shapes (cli/channel/managed): the orchestrator's cost/trace/metrics/
|
|
@@ -2287,6 +2460,22 @@ const browserDriverSchema = z
|
|
|
2287
2460
|
.strict()
|
|
2288
2461
|
.default({ width: 1280, height: 720 }),
|
|
2289
2462
|
startUrl: z.string().url().optional(),
|
|
2463
|
+
/**
|
|
2464
|
+
* SECURITY — opt in to private/loopback navigation targets. Default false:
|
|
2465
|
+
* the Navigate tool refuses private/loopback/link-local/metadata hosts
|
|
2466
|
+
* before `driver.goto`, and the chromium backend routes every request
|
|
2467
|
+
* through a DNS-pinning proxy that refuses the same floor at the
|
|
2468
|
+
* connection layer. Together they stop a prompt-injected page from
|
|
2469
|
+
* reaching the host's own services.
|
|
2470
|
+
*
|
|
2471
|
+
* Set true ONLY when the browser legitimately must reach a private target
|
|
2472
|
+
* the operator controls AND the page content is trusted — an intranet app
|
|
2473
|
+
* under test, or a locally-served fixture page (what the browser runtime
|
|
2474
|
+
* smoke does). It relaxes BOTH layers for this spec, so it stays a
|
|
2475
|
+
* per-spec reviewed decision and never a global switch. The http/https
|
|
2476
|
+
* scheme allowlist is NOT waived.
|
|
2477
|
+
*/
|
|
2478
|
+
allowPrivateTargets: z.boolean().default(false),
|
|
2290
2479
|
})
|
|
2291
2480
|
.strict();
|
|
2292
2481
|
const browserSchema = z
|
|
@@ -2517,6 +2706,48 @@ function crossFieldIssues(data) {
|
|
|
2517
2706
|
if (roleNames.length > 0 && !roleNames.includes(data.entry)) {
|
|
2518
2707
|
custom(["entry"], `crew.entry "${data.entry}" must name one of crew.roles (got: ${roleNames.join(", ")})`);
|
|
2519
2708
|
}
|
|
2709
|
+
// 0.5.0 — the role-keyed thredz fan-out. Both checks are cross-field, so
|
|
2710
|
+
// they cannot live in the schema: `thredz.roles` and `roles` are siblings.
|
|
2711
|
+
const crewThredz = data.thredz;
|
|
2712
|
+
if (typeof crewThredz === "object" && crewThredz !== null && "roles" in crewThredz) {
|
|
2713
|
+
const fanOut = crewThredz.roles;
|
|
2714
|
+
const inheritedKey = crewThredz.api_key;
|
|
2715
|
+
if (fanOut !== undefined) {
|
|
2716
|
+
for (const name of Object.keys(fanOut)) {
|
|
2717
|
+
if (!roleNames.includes(name)) {
|
|
2718
|
+
custom(["thredz", "roles", name], `thredz.roles["${name}"]: no such role — crew.roles declares ${roleNames.join(", ")}`);
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
// Every role must resolve to a key: its own, or the inherited one. A
|
|
2722
|
+
// role with neither would silently get NO hosted wiki while its
|
|
2723
|
+
// siblings got one, which is the kind of gap you find in production.
|
|
2724
|
+
if (inheritedKey === undefined) {
|
|
2725
|
+
for (const name of roleNames) {
|
|
2726
|
+
if (fanOut[name]?.api_key === undefined) {
|
|
2727
|
+
custom(["thredz", "roles", name], `role "${name}" has no Thredz api_key and thredz.api_key is not set — give it one, or set a crew-wide thredz.api_key for the roles that share a wiki`);
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
// Two role names that slugify to the same MCP server name would make
|
|
2732
|
+
// one silently overwrite the other's server entry.
|
|
2733
|
+
const slugs = new Map();
|
|
2734
|
+
for (const name of Object.keys(fanOut)) {
|
|
2735
|
+
const slug = name
|
|
2736
|
+
.toLowerCase()
|
|
2737
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
2738
|
+
.replace(/^-+|-+$/g, "");
|
|
2739
|
+
if (slug === "") {
|
|
2740
|
+
custom(["thredz", "roles", name], `thredz.roles["${name}"]: the role name has no characters usable in an MCP server name — rename the role`);
|
|
2741
|
+
continue;
|
|
2742
|
+
}
|
|
2743
|
+
const clash = slugs.get(slug);
|
|
2744
|
+
if (clash !== undefined) {
|
|
2745
|
+
custom(["thredz", "roles", name], `thredz.roles["${name}"] and thredz.roles["${clash}"] both reduce to the MCP server name "thredz-${slug}" — rename one`);
|
|
2746
|
+
}
|
|
2747
|
+
slugs.set(slug, name);
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2520
2751
|
if (data.routing !== undefined && data.routing.kind === "match" && data.routing.match) {
|
|
2521
2752
|
for (const [from, rules] of Object.entries(data.routing.match)) {
|
|
2522
2753
|
if (!roleNames.includes(from)) {
|
|
@@ -2597,6 +2828,21 @@ function crossFieldIssues(data) {
|
|
|
2597
2828
|
custom(["retrieve", "collection"], `pipeline retrieve.vectorBackend "${vectorBackend}" requires retrieve.collection`);
|
|
2598
2829
|
}
|
|
2599
2830
|
}
|
|
2831
|
+
// "Watch me" (design/watch-me.md §4.2) — `watchme.share: true` publishes
|
|
2832
|
+
// co-learning articles, so it conflicts with a thredz OBJECT that declares
|
|
2833
|
+
// an EXPLICIT `visibility: "private"`. The boolean/string shorthands
|
|
2834
|
+
// (default-private) get NO issue in v1 — publishing then lands
|
|
2835
|
+
// private-visibility articles, legal single-agent behaviour — and an
|
|
2836
|
+
// absent `thredz:` block is fine (publish degrades to the local wiki
|
|
2837
|
+
// store, a feature not an error).
|
|
2838
|
+
if (data.target === "cli" || data.target === "channel" || data.target === "managed") {
|
|
2839
|
+
const thredz = data.thredz;
|
|
2840
|
+
if (data.watchme?.share === true &&
|
|
2841
|
+
typeof thredz === "object" &&
|
|
2842
|
+
thredz.visibility === "private") {
|
|
2843
|
+
custom(["watchme", "share"], "watchme.share publishes co-learning articles; thredz.visibility: private blocks cross-agent sharing — set visibility: shared or drop watchme.share");
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2600
2846
|
return issues;
|
|
2601
2847
|
}
|
|
2602
2848
|
export function parseSpec(yamlText) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/spec",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "User-facing spec schema (Zod) + YAML parser",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"test": "bun test src"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@crewhaus/errors": "0.
|
|
18
|
+
"@crewhaus/errors": "0.5.0",
|
|
19
19
|
"yaml": "^2.6.0",
|
|
20
20
|
"zod": "^3.23.8",
|
|
21
21
|
"zod-to-json-schema": "^3.23.5"
|