@directive-run/claude-plugin 1.19.1 → 1.19.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directive-run/claude-plugin",
3
- "version": "1.19.1",
3
+ "version": "1.19.3",
4
4
  "description": "Claude Code plugin for Directive — 12 skills covering modules, constraints, resolvers, derivations, AI orchestration, and adapters. Installable via Claude Code's plugin marketplace or consumable programmatically as an npm package.",
5
5
  "license": "(MIT OR Apache-2.0)",
6
6
  "author": "Jason Comes",
@@ -52,7 +52,7 @@
52
52
  "tsx": "^4.19.2",
53
53
  "typescript": "^5.7.2",
54
54
  "vitest": "^3.0.0",
55
- "@directive-run/knowledge": "1.19.1"
55
+ "@directive-run/knowledge": "1.19.3"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsx scripts/build-skills.ts && tsup",
@@ -189,11 +189,13 @@ publish closure (if any) receives lifecycle events.
189
189
  > holder from two places (e.g., a Worker that mounts one Directive
190
190
  > system per tenant DO), a module-level `let publishRef` would be
191
191
  > SHARED — the second tenant's `attach` would silently overwrite the
192
- > first tenant's pipe. **Always declare the holder + adapter inside a
193
- > factory function** so each call yields an isolated closure pair.
194
- > The recipe below uses `makeOrchestrator(adapter)` for exactly this
195
- > reason; if you create the adapter outside the factory, pass it in
196
- > per call too.
192
+ > first tenant's pipe. **Always construct BOTH the adapter AND the
193
+ > module inside the same factory function** so the adapter's
194
+ > `events` callbacks close over the same factory-local `publishRef`
195
+ > as the source's `attach`. Sharing an adapter across factory calls
196
+ > re-introduces the cross-contamination because the adapter's
197
+ > `events.onConnect` was bound at construction time to whichever
198
+ > factory's `publishRef` was in scope first.
197
199
 
