@crewhaus/spec 0.4.0 → 0.4.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/index.js CHANGED
@@ -1325,6 +1325,57 @@ const observabilityBlock = z
1325
1325
  })
1326
1326
  .strict()
1327
1327
  .optional();
1328
+ /**
1329
+ * "Watch me" — the sampled phase-2 judge pass of `crewhaus watchme report`,
1330
+ * the ONE model-spending analysis phase. Absent ⇒ deterministic-only reports
1331
+ * (the field defaults below still resolve at lower time, so a budgeted judge
1332
+ * always names a model).
1333
+ */
1334
+ const watchmeJudgeBlock = z
1335
+ .object({
1336
+ /** Judge model for the sampled phase-2 quality pass. Refused at runtime
1337
+ * if unpriced (dream-engine pattern) — the budget cap must be
1338
+ * enforceable. */
1339
+ model: z.string().min(1).default("claude-haiku-4-5"),
1340
+ /** Fraction of ungraded turns escalated to the judge. */
1341
+ sample_rate: z.number().min(0).max(1).default(0.15),
1342
+ /** Per-report spend cap. 0 (default) = deterministic-only reports. */
1343
+ budget_usd: z.number().min(0).default(0),
1344
+ })
1345
+ .strict();
1346
+ /**
1347
+ * "Watch me" — observe this harness's interactions and learn from them
1348
+ * (design/watch-me.md). Presence turns on the live capture tap; `crewhaus
1349
+ * watchme report` distills the watched sessions post-hoc. Carried on the
1350
+ * three interactive-loop shapes (cli, channel, managed); the strict unions
1351
+ * reject the key loudly elsewhere (research/crew are a named deferral,
1352
+ * design/watch-me.md §13.1).
1353
+ *
1354
+ * Deliberately a SIBLING of `observability:`, not a sub-key of it:
1355
+ * observability controls the generic telemetry subscribers (ring buffer,
1356
+ * printers, metrics, cost, alerts, otel) while watchme is a learning feature
1357
+ * with its own durable store and spec-synthesis outputs. Capture is
1358
+ * INDEPENDENT of `observability.trace.level` — that knob controls the ring
1359
+ * buffer + printers only, never the watchme tap.
1360
+ *
1361
+ * Every knob defaults, so a bare `watchme: {}` is a complete declaration.
1362
+ * NO `watchme.*` path is optimizer-tunable — see the exclusion note beside
1363
+ * OPTIMIZABLE_PATHS in `@crewhaus/spec-patch`.
1364
+ */
1365
+ const watchmeBlock = z
1366
+ .object({
1367
+ enabled: z.boolean().default(true),
1368
+ /** "full" = write the .events.jsonl trace sibling; "mirrors" = rely on the
1369
+ * default-on advisor mirrors only (retro-analysis grade, no extra file). */
1370
+ capture: z.enum(["full", "mirrors"]).default("full"),
1371
+ judge: watchmeJudgeBlock.optional(),
1372
+ /** "user" additionally registers this harness in the global registry at run time. */
1373
+ scope: z.enum(["harness", "user"]).default("harness"),
1374
+ /** Publish redacted distilled findings to the wiki/Thredz at report time. */
1375
+ share: z.boolean().default(false),
1376
+ })
1377
+ .strict()
1378
+ .optional();
1328
1379
  /**
1329
1380
  * Section 47 — blockchain subsystem blocks (cross-cutting). Any shape may
1330
1381
  * declare any subset of `chains` / `wallets` / `contracts` /
@@ -1396,10 +1447,13 @@ const contractsBlock = z.array(contractBindingSchema).optional();
1396
1447
  const transactionPolicyBlock = transactionPolicySchema.optional();
1397
1448
  /**
1398
1449
  * Phase 3 §3.3 — CLI banner with optional tagline rotation. When set,
1399
- * the compiled cli-target bundle prints this banner on cold start
1400
- * (suppressed under `--resume` / `--continue` so resumed sessions
1401
- * don't re-banner). Static mode picks the first tagline; random mode
1402
- * picks one uniformly per startup.
1450
+ * BOTH cli surfaces print this banner on cold start — the compiled
1451
+ * bundle and `crewhaus run` (which used to ignore the block entirely,
1452
+ * making an authored banner invisible to anyone who ran the spec
1453
+ * directly). Suppressed under `--resume` / `--continue` so resumed
1454
+ * sessions don't re-banner, and under `CREWHAUS_RESUMED=1` for a
1455
+ * wrapper re-invoking a compiled bundle. Static mode picks the first
1456
+ * tagline; random mode picks one uniformly per startup.
1403
1457
  */
1404
1458
  const cliBannerBlock = z
