blun-king-cli 9.1.509 → 9.1.511

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LIESMICH.txt +12 -1
  3. package/README.md +12 -1
  4. package/agent-spine-plugin/.claude-plugin/marketplace.json +1 -1
  5. package/agent-spine-plugin/.claude-plugin/plugin.json +1 -1
  6. package/agent-spine-plugin/.codex-plugin/plugin.json +2 -1
  7. package/agent-spine-plugin/CHANGELOG.md +70 -8
  8. package/agent-spine-plugin/README.md +1 -1
  9. package/agent-spine-plugin/blun.plugin.json +33 -33
  10. package/agent-spine-plugin/docs/acceptance.md +2 -2
  11. package/agent-spine-plugin/docs/gateway-runtime.md +8 -1
  12. package/agent-spine-plugin/docs/host-integration.md +34 -34
  13. package/agent-spine-plugin/docs/preflight-recall.md +69 -0
  14. package/agent-spine-plugin/docs/relationships.md +6 -0
  15. package/agent-spine-plugin/hooks/codex.json +47 -0
  16. package/agent-spine-plugin/hooks/hooks.json +11 -0
  17. package/agent-spine-plugin/hooks/version.json +2 -2
  18. package/agent-spine-plugin/package.json +4 -4
  19. package/agent-spine-plugin/scripts/check-hosts.js +53 -51
  20. package/agent-spine-plugin/scripts/check-install.js +53 -35
  21. package/agent-spine-plugin/scripts/release-check.js +11 -10
  22. package/agent-spine-plugin/skills/agent-spine/SKILL.md +1 -1
  23. package/agent-spine-plugin/src/cli.js +46 -1
  24. package/agent-spine-plugin/src/hook.js +168 -90
  25. package/agent-spine-plugin/src/index.js +6 -0
  26. package/agent-spine-plugin/src/lib/acceptance.js +40 -0
  27. package/agent-spine-plugin/src/lib/audit.js +9 -2
  28. package/agent-spine-plugin/src/lib/graph.js +22 -4
  29. package/agent-spine-plugin/src/lib/persona-runtime.js +103 -31
  30. package/agent-spine-plugin/src/lib/preflight.js +678 -0
  31. package/agent-spine-plugin/src/lib/source-roots.js +32 -32
  32. package/agent-spine-plugin/src/version.js +1 -1
  33. package/agent-spine-plugin/src/worker.js +20 -3
  34. package/bin/read-batch-policy.cjs +32 -0
  35. package/bin/turn-tool-performance-policy.cjs +1 -0
  36. package/blun.mjs +58 -2
  37. package/package.json +3 -2
@@ -228,6 +228,86 @@ function nextEventTypes(previous, binding) {
228
228
  return types;
229
229
  }
230
230
 