198
200
  ```ts
199
201
  import { createSystem, createModule, t } from "@directive-run/core";
@@ -364,7 +366,13 @@ that should accompany this plugin in regulated deployments.
364
366
  ## Adapter packages
365
367
 
366
368
  The canonical bridges live in **`@directive-run/sources`** as subpath
367
- exports. One install, optional peerDependencies per vendor.
369
+ exports. One install, optional peerDependencies per vendor:
370
+
371
+ ```bash
372
+ pnpm add @directive-run/sources # umbrella
373
+ pnpm add @directive-run/sources @supabase/supabase-js # + supabase
374
+ pnpm add @directive-run/sources @cloudflare/workers-types # + cloudflare (devDep)
375
+ ```
368
376
 
369
377
  | Subpath | Source factory | Wraps |
370
378
  |---|---|---|
@@ -375,7 +383,57 @@ exports. One install, optional peerDependencies per vendor.
375
383
  | `@directive-run/sources/sentry` | `sourceFromSentryHook()` | (future RFC) Sentry production-error stream |
376
384
 
377
385
  Install only the umbrella; the vendor peerDeps are optional and pull in
378
- only when the corresponding subpath is imported.
386
+ only when the corresponding subpath is imported. `package.json` marks
387
+ both `@supabase/supabase-js` and `@cloudflare/workers-types` as
388
+ optional peer dependencies — consumers using only one subpath get no
389
+ install-error nag for the other.
390
+
391
+ ---
392
+
393
+ ## Observability — pipe `source.*` events to OpenTelemetry
394
+
395
+ `attachSourcesToOtel(system, { tracer, serviceName })` bridges the
396
+ `system.observe()` source lifecycle (`source.attach` / `.publish` /
397
+ `.detach` / `.error`) into OTel spans. Pairs with `createOtelPlugin`
398
+ which exports the agent-side trace surface; the two together cover
399
+ the entire publish-to-prompt pipeline:
400
+
401
+ ```ts
402
+ import { createAgentOrchestrator, createOtelPlugin } from "@directive-run/ai";
403
+ import { attachSourcesToOtel } from "@directive-run/ai";
404
+ import { trace } from "@opentelemetry/api";
405
+
406
+ const tracer = trace.getTracer("agent-service", "1.0.0");
407
+
408
+ const orchestrator = createAgentOrchestrator({
409
+ runner: anthropic(/* ... */),
410
+ plugins: [createOtelPlugin({ tracer })],
411
+ });
412
+
413
+ // One long-lived span per (sourceId, moduleId); publishes land as span
414
+ // events on the parent attach span (cardinality-budgeted).
415
+ const unsub = attachSourcesToOtel(system, {
416
+ tracer,
417
+ serviceName: "agent-service",
418
+ // Optional: throttle high-frequency publishes (default 1 = every publish).
419
+ publishSampleRate: 1,
420
+ });
421
+
422
+ // On `system.destroy()` / `evict()`, call unsub to close any open spans.
423
+ ```
424
+
425
+ What you see in your trace backend:
426
+ - `directive.source.attached` — long-lived span; closes on `detach` with
427
+ OK status, or on `unsub()` with `directive.detached: true` attribute.
428
+ - Span events (`publish`) on the parent span with `source.event_name`
429
+ attribute and a wall-clock timestamp.
430
+ - `directive.source.error` spans with `phase: "attach" | "cleanup" |
431
+ "runtime"` and the truncated error message.
432
+
433
+ This complements `createOtelPlugin`'s agent-span coverage so an SRE
434
+ debugging "agent stopped mid-stream" can trace back through
435
+ `runStream` → `liveContext` interrupt → the watched fact → the source
436
+ publish that triggered the change — one trace tree, no manual joins.
379
437
 
380
438
  ---
381
439
 
@@ -194,6 +194,17 @@ const unsub = system.observe((event) => {
194
194
 
195
195
  `system.inspect().sources` lists the declared sources + `system.inspect().attachedSourceCount` reports how many are currently bound.
196
196
 
197
+ ### Pipe `source.*` to OpenTelemetry
198
+
199
+ For production observability, `@directive-run/ai` ships
200
+ `attachSourcesToOtel(system, { tracer, serviceName })` which bridges
201
+ the lifecycle into OTel spans (one long-lived span per
202
+ `(sourceId, moduleId)`; publishes as span events; errors as
203
+ `directive.source.error` spans tagged with `phase`). See
204
+ [`../ai/ai-sources.md` § Observability](../ai/ai-sources.md#observability--pipe-source-events-to-opentelemetry)
205
+ for the full recipe; nothing in core depends on OTel so the bridge
206
+ lives next to the AI orchestrator's own span integration.
207
+
197
208
  ## Error handling — runtime errors via `reportError`
198
209
 
199
210
  Per RFC 0008, `attach` receives an optional second argument: a
@@ -314,6 +325,74 @@ sources: {
314
325
  Inside a DO `alarm()` or `webSocketClose()` handler:
315
326
  `await system.evict(/* deadline */ Date.now() + 5000)`.
316
327
 
328
+ ## Runtime compatibility
329
+
330
+ The `source` primitive itself is runtime-agnostic — `attach`, `publish`,
331
+ `unsubscribe`, `onEvict`, and `coalesce` only depend on `Promise`,
332
+ `queueMicrotask`, and standard timers (all available in every modern
333
+ JS runtime). The matrix below covers the SHIPPED adapters in
334
+ `@directive-run/sources`; consumer-authored sources inherit each
335
+ runtime's standard guarantees.
336
+
337
+ | Runtime | Source primitive | `sourceFromSupabaseChannel` | `sourceFromDOAlarm` | `sourceFromWebSocketMessage` | Notes |
338
+ |---|---|---|---|---|---|
339
+ | **Cloudflare DO** | ✅ | ✅ (peerDep `@supabase/supabase-js` works in workerd) | ✅ default `onEvict` clears alarm | ✅ default `onEvict` closes socket 1001 | DO eviction recipe at top of this section; call `await system.evict(deadline)` from `alarm()` / `webSocketClose()`. |
340
+ | **Cloudflare Workers** | ✅ | ✅ | ✅ (needs DO `storage` handle) | ✅ (needs DO `WebSocket` handle) | Same recipes as DO; the 30s wall-clock budget applies to `cleanupAllAsync`. |
341
+ | **Bun** | ✅ | ✅ (Bun supports the `ws` transport supabase uses) | n/a (DO API) | n/a (DO API) | Sync `stop()` + async `stopAsync()` both work; Bun supports top-level `await`. |
342
+ | **Deno** | ✅ | ✅ via `npm:` specifier; needs `--allow-net` | n/a (DO API) | n/a (DO API) | Permissions: `--allow-net` for any transport; `--allow-env` for SUPABASE_URL / SUPABASE_KEY. |
343
+ | **Browser** | ✅ | ✅ | n/a (DO API) | partial — DOM `WebSocket` works with a thin adapter; the Cloudflare-typed helper is DO-specific | DOM `addEventListener` / `BroadcastChannel` / `visibilitychange` recipes inline above. |
344
+ | **Node** | ✅ | ✅ | n/a (DO API) | n/a (DO API) | Test target; vitest exercises every source path. |
345
+
346
+ The `@directive-run/sandbox` validator allowlists `@directive-run/sources`
347
+ and both subpaths, so the playground / `run_in_sandbox` MCP tool / docs
348
+ live runner all accept source-using snippets.
349
+
350
+ ### Polling — when a transport is request/response only
351
+
352
+ When the upstream system has no push channel (REST endpoint, a polled
353
+ status URL, a `setInterval`-driven heartbeat), declare a polling source.
354
+ The runtime owns mount/unmount, the `onEvict` hook lets you cancel any
355
+ pending fetch before hibernation, and the engine's batching keeps the
356
+ per-tick fact mutations cheap:
357
+
358
+ ```typescript
359
+ sources: {
360
+ status: {
361
+ attach: (publish, reportError) => {
362
+ const controller = new AbortController();
363
+ const tick = async () => {
364
+ try {
365
+ const res = await fetch(statusUrl, { signal: controller.signal });
366
+ const body = await res.json();
367
+ publish("STATUS_TICK", body);
368
+ } catch (err) {
369
+ // RFC 0008 — runtime errors go through reportError so they
370
+ // route to source.error with phase: "runtime"; the source
371
+ // stays attached.
372
+ if ((err as { name?: string }).name !== "AbortError") {
373
+ reportError?.(err);
374
+ }
375
+ }
376
+ };
377
+ const id = setInterval(tick, 5_000);
378
+ void tick(); // first poll, immediate.
379
+ return () => {
380
+ clearInterval(id);
381
+ controller.abort();
382
+ };
383
+ },
384
+ coalesce: "lastWriteWins", // only the most recent tick survives a flush
385
+ },
386
+ },
387
+ ```
388
+
389
+ Choose the `coalesce` strategy that matches the consumer:
390
+ - `"lastWriteWins"` — gauge / dashboard updates where stale ticks
391
+ don't matter.
392
+ - `"none"` — counters / time-series where every tick must land.
393
+ - `"all"` — currently an alias for `"none"`; reserved for future
394
+ bounded-buffer semantics (do not depend on it).
395
+
317
396
  ## Common Patterns
318
397
 
319
398
  ### Pattern: Source supplies inbound; resolver supplies outbound