1405
1459
  .object({
@@ -1602,6 +1656,8 @@ const cliSchema = z
1602
1656
  thredz: thredzBlock,
1603
1657
  learning: learningBlock,
1604
1658
  observability: observabilityBlock,
1659
+ // "Watch me" — observe-and-learn (sibling of observability, see watchmeBlock).
1660
+ watchme: watchmeBlock,
1605
1661
  cli: cliOptionsBlock,
1606
1662
  chains: chainsBlock,
1607
1663
  wallets: walletsBlock,
@@ -1710,6 +1766,15 @@ const whatsappChannelSchema = z
1710
1766
  phoneNumberId: z.string().min(1),
1711
1767
  accessToken: z.string().min(1),
1712
1768
  appSecret: z.string().min(1),
1769
+ // The token Meta presents on the GET callback-URL verification handshake
1770
+ // (`hub.verify_token`). Optional: a daemon serving an already-verified
1771
+ // subscription does not need it, and without it the handshake fails
1772
+ // closed rather than echoing an unauthenticated challenge.
1773
+ verifyToken: z
1774
+ .string()
1775
+ .min(1)
1776
+ .optional()
1777
+ .describe("shared token echoed back on Meta's GET callback-URL verification handshake; required to verify a new webhook subscription"),
1713
1778
  })
1714
1779
  .strict();
1715
1780
  const imessageChannelSchema = z
@@ -1792,6 +1857,8 @@ const channelSchema = z
1792
1857
  thredz: thredzBlock,
1793
1858
  learning: learningBlock,
1794
1859
  observability: observabilityBlock,
1860
+ // "Watch me" — observe-and-learn (sibling of observability, see watchmeBlock).
1861
+ watchme: watchmeBlock,
1795
1862
  heartbeat: heartbeatBlock,
1796
1863
  // Loop contract 0.4 (Batch F) — cron/interval wake trigger (the general
1797
1864
  // temporal surface beside the interval-only `heartbeat`).
@@ -1818,9 +1885,28 @@ const graphNodeSchema = z
1818
1885
  tools: z.array(z.string().min(1)).optional(),
1819
1886
  tool_config: toolConfigBlock,
1820
1887
  /**
1821
- * When true, the node calls `ctx.requestApproval(prompt)` before
1822
- * returning. The engine pauses, persists a checkpoint, and waits for
1823
- * `resume(checkpointId, decision)` from the operator/CLI.
1888
+ * A human approval gate on this node, and a PRE-condition: the node
1889
+ * calls `ctx.requestApproval(prompt)` BEFORE its model turn, so the
1890
+ * prompt is answered against the UPSTREAM state (which the `hitl_pause`
1891
+ * event and the bundle's pause report both print) and no tokens are
1892
+ * spent until the human answers. The engine pauses, persists a
1893
+ * checkpoint, and waits for `resume(checkpointId, decision)` from the
1894
+ * operator/CLI; the resumed run replays this node from the top and
1895
+ * makes its FIRST model call.
1896
+ *
1897
+ * The decision string is recorded at `state["<node>_decision"]`, which
1898
+ * every downstream node reads as part of the upstream state. (NOTE:
1899
+ * `edges[].when.key` cannot name it yet — that key must name a declared
1900
+ * node; see the `graphEdgeWhenSchema` note below.) A rejecting decision
1901
+ * — `reject`, `no`, `deny`, `decline`, `abort`, `cancel`, `stop`, `veto`
1902
+ * (trimmed, case-insensitive) — cancels this node's turn entirely, so
1903
+ * the node records only its decision and no output; any other string,
1904
+ * including free text, approves it. To halt the run on a rejection,
1905
+ * guard the node's outgoing edge with `when: { key: <node>, exists:
1906
+ * true }` — a cancelled node records no output, so no edge matches.
1907
+ *
1908
+ * To have a human approve a node's OWN output, put the gate on the
1909
+ * DOWNSTREAM node: its upstream state is exactly that output.
1824
1910
  */
1825
1911
  hitl: z