231
+ function runtimeEntityAttributes(persona, binding, existing) {
232
+ return {
233
+ ...(existing?.attributes || {}),
234
+ runtimeKind: persona.kind,
235
+ identityBindingId: persona.bindingId,
236
+ identityStatus: persona.status,
237
+ sourceBinding: binding?.sourceBinding || null
238
+ };
239
+ }
240
+
241
+ function sameRuntimeEntity(entity, persona, binding) {
242
+ if (!entity) return false;
243
+ const expectedKind = persona.kind === "bot" ? "agent" : persona.kind;
244
+ const expectedPrivacy = persona.groupId ? "group" : "shared";
245
+ const attributes = runtimeEntityAttributes(persona, binding, entity);
246
+ return entity.kind === expectedKind && entity.displayName === persona.displayName
247
+ && entity.privacy === expectedPrivacy
248
+ && JSON.stringify(entity.attributes) === JSON.stringify(attributes);
249
+ }
250
+
251
+ async function reconcilePersonaGraph(paths, policy, runtime) {
252
+ let { graph } = await loadGraph(paths.catalog.root);
253
+ const changes = { groupsCreated: 0, entitiesUpdated: 0, membershipsAdded: 0, membershipsRemoved: 0 };
254
+ const activeGroupIds = [...new Set(runtime.personas
255
+ .filter((item) => item.status === "active" && item.groupId !== null)
256
+ .map((item) => item.groupId))].sort();
257
+
258
+ for (const groupId of activeGroupIds) {
259
+ const existing = graph.entities.find((item) => item.id === groupId);
260
+ if (existing && existing.kind !== "group") {
261
+ throw new Error(`authenticated persona group conflicts with a non-group entity: ${groupId}`);
262
+ }
263
+ if (existing?.privacy === "private") {
264
+ throw new Error(`authenticated persona group cannot use private relationship visibility: ${groupId}`);
265
+ }
266
+ if (!existing) {
267
+ await upsertEntity({ root: paths.catalog.root, id: groupId, kind: "group", privacy: "group",
268
+ attributes: { identitySource: "authenticated-persona-roster" }, confidence: 1 });
269
+ changes.groupsCreated += 1;
270
+ graph = (await loadGraph(paths.catalog.root)).graph;
271
+ }
272
+ }
273
+
274
+ for (const persona of runtime.personas) {
275
+ const binding = policy.bindings.find((item) => item.id === persona.bindingId);
276
+ let existing = graph.entities.find((item) => item.id === persona.personaId);
277
+ if (existing && existing.kind !== (persona.kind === "bot" ? "agent" : persona.kind)) {
278
+ throw new Error(`authenticated persona conflicts with an existing entity kind: ${persona.personaId}`);
279
+ }
280
+ if (!sameRuntimeEntity(existing, persona, binding)) {
281
+ await upsertEntity({ root: paths.catalog.root, id: persona.personaId,
282
+ kind: persona.kind === "bot" ? "agent" : persona.kind, displayName: persona.displayName,
283
+ aliases: existing?.aliases || [], attributes: runtimeEntityAttributes(persona, binding, existing),
284
+ sourceDocument: existing?.sourceDocument
285
+ && paths.catalog.documents.some((item) => item.relativePath === existing.sourceDocument)
286
+ ? existing.sourceDocument : null,
287
+ privacy: persona.groupId ? "group" : "shared", confidence: 1 });
288
+ changes.entitiesUpdated += 1;
289
+ graph = (await loadGraph(paths.catalog.root)).graph;
290
+ existing = graph.entities.find((item) => item.id === persona.personaId);
291
+ }
292
+
293
+ const memberships = graph.entityEdges.filter((edge) => edge.from === persona.personaId && edge.relation === "member-of");
294
+ for (const edge of memberships.filter((item) => persona.status !== "active" || item.to !== persona.groupId)) {
295
+ await unlinkEntities({ root: paths.catalog.root, from: edge.from, to: edge.to, relation: edge.relation });
296
+ changes.membershipsRemoved += 1;
297
+ graph = (await loadGraph(paths.catalog.root)).graph;
298
+ }
299
+ if (persona.status === "active" && persona.groupId !== null
300
+ && !graph.entityEdges.some((edge) => edge.from === persona.personaId && edge.to === persona.groupId
301
+ && edge.relation === "member-of" && edge.privacy === "group")) {
302
+ await linkEntities({ root: paths.catalog.root, from: persona.personaId, to: persona.groupId,
303
+ relation: "member-of", reason: "Authenticated roster membership; context only.", confidence: 1, privacy: "group" });
304
+ changes.membershipsAdded += 1;
305
+ graph = (await loadGraph(paths.catalog.root)).graph;
306
+ }
307
+ }
308
+ return changes;
309
+ }
310
+
231
311
  export async function applyPersonaRoster({ root = process.cwd(), bindings, rosterScopes = [], confirmation, now = new Date() }) {
232
312
  if (confirmation !== CONFIRMATION) throw new Error("persona roster changes require explicit local owner confirmation");
233
313
  if (!Array.isArray(bindings) || bindings.length > 256 || (!bindings.length && !rosterScopes.length)) {
@@ -242,7 +322,6 @@ export async function applyPersonaRoster({ root = process.cwd(), bindings, roste
242
322
  const observedAt = at(now);
243
323
  const normalized = bindings.map((item) => normalizeBinding(item, observedAt));
244
324
  let changed = false;
245
- const membershipRemovals = [];
246
325
  const scopedRosterKeys = new Set([
247
326
  ...normalized.map((item) => [item.authenticator, item.issuer, item.tenantId, item.host, item.profileId].join("\0")),
248
327
  ...rosterScopes.map((item) => [item.authenticator, item.issuer, item.tenantId, item.host, item.profileId].join("\0"))
@@ -272,9 +351,6 @@ export async function applyPersonaRoster({ root = process.cwd(), bindings, roste
272
351
  policy.bindings = policy.bindings.filter((item) => item.id !== binding.id);
273
352
  policy.bindings.push(binding);
274
353
  const previous = runtime.personas.find((item) => item.personaId === binding.personaId);
275
- if (previous?.groupId && (previous.groupId !== binding.groupId || !binding.active)) {
276
- membershipRemovals.push({ personaId: binding.personaId, groupId: previous.groupId });
277
- }
278
354
  const types = nextEventTypes(previous, binding);
279
355
  let sequence = previous?.sequence || 0;
280
356
  for (const type of types) {
@@ -293,30 +369,18 @@ export async function applyPersonaRoster({ root = process.cwd(), bindings, roste
293
369
  sequence, updatedAt: observedAt, authority: "identity-state-only" });
294
370
  }
295
371
  }
296
- if (!changed) return { policy, runtime, personaPolicyPath: paths.personaPolicyPath, personaRuntimePath: paths.personaRuntimePath, duplicate: true };
297
- policy.bindings.sort((a, b) => a.id.localeCompare(b.id));
298
- runtime.personas.sort((a, b) => a.personaId.localeCompare(b.personaId));
299
- runtime.events.sort((a, b) => a.observedAt.localeCompare(b.observedAt) || a.eventId.localeCompare(b.eventId));
300
- policy.revision += 1;
301
- runtime.revision += 1;
302
- await Promise.all([writeJson(paths.personaPolicyPath, policy), writeJson(paths.personaRuntimePath, runtime)]);
303
- for (const membership of membershipRemovals) {
304
- await unlinkEntities({ root: paths.catalog.root, from: membership.personaId, to: membership.groupId, relation: "member-of" });
305
- }
306
- for (const persona of runtime.personas.filter((item) => item.status === "active")) {
307
- const binding = policy.bindings.find((item) => item.id === persona.bindingId);
308
- await upsertEntity({ root: paths.catalog.root, id: persona.personaId, kind: persona.kind === "bot" ? "agent" : persona.kind,
309
- displayName: persona.displayName, attributes: { runtimeKind: persona.kind, identityBindingId: persona.bindingId,
310
- identityStatus: persona.status, sourceBinding: binding?.sourceBinding || null }, privacy: persona.groupId ? "group" : "shared", confidence: 1 });
311
- if (persona.groupId) {
312
- const { graph } = await loadGraph(paths.catalog.root);
313
- if (graph.entities.some((item) => item.id === persona.groupId && item.kind === "group")) {
314
- await linkEntities({ root: paths.catalog.root, from: persona.personaId, to: persona.groupId,
315
- relation: "member-of", reason: "Authenticated roster membership; context only.", confidence: 1, privacy: "group" });
316
- }
317
- }
372
+ if (changed) {
373
+ policy.bindings.sort((a, b) => a.id.localeCompare(b.id));
374
+ runtime.personas.sort((a, b) => a.personaId.localeCompare(b.personaId));
375
+ runtime.events.sort((a, b) => a.observedAt.localeCompare(b.observedAt) || a.eventId.localeCompare(b.eventId));
376
+ policy.revision += 1;
377
+ runtime.revision += 1;
378
+ await Promise.all([writeJson(paths.personaPolicyPath, policy), writeJson(paths.personaRuntimePath, runtime)]);
318
379
  }
319
- return { policy, runtime, personaPolicyPath: paths.personaPolicyPath, personaRuntimePath: paths.personaRuntimePath, duplicate: false };
380
+ const graphChanges = await reconcilePersonaGraph(paths, policy, runtime);
381
+ const graphReconciled = Object.values(graphChanges).some((value) => value > 0);
382
+ return { policy, runtime, personaPolicyPath: paths.personaPolicyPath,
383
+ personaRuntimePath: paths.personaRuntimePath, duplicate: !changed, graphReconciled, graphChanges };
320
384
  });
321
385
  }
322
386
 
@@ -334,9 +398,9 @@ function normalizeNativeDiscovery(value) {
334
398
  }
335
399
 
336
400
  function nativeAgentDirectory(scope, catalog, env) {
337
- if (scope.scope === "project") return join(catalog.root, scope.host === "claude" ? ".claude" : ".codex", "agents");
338
- const home = scope.host === "claude" ? env.CLAUDE_CONFIG_DIR
339
- : env.CODEX_HOME || env.BLUN_HOME;
401
+ if (scope.scope === "project") return join(catalog.root, scope.host === "claude" ? ".claude" : ".codex", "agents");
402
+ const home = scope.host === "claude" ? env.CLAUDE_CONFIG_DIR
403
+ : env.CODEX_HOME || env.BLUN_HOME;
340
404
  if (home) {
341
405
  if (!isAbsolute(home)) throw new Error(scope.host + " home override must be absolute");
342
406
  return join(home, "agents");
@@ -434,7 +498,9 @@ export async function syncPersonaRosterFromEnvironment({ root = process.cwd(), e
434
498
  tenantId: scope.tenantId, host: scope.host, profileId: scope.profileId }));
435
499
  const result = await applyPersonaRoster({ root: catalog.root, bindings, rosterScopes,
436
500
  confirmation: CONFIRMATION, now: value.observedAt || now });
437
- return { configured: true, changed: !result.duplicate, revision: value.revision,
501
+ return { configured: true, changed: !result.duplicate || result.graphReconciled,
502
+ rosterChanged: !result.duplicate, graphReconciled: result.graphReconciled, graphChanges: result.graphChanges,
503
+ revision: value.revision,
438
504
  personas: result.runtime.personas.length, nativeManifests: nativeBindings.length,
439
505
  rosterDigest: digest(JSON.stringify(value)) };
440
506
  }
@@ -466,7 +532,13 @@ export function personaRuntimeFindings(policy, runtime, graph = null) {
466
532
  const entity = graph.entities.find((item) => item.id === persona.personaId);
467
533
  const expectedKind = persona.kind === "bot" ? "agent" : persona.kind;
468
534
  if (!entity || entity.kind !== expectedKind) findings.push("persona-graph-mismatch:" + persona.personaId);
535
+ if (entity?.attributes?.identityStatus !== persona.status) findings.push("persona-graph-status-mismatch:" + persona.personaId);
469
536
  const memberships = graph.entityEdges.filter((edge) => edge.from === persona.personaId && edge.relation === "member-of");
537
+ const group = persona.groupId === null ? null : graph.entities.find((item) => item.id === persona.groupId);
538
+ if (persona.status === "active" && persona.groupId !== null
539
+ && (!group || group.kind !== "group" || group.privacy === "private")) {
540
+ findings.push("persona-group-entity-mismatch:" + persona.personaId);
541
+ }
470
542
  if (persona.status === "active" && persona.groupId !== null
471
543
  && !memberships.some((edge) => edge.to === persona.groupId && edge.privacy === "group")) {
472
544
  findings.push("persona-group-membership-mismatch:" + persona.personaId);