@directive-run/claude-plugin 1.18.0 → 1.19.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directive-run/claude-plugin",
3
- "version": "1.18.0",
3
+ "version": "1.19.0",
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.18.0"
55
+ "@directive-run/knowledge": "1.19.0"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsx scripts/build-skills.ts && tsup",
@@ -101,9 +101,6 @@ const result = orchestrator.runStream(agent, input, {
101
101
  system, // the Directive system whose facts feed the agent
102
102
  keys: ["pr.headSha", "pr.state"], // REQUIRED — fact keys to watch
103
103
  interruptWhen: (facts, changedKeys) => boolean, // optional; default: () => true
104
- mode: "inject-system-message", // shipped today (default behavior).
105
- // "restart" reserved for follow-up minor;
106
- // behaves identically to inject-system-message today.
107
104
  notifyOn: "interrupt-only", // default; "all-changes" emits chunk per watched change
108
105
  onContextUpdate: (changedKeys) => void, // optional logging hook
109
106
  },
@@ -170,26 +167,50 @@ threat model.
170
167
 
171
168
  ## MCP Lifecycle as a Source
172
169
 
173
- `@directive-run/ai/mcp` already exposes `MCPAdapterConfig.events`
174
- (`onConnect`, `onDisconnect`, `onToolCall`, ...) as callback hooks. The
175
- canonical adapter wraps those callbacks into a source on the
176
- orchestrator's module:
170
+ `@directive-run/ai/mcp` exposes `MCPAdapterConfig.events`
171
+ (`onConnect`, `onDisconnect`, `onToolCall`, `onError`, ...) as a single
172
+ bag of callbacks set at adapter-construction time. To bridge the
173
+ adapter's lifecycle into a Directive source the recipe is: declare a
174
+ holder that the source's `attach` populates with the live `publish`,
175
+ then point the adapter's `events.onConnect` / `onDisconnect` at the
176
+ holder. The holder pattern lets you construct the adapter and the
177
+ source in either order — the source's mount/unmount drives which
178
+ publish closure (if any) receives lifecycle events.
177
179
 
178
180
  ```ts
179
181
  import { createSystem, createModule, t } from "@directive-run/core";
182
+ import type { SourcePublish } from "@directive-run/core";
180
183
  import { createMCPAdapter } from "@directive-run/ai/mcp";
181
184
 
182
- const adapter = createMCPAdapter({ /* ... */ });
185
+ // Holder that the source's `attach` populates. The adapter's events
186
+ // forward through this — when the source is detached (`attach`'s
187
+ // unsubscribe runs), the holder goes null and the adapter callbacks
188
+ // no-op. This guarantees post-stop lifecycle events from the MCP
189
+ // transport can't write into a torn-down system.
190
+ let publishRef: SourcePublish | null = null;
191
+
192
+ const adapter = createMCPAdapter({
193
+ servers: [/* ... */],
194
+ events: {
195
+ onConnect: (name) => publishRef?.("MCP_SERVER_CONNECTED", { name }),
196
+ onDisconnect: (name) => publishRef?.("MCP_SERVER_DISCONNECTED", { name }),
197
+ },
198
+ });
183
199
 
184
200
  const orchestrator = createModule("orchestrator", {
185
201
  schema: {
186
202
  facts: {
187
- mcp: t.object<{ servers: Record<string, "connected" | "disconnected"> }>(),
203
+ mcp: t.object<{
204
+ servers: Record<string, "connected" | "disconnected">;
205
+ }>(),
188
206
  },
189
207
  events: {
190
208
  MCP_SERVER_CONNECTED: { name: t.string() },
191
209
  MCP_SERVER_DISCONNECTED: { name: t.string() },
192
210
  },
211
+ derivations: {
212
+ allServersHealthy: t.boolean(),
213
+ },
193
214
  },
194
215
  init: (f) => {
195
216
  f.mcp = { servers: {} };
@@ -209,21 +230,19 @@ const orchestrator = createModule("orchestrator", {
209
230
  },
210
231
  },
211
232
  derive: {
212
- allServersHealthy: (f) =>
213
- Object.values(f.mcp.servers).every((s) => s === "connected"),
233
+ allServersHealthy: (facts) =>
234
+ Object.values(facts.mcp.servers).every((s) => s === "connected"),
214
235
  },
215
236
  sources: {
216
237
  mcpLifecycle: {
217
238
  attach: (publish) => {
218
- const off1 = adapter.onConnect((name) =>
219
- publish("MCP_SERVER_CONNECTED", { name }),
220
- );
221
- const off2 = adapter.onDisconnect((name) =>
222
- publish("MCP_SERVER_DISCONNECTED", { name }),
223
- );
224
- return () => {
225
- off1();
226
- off2();
239
+ publishRef = publish;
240
+ // Kick the adapter's connection lifecycle once the source is
241
+ // attached so the first `onConnect` flows into our publish.
242
+ void adapter.connect();
243
+ return async () => {
244
+ publishRef = null;
245
+ await adapter.disconnect();
227
246
  };
228
247
  },
229
248
  },
@@ -234,6 +253,14 @@ const orchestrator = createModule("orchestrator", {
234
253
  Constraints can now gate agent execution on
235
254
  `facts.allServersHealthy` — no separate health-check loop needed.
236
255
 
256
+ > The holder + closure pattern is the canonical bridge from any
257
+ > single-callback-bag third-party SDK (MCP, telemetry SDKs that take an
258
+ > `onEvent` config at construction) into a Directive source. The
259
+ > alternative — passing `publish` from inside `attach` into the
260
+ > adapter's events — requires the adapter to be constructed AFTER the
261
+ > source is created, which is awkward when the adapter is used outside
262
+ > Directive too.
263
+
237
264
  ---
238
265
 
239
266
  ## Sources × Security
@@ -421,4 +421,4 @@ sources: {
421
421
  - `anti-patterns.md` (#20) — "subscribe inside an effect / useEffect" anti-pattern; prefer sources.
422
422
  - `naming.md` — canonical-term entry for `sources` + cross-paradigm aliases (RxJS Observable, DOM EventTarget, XState callback actor, Supabase realtime).
423
423
  - [`../ai/ai-sources.md`](../ai/ai-sources.md) — AI integration: `runStream({ liveContext })`, MCP lifecycle as a source, the `@directive-run/sources/*` adapter subpaths.
424
- - [`../ai/ai-security.md`](../ai/ai-security.md#sources-pii--closing-the-fact-injection-bypass) — `createFactPIIGuardrail`: closes the source → fact → agent-prompt PII bypass. Wire whenever sources publish into facts the agent reads.
424
+ - [`../ai/ai-security.md`](../ai/ai-security.md#sources--pii--closing-the-fact-injection-bypass) — `createFactPIIGuardrail`: closes the source → fact → agent-prompt PII bypass. Wire whenever sources publish into facts the agent reads.