1826
1912
  .object({
@@ -1857,8 +1943,16 @@ const graphAnyNodeSchema = z.union([graphNodeSchema, graphJudgeNodeSchema]);
1857
1943
  * - `equals` — take the edge when `state[key] === equals` (string/number/
1858
1944
  * boolean strict equality).
1859
1945
  * - `exists: true` — take the edge when `state[key] !== undefined` (the
1860
- * node has produced output; pairs with hitl `_decision` gating in a
1861
- * follow-up).
1946
+ * node has produced output which, for a `hitl:` node, is FALSE when
1947
+ * the operator rejected the gate, since a rejected node records only
1948
+ * `state["<node>_decision"]`).
1949
+ *
1950
+ * GAP (unchanged by the pre-condition HITL fix): `key` may not yet name a
1951
+ * hitl node's `<node>_decision` record — the cross-check below pins it to a
1952
+ * declared node name, so a rejection can be observed via `exists` on the
1953
+ * node itself but not matched on the decision string. Widening it means
1954
+ * touching the three mirrored checks (this one, ir-passes' graph
1955
+ * wellformedness, target-graph's validateGraph).
1862
1956
  *
1863
1957
  * Lowered to `IrGraphEdge.when` and emitted as a graph-engine
1864
1958
  * `EdgeCondition` (`(state) => state[key] === equals` / `!== undefined`).
@@ -1979,6 +2073,23 @@ const managedSchema = z
1979
2073
  expose: exposeBlock,
1980
2074
  // Loop contract 0.4 (Batch B, G02) — in-loop output evaluation.
1981
2075
  evaluation: evaluationBlock,
2076
+ // NEW-inloop-coverage — human-rating capture on the GATEWAY shape.
2077
+ //
2078
+ // WHAT MANAGED SUPPORTS: the daemon serves a `feedback.submit` JSON-RPC
2079
+ // method (params = the user-supplied FeedbackRecord subset) that appends
2080
+ // a standard record to `.crewhaus/feedback/<tenant>.jsonl` — the exact
2081
+ // sink `crewhaus distill` / `optimize --ratings` / `judge calibrate`
2082
+ // already read; and `autoDistill: true` registers the janitor step that
2083
+ // turns those ratings into versioned `<name>-ratings` registry datasets
2084
+ // on the daemon's own clock (D39), because a gateway daemon never runs a
2085
+ // `crewhaus run` teardown.
2086
+ //
2087
+ // WHAT IT CANNOT SUPPORT: `exitPrompt` is meaningless here (there is no
2088
+ // REPL to exit — the compiler warns when it is set), and
2089
+ // `channelReactions` is the channel shape's own inbound-reaction gate
2090
+ // (also warned). Both parse for schema uniformity across shapes rather
2091
+ // than being silently honoured.
2092
+ feedback: feedbackBlock,
1982
2093
  memory: memoryBlock,
1983
2094
  // Loop contract 0.4 (Batch E, G22) — agent-shape RAG over doc sources.
1984
2095
  knowledge: knowledgeBlock,
@@ -1986,6 +2097,9 @@ const managedSchema = z
1986
2097
  thredz: thredzBlock,
1987
2098
  learning: learningBlock,
1988
2099
  observability: observabilityBlock,
2100
+ // "Watch me" — observe-and-learn (sibling of observability, see watchmeBlock).
2101
+ // Parse + lower ONLY on this shape in v1: compile() warns accepted-but-unwired.
2102
+ watchme: watchmeBlock,
1989
2103
  // Loop contract 0.4 (Batch F) — cron/interval wake trigger.
1990
2104
  schedule: scheduleBlock,
1991
2105
  })
@@ -2287,6 +2401,22 @@ const browserDriverSchema = z
2287
2401
  .strict()
2288
2402
  .default({ width: 1280, height: 720 }),
2289
2403
  startUrl: z.string().url().optional(),
2404
+ /**
2405
+ * SECURITY — opt in to private/loopback navigation targets. Default false:
2406
+ * the Navigate tool refuses private/loopback/link-local/metadata hosts
2407
+ * before `driver.goto`, and the chromium backend routes every request
2408
+ * through a DNS-pinning proxy that refuses the same floor at the
2409
+ * connection layer. Together they stop a prompt-injected page from
2410
+ * reaching the host's own services.
2411
+ *
2412
+ * Set true ONLY when the browser legitimately must reach a private target
2413
+ * the operator controls AND the page content is trusted — an intranet app
2414
+ * under test, or a locally-served fixture page (what the browser runtime
2415
+ * smoke does). It relaxes BOTH layers for this spec, so it stays a
2416
+ * per-spec reviewed decision and never a global switch. The http/https
2417
+ * scheme allowlist is NOT waived.
2418
+ */
2419
+ allowPrivateTargets: z.boolean().default(false),
2290
2420
  })
2291
2421
  .strict();
2292
2422
  const browserSchema = z
@@ -2597,6 +2727,21 @@ function crossFieldIssues(data) {
2597
2727
  custom(["retrieve", "collection"], `pipeline retrieve.vectorBackend "${vectorBackend}" requires retrieve.collection`);
2598
2728
  }
2599
2729
  }
2730
+ // "Watch me" (design/watch-me.md §4.2) — `watchme.share: true` publishes
2731
+ // co-learning articles, so it conflicts with a thredz OBJECT that declares
2732
+ // an EXPLICIT `visibility: "private"`. The boolean/string shorthands
2733
+ // (default-private) get NO issue in v1 — publishing then lands
2734
+ // private-visibility articles, legal single-agent behaviour — and an
2735
+ // absent `thredz:` block is fine (publish degrades to the local wiki
2736
+ // store, a feature not an error).
2737
+ if (data.target === "cli" || data.target === "channel" || data.target === "managed") {
2738
+ const thredz = data.thredz;
2739
+ if (data.watchme?.share === true &&
2740
+ typeof thredz === "object" &&
2741
+ thredz.visibility === "private") {
2742
+ custom(["watchme", "share"], "watchme.share publishes co-learning articles; thredz.visibility: private blocks cross-agent sharing — set visibility: shared or drop watchme.share");
2743
+ }
2744
+ }
2600
2745
  return issues;
2601
2746
  }
2602
2747
  export function parseSpec(yamlText) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewhaus/spec",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
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.4.0",
18
+ "@crewhaus/errors": "0.4.2",
19
19
  "yaml": "^2.6.0",
20
20
  "zod": "^3.23.8",
21
21
  "zod-to-json-schema": "^3.23.5"