@arnilo/prism 0.11.0 → 0.12.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.
Files changed (89) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +28 -27
  3. package/dist/cli-dev.js +1 -1
  4. package/dist/cli-init.js +4 -4
  5. package/dist/cli-provider-add.js +2 -2
  6. package/dist/field-evidence.d.ts +53 -0
  7. package/dist/field-evidence.js +143 -0
  8. package/dist/index.d.ts +3 -1
  9. package/dist/index.js +2 -1
  10. package/dist/run-bundle.js +2 -2
  11. package/docs/acp-agent.md +2 -2
  12. package/docs/ag-ui.md +1 -1
  13. package/docs/attention-compiler.md +1 -1
  14. package/docs/cli-rpc.md +2 -2
  15. package/docs/coding-tools.md +3 -3
  16. package/docs/compaction-observational-memory.md +13 -3
  17. package/docs/computer-use-linux.md +2 -2
  18. package/docs/core.md +9 -10
  19. package/docs/dev-inspector.md +1 -1
  20. package/docs/disaster-recovery.md +1 -1
  21. package/docs/document-extraction.md +140 -0
  22. package/docs/evaluations.md +1 -1
  23. package/docs/execution-timeline.md +30 -0
  24. package/docs/guardrails.md +53 -0
  25. package/docs/history/130-cyclic-workflows-primitive-review.md +293 -0
  26. package/docs/history/132-document-ingestion-primitive-review.md +124 -0
  27. package/docs/history/README.md +2 -0
  28. package/docs/history/migrate-to-0.12.0.md +57 -0
  29. package/docs/history/retire-node-runtime.md +59 -0
  30. package/docs/hooks.md +1 -1
  31. package/docs/host-compositions.md +2 -2
  32. package/docs/host-security.md +1 -1
  33. package/docs/index.md +30 -25
  34. package/docs/live-testing.md +1 -1
  35. package/docs/mcp-tools.md +2 -2
  36. package/docs/messaging-channels.md +1 -1
  37. package/docs/migrate-to-0.7.md +1 -1
  38. package/docs/migrate-to-0.8.md +2 -2
  39. package/docs/migrate-to-0.9.md +1 -1
  40. package/docs/migration.md +2 -2
  41. package/docs/model-registry.md +1 -1
  42. package/docs/model-routing.md +21 -0
  43. package/docs/multi-agent-patterns.md +77 -4
  44. package/docs/obscura.md +1 -1
  45. package/docs/openapi-tools.md +1 -1
  46. package/docs/operations.md +2 -2
  47. package/docs/peer-dependencies.md +18 -17
  48. package/docs/performance.md +32 -32
  49. package/docs/prompt-registry.md +1 -1
  50. package/docs/provider-packages.md +23 -21
  51. package/docs/providers/decisions.md +134 -0
  52. package/docs/providers/laya.md +116 -0
  53. package/docs/providers/typesafe.md +148 -0
  54. package/docs/rag.md +17 -5
  55. package/docs/release-and-install.md +119 -113
  56. package/docs/run-bundle.md +4 -0
  57. package/docs/runs-and-usage.md +1 -1
  58. package/docs/signal-channel.md +1 -1
  59. package/docs/sqlite-persistence.md +5 -5
  60. package/docs/synapta-integration-review.md +238 -0
  61. package/docs/telegram-channel.md +1 -1
  62. package/docs/testing.md +33 -14
  63. package/docs/wiki.md +12 -11
  64. package/docs/work-sandbox.md +2 -0
  65. package/docs/work-tools.md +3 -3
  66. package/docs/workflows.md +213 -11
  67. package/package.json +22 -21
  68. package/templates/business-worker/README.md.tmpl +3 -3
  69. package/templates/business-worker/package.json.tmpl +4 -5
  70. package/templates/business-worker/src/tests/agent.test.ts.tmpl +1 -1
  71. package/templates/business-worker/tsconfig.json.tmpl +2 -1
  72. package/templates/deep-research/README.md.tmpl +4 -4
  73. package/templates/deep-research/package.json.tmpl +4 -5
  74. package/templates/deep-research/src/tests/research.test.ts.tmpl +1 -1
  75. package/templates/deep-research/tsconfig.json.tmpl +2 -1
  76. package/templates/init/README.md.tmpl +3 -3
  77. package/templates/init/optional/evals-example.ts.tmpl +1 -1
  78. package/templates/init/optional/workflows-example.ts.tmpl +1 -1
  79. package/templates/init/package.json.tmpl +4 -5
  80. package/templates/init/src/tests/agent.test.ts.tmpl +1 -1
  81. package/templates/init/tsconfig.json.tmpl +2 -1
  82. package/templates/personal-assistant/README.md.tmpl +3 -3
  83. package/templates/personal-assistant/package.json.tmpl +4 -5
  84. package/templates/personal-assistant/src/tests/agent.test.ts.tmpl +1 -1
  85. package/templates/personal-assistant/tsconfig.json.tmpl +2 -1
  86. package/templates/provider/README.md.tmpl +2 -2
  87. package/templates/provider/package.json.tmpl +3 -4
  88. package/templates/provider/src/tests/provider.test.ts.tmpl +1 -1
  89. package/templates/provider/tsconfig.json.tmpl +1 -1
package/docs/workflows.md CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  ## What it does
4
4
 
5
- `@arnilo/prism-core/runtime/workflows` is an optional package for typed, bounded DAG orchestration over Prism sessions, tools, events, and persistence seams. Hosts define acyclic workflows with agent/function/tool/conditional/fan-out/join/nested-workflow/loop nodes; the package runs a Kahn-style scheduler with a bounded worker pool, emits package-local `WorkflowEvent`s, checkpoints progress, can coordinate queued runs across multiple host processes using durable leases and fencing, and can run bounded linear sagas with durable compensation.
5
+ `@arnilo/prism-core/runtime/workflows` is an optional package for typed, bounded DAG and cyclic graph orchestration over Prism sessions, tools, events, and persistence seams. Hosts define acyclic or cyclic workflows with agent/function/tool/conditional/fan-out/join/nested-workflow/loop/route nodes; the package runs a Kahn-style scheduler for DAGs and a wave-based superstep engine for cyclic graphs and dynamic routing, emits package-local `WorkflowEvent`s, checkpoints progress, can coordinate queued runs across multiple host processes using durable leases and fencing, and can run bounded linear sagas with durable compensation.
6
6
 
7
7
  Primary exports:
8
8
 
9
9
  | Export | Purpose |
10
10
  | --- | --- |
11
- | `defineWorkflow` / `buildGraph` | Validate definitions (acyclicity, edge refs, limits) and build deterministic successor/indegree maps |
12
- | `agentNode`, `functionNode`, `loopNode`, `toolNode`, `conditionalNode`, `fanOutNode`, `joinNode`, `workflowNode` | Typed node factories, including bounded iterative refinement and composition through the same runner |
11
+ | `defineWorkflow` / `buildGraph` | Validate definitions (cycle policy, edge refs, limits) and build deterministic successor/indegree maps |
12
+ | `agentNode`, `functionNode`, `loopNode`, `toolNode`, `conditionalNode`, `fanOutNode`, `joinNode`, `workflowNode`, `routeNode` | Typed node factories, including dynamic branch routing, bounded iterative refinement, and composition through the same runner |
13
13
  | `runWorkflow` / `resumeWorkflow` / `suspend` / `replayWorkflow` | Execute, durably suspend, exactly-once resume, or create an immutable-lineage replay from a succeeded node |
14
14
  | `createMemoryWorkflowCheckpoints` | In-process `WorkflowCheckpointAdapter` over core `createMemoryCheckpointStore()` |
15
15
  | `createWorkflowCheckpoints` | Adapt core `CheckpointStore` (including SQLite/PostgreSQL persistence capabilities) to workflow checkpoint shapes |
@@ -20,7 +20,7 @@ Primary exports:
20
20
  | `defineSaga` / `runSaga` / `resumeSaga` | Bounded linear durable forward steps, reverse compensation, unknown-outcome reconciliation, lease fencing, and manual resolution over existing checkpoint/lease stores |
21
21
  | `createWorkflowSchedules` | Explicit ownership-scoped one-time/interval/host-calculated schedules over existing checkpoint/lease stores |
22
22
  | `createProactiveScheduleCapabilities` | Scoped, expiring, revocable capability tokens that enable proactive schedules; revocation stops firing fail-closed |
23
- | `serializeWorkflowGraph` / `collectWorkflowGraphs` | Pure JSON serialization of workflow DAGs (`WorkflowGraphView`) without functions/closures; collect nested graphs |
23
+ | `serializeWorkflowGraph` / `collectWorkflowGraphs` | Pure JSON serialization of workflow graphs (`WorkflowGraphView`) without functions/closures; collect nested graphs |
24
24
  | `workflowGraphToMermaid` / `workflowGraphToDot` | Deterministic Mermaid flowchart and Graphviz DOT exporters with node shapes by kind and label escaping |
25
25
  | `projectWorkflowGraphRun` / `createWorkflowGraphRunFolder` | Run overlay view (`WorkflowGraphRunView`) from checkpoints, timelines, or live event stream |
26
26
 
@@ -28,7 +28,7 @@ Included through the `@arnilo/prism` / `@arnilo/prism-core` family packages; ins
28
28
 
29
29
  ## When to use it
30
30
 
31
- Use this package when a host needs multi-node dependency scheduling, conditionals, bounded fan-out/join, retries/timeouts, workflow events, or checkpoint/resume — without putting graph vocabulary into core.
31
+ Use this package when a host needs multi-node dependency scheduling, conditionals, dynamic routing, cyclic reflection loops, bounded fan-out/join, retries/timeouts, workflow events, or checkpoint/resume — without putting graph vocabulary into core.
32
32
 
33
33
  Use `createWorkflowCoordinator()` when multiple processes share SQLite/PostgreSQL persistence and must claim queued work exclusively. It is a database-backed coordinator, not a separate broker, DSL parser, provider abstraction, or terminal UI. Agent nodes call public `AgentSession.run()` only; tool nodes go through ordinary `ToolDefinition` dispatch and optional `ExecutionPolicy`.
34
34
 
@@ -42,8 +42,9 @@ Use `defineSaga`/`runSaga` for a linear business sequence whose remote effects n
42
42
  | --- | --- |
43
43
  | `id` | Stable workflow id (required) |
44
44
  | `revision` | Non-empty host-authored definition revision (required); parent and nested revisions enter `definitionHash` |
45
- | `nodes` | Record of node definitions (`kind` + typed fields) |
46
- | `edges` | `[from, to]` pairs; must be acyclic; unknown ids rejected |
45
+ | `nodes` | Record of node definitions (`kind` + typed fields + optional `activation`) |
46
+ | `edges` | `[from, to]` pairs; cycles permitted when `limits.maxSupersteps` is declared; undeclared cycles fail closed; unknown node ids and self-edges rejected |
47
+ | `limits.maxSupersteps` | Required on cyclic graphs, optional opt-in on DAG graphs; default none / hard cap 256 |
47
48
  | `limits.maxNodes` | Default 1,000 / hard cap 10,000 |
48
49
  | `limits.maxFanOut` | Default 64 / hard cap 1,024 |
49
50
  | `limits.maxConcurrency` | Default 8 / hard cap 256 |
@@ -65,10 +66,13 @@ Use `defineSaga`/`runSaga` for a linear business sequence whose remote effects n
65
66
  | `loop` | `loopNode` | Runs one bounded inline or function/tool body repeatedly until `until(ctx)` is true |
66
67
  | `tool` | `toolNode` | Dispatches one registered tool, optionally behind durable approval |
67
68
  | `conditional` | `conditionalNode` | Evaluates a predicate and skips configured successors |
69
+ | `route` | `routeNode` | Evaluates dynamic branch selection via `select(ctx)`, returning a subset of declared successor node IDs |
68
70
  | `fan_out` | `fanOutNode` | Maps a bounded list with workflow concurrency |
69
71
  | `join` | `joinNode` | Reduces an upstream array |
70
72
  | `workflow` | `workflowNode` | Runs a nested workflow with inherited capabilities |
71
73
 
74
+ Nodes default to `activation: "all"` (requires all declared predecessors to complete before becoming ready). Setting `activation: "any"` enables a node to fire in any wave where at least one predecessor completes (requires at least one predecessor).
75
+
72
76
  All workflow limits and runtime `concurrency` reject non-safe integers, zero, negatives, NaN, `Infinity`, and values above the named hard cap. Node retries allow 0–100; an explicit node timeout allows 1–86,400,000 ms. Omitting `timeoutMs` remains an explicit host choice.
73
77
 
74
78
  `runWorkflow(workflow, input, options?)`:
@@ -94,7 +98,7 @@ All workflow limits and runtime `concurrency` reject non-safe integers, zero, ne
94
98
  | `validateState` | Host validator for every initial/restored/updated state; required when workflow declares `state.schema` |
95
99
  | `initialState` | Optional host initial state override; nested workflows receive parent state automatically |
96
100
 
97
- A function node returns `suspend({ reason, data?, resumeSchema? })` to persist `status: "suspended"`. Its next invocation receives `ctx.resume` only after an approved resume. `resumeWorkflow(workflow, { runId }, options)` validates schema/version/ownership/`definitionHash`, claims the checkpoint before node execution, and continues the suspended node. Denial persists terminal `denied` status without invoking it. Existing failed/aborted checkpoint resume remains available without a human decision.
101
+ A function node returns `suspend({ reason, data?, resumeSchema? })` to persist `status: "suspended"`. Its next invocation receives `ctx.resume` only after an approved resume. `resumeWorkflow(workflow, { runId }, options)` validates schema/version/ownership/`definitionHash`, claims the checkpoint before node execution, and continues the suspended node. Denial persists terminal `denied` status without invoking it. Existing failed/aborted checkpoint resume remains available without a human decision. Workflow checkpoints persist `schemaVersion: 2`, recording an `execution` block with `{ superstep, pending, maxSupersteps }` alongside `outputs`, `state`, and `status`. Cyclic and superstep-enabled workflows resume from the persisted superstep and pending queue without re-executing completed wave work; legacy v1 checkpoints upgrade to v2 transparently on resume.
98
102
 
99
103
  Restore hooks make the resume all-or-nothing across layers: workflow checkpoints carry the host's `metadata` (git commit, document version, workspace fingerprint), `restoreHooks` put each recorded layer back, and only when every hook succeeds does the scheduler claim the checkpoint and continue. Each hook receives `{ workflowId, runId, version, status, metadata, checkpoint }` and an `AbortSignal`; the successful run's `workflow_resumed` event carries `restore: { hooks: [{ hook, durationMs }], durationMs }`. No hooks ⇒ no hook call and no `restore` field. Compensation is the same rule as agent resumes: an object-form handler may declare `compensate`, a failed restore undoes the applied layers in reverse (failing hook first) under the same per-hook timeout, and `CheckpointRestoreError.compensation` reports `{ ran, failed? }` best-effort while the checkpoint stays unclaimed and resumable. A successful resume's audit also reaches the review surface: `projectWorkflowTimeline` carries the same `ExecutionTimeline.restore` as the agent timeline.
100
104
 
@@ -112,7 +116,7 @@ Coding-agent ask-user glue (opt-in, no Goal DB): `suspendAskUserDecision(request
112
116
 
113
117
  Every node receives bounded `ctx.state`, `ctx.stateVersion`, and async `ctx.updateState(patch, { mode: "merge" | "replace" })`. Updates serialize, validate, redact, and snapshot before checkpoint save. A rejected state or checkpoint write stays rejected (nothing committed) and recovers the per-run chain so a later valid write can run. `workflowNode({ workflow })` runs its child with the same ownership, agent/tool registries, execution policy, redactor, signal, checkpoints, and event bus; child state replaces parent state after success.
114
118
 
115
- `replayWorkflow(workflow, { sourceRunId, fromNodeId, runId? }, options)` requires a succeeded source/node, creates a new checkpoint, copies terminal evidence outside the selected node's downstream closure, restores selected-node pre-state, and records `{ sourceRunId, fromNodeId, rootRunId, depth }`. Source evidence is untouched. Copying any prior nested/tool approval is rejected; replay from that approval node or earlier so Phase 8 approval executes again.
119
+ `replayWorkflow(workflow, { sourceRunId, fromNodeId, runId? }, options)` requires a succeeded source/node, creates a new checkpoint, copies terminal evidence outside the selected node's downstream closure, restores selected-node pre-state, and records `{ sourceRunId, fromNodeId, rootRunId, depth }`. For cyclic workflows, the downstream closure includes all nodes reachable across forward- and back-edges from `fromNodeId`, and upstream evidence for predecessor nodes that executed across multiple iterations resolves to their latest succeeded iteration output. Replay from a cyclic node that never succeeded in the source run fails closed. Source evidence is untouched. Copying any prior nested/tool approval is rejected; replay from that approval node or earlier so Phase 8 approval executes again.
116
120
 
117
121
  `createWorkflowCoordinator({ coordinatorId, workflows, checkpoints, leases, ... })` polls queued/running checkpoints with bounded pages, atomically claims each run, renews its lease, and aborts/fences work after lease loss. Key controls: `leaseTtlMs` (default 30s), `renewalIntervalMs` (default TTL/3), `pollIntervalMs` (default 1s), `maxConcurrentRuns` (default 4), and `pageSize` (default 100, maximum 500). Optional `admission` wraps claims: cursor wrap across pages (default 4 pages/poll, hard 16) so a noisy first page cannot starve later tenants; `perTenant` / `perClass` cap concurrent claims on that worker; `deadlineMs` skips stale `createdAt`; `drain` stops new claims while draining and aborts in-flight after `snapshot().expired`. Workload class is `metadata.workloadClass` (`^[a-z][a-z0-9_-]{0,31}$`, else `default`). `onMetric` labels are `outcome` + `class` only — never tenant or run ids. This is not a second scheduler.
118
122
 
@@ -401,6 +405,200 @@ if (!passed(last.outputs)) throw new BudgetExhaustedError(MAX_ITERATIONS);
401
405
 
402
406
  For a single bounded refinement, prefer `loopNode`. Keep this host-loop pattern when separate run ids, per-run checkpoints, or a new workflow definition are part of the contract.
403
407
 
408
+ ## Cyclic execution and superstep engine
409
+
410
+ Workflows support cyclic graphs with directed back-edges for iterative refinement, multi-agent reflection loops, and state-driven rework. Cyclic graphs require `limits.maxSupersteps`. Defining a cyclic graph without `limits.maxSupersteps` fails closed at validation time with `WorkflowDefinitionError`. On acyclic graphs, specifying `limits.maxSupersteps` is an optional opt-in that switches execution from the default Kahn DAG scheduler to the wave-based superstep engine.
411
+
412
+ ### Superstep execution lifecycle
413
+
414
+ 1. **Initialization (Superstep 0)**: Ready nodes (indegree 0 for DAG entry or initial entry nodes) are queued for the first wave.
415
+ 2. **Activation Waves**:
416
+ - Ready nodes execute concurrently up to configured `concurrency` (bounded by `limits.maxConcurrency`).
417
+ - Succeeded nodes enqueue their downstream successors into the next wave's pending queue.
418
+ - Nodes configure activation behavior via `activation`:
419
+ - `"all"` (default): Requires all declared predecessors to complete before the node fires. On cyclic back-edges, predecessors must complete again in subsequent waves to reactivate the node.
420
+ - `"any"`: Fires in any wave where at least one declared predecessor completes (requires at least one declared predecessor; root nodes cannot specify `activation: "any"`).
421
+ 3. **Idle-Drain Success**: When the pending queue becomes empty and no nodes remain in-flight, the workflow completes with `status: "succeeded"`.
422
+ 4. **Budget Enforcement**: If the superstep count reaches `limits.maxSupersteps` before the graph drains, execution immediately halts and fails closed with `WorkflowSuperstepLimitError` (error code `ERR_PRISM_WORKFLOW_SUPERSTEP_LIMIT`).
423
+
424
+ ### Checkpoint cadence and sizing trade-off
425
+
426
+ The superstep engine writes one checkpoint per activation wave (never more than the per-node-completion cadence it replaces); hosts that opt into `limits.maxSupersteps` on acyclic graphs keep the existing cadence.
427
+
428
+ Requirement default: none for acyclic graphs (uses the standard DAG scheduler); explicitly required for cyclic graphs. The maximum superstep limit is capped at 256 (`HARD_MAX_SUPERSTEPS`).
429
+
430
+ ```ts
431
+ import { defineWorkflow, runWorkflow, agentNode, routeNode } from "@arnilo/prism-core/runtime/workflows";
432
+
433
+ const worker = agentNode({
434
+ agent: "worker",
435
+ input: (ctx) => ({ draft: ctx.state.draft ?? "initial draft", feedback: ctx.state.feedback }),
436
+ });
437
+
438
+ const reviewer = agentNode({
439
+ agent: "reviewer",
440
+ input: (ctx) => ({ draft: ctx.upstream.worker }),
441
+ });
442
+
443
+ const router = routeNode({
444
+ select: (ctx) => {
445
+ const review = ctx.upstream.reviewer as { approved: boolean; feedback?: string };
446
+ return review.approved ? ["done"] : ["worker"];
447
+ },
448
+ });
449
+
450
+ const done = agentNode({
451
+ agent: "summarizer",
452
+ input: (ctx) => ctx.upstream.worker,
453
+ });
454
+
455
+ const workflow = defineWorkflow({
456
+ id: "cyclic-reflection",
457
+ revision: "1",
458
+ nodes: {
459
+ worker: { ...worker, activation: "any" }, // Worker fires on entry and on rework back-edge
460
+ reviewer,
461
+ router,
462
+ done,
463
+ },
464
+ edges: [
465
+ ["worker", "reviewer"],
466
+ ["reviewer", "router"],
467
+ ["router", "done"],
468
+ ["router", "worker"], // Back-edge forming a cycle
469
+ ],
470
+ limits: {
471
+ maxSupersteps: 10,
472
+ maxConcurrency: 2,
473
+ },
474
+ });
475
+ ```
476
+
477
+ ### Per-iteration replay forking
478
+
479
+ Cyclic workflows record checkpoint evidence for each executed iteration of a node. `replayWorkflow` supports targeting a specific zero-based iteration (`iteration`) of a cyclic node, with optional state patch injection (`injectState`) and input override (`injectInput`):
480
+
481
+ ```ts
482
+ const forkedRun = await replayWorkflow(workflow, {
483
+ sourceRunId: "run-original",
484
+ fromNodeId: "worker",
485
+ iteration: 1, // Replay specifically from the 2nd loop iteration
486
+ injectState: {
487
+ reviewerGuidance: "Focus strictly on security and error boundary handling",
488
+ },
489
+ }, { checkpoints, ownership: { tenantId: "t1" } });
490
+ ```
491
+
492
+ When replaying from iteration $N$, prior nodes resolve their upstream dependencies to their recorded outputs at or before iteration $N$, enabling deterministic simulation, prompt engineering counterfactuals, and interactive debugging without restarting the entire cycle.
493
+
494
+ ## Dynamic routing (`routeNode`)
495
+
496
+ `routeNode` enables dynamic branch selection at runtime based on upstream outputs and workflow state.
497
+
498
+ ```ts
499
+ const router = routeNode({
500
+ select: async (ctx) => {
501
+ const verdict = (ctx.upstream.reviewer as { verdict: string }).verdict;
502
+ if (verdict === "needs_human") return ["escalate"];
503
+ if (verdict === "retry") return ["worker"];
504
+ if (verdict === "accept") return ["done"];
505
+ return []; // Empty array cleanly drains this branch
506
+ },
507
+ });
508
+ ```
509
+
510
+ - **Target validation**: The node IDs returned by `select(ctx)` must be a subset of the route node's declared successor edges (`targets ⊆ declaredSuccessors`). If `select` returns any target node ID not declared in the workflow edges, execution fails closed with `WorkflowRouteTargetError` (`ERR_PRISM_WORKFLOW_ROUTE_TARGET`).
511
+ - **Empty selection**: Returning an empty array (`[]`) terminates routing along this path without error, allowing the branch or workflow to drain cleanly.
512
+ - **Context and Async**: `select(ctx)` receives `ctx.upstream`, `ctx.state`, and `ctx.iteration`, and can be synchronous or return a Promise.
513
+ - **Topology support**: `routeNode` works identically in both acyclic workflows and cyclic graphs.
514
+
515
+ ## Scoped per-node state and subgraph isolation
516
+
517
+ In multi-node workflows and cyclic loops, concurrent or collaborating nodes sharing a single un-scoped state dictionary can inadvertently overwrite sibling keys. Prism provides scoped state wrappers to isolate working memory:
518
+
519
+ ### Node-level scoping (`withNodeScope`)
520
+
521
+ `withNodeScope(scopeKey, node)` wraps any workflow node definition (function, agent, tool, route, conditional, fan_out, join, workflow):
522
+ - Inside the wrapped node, `ctx.state` automatically points to `ctx.state[scopeKey]`.
523
+ - Calling `ctx.updateState(patch)` updates only `ctx.state[scopeKey]`.
524
+ - Un-scoped global workflow state remains accessible via `ctx.rootState`.
525
+ - Scope keys are validated against prototype pollution (`__proto__`, `constructor`, `prototype`).
526
+
527
+ ```ts
528
+ import { functionNode, withNodeScope } from "@arnilo/prism-core/runtime/workflows";
529
+
530
+ const researcher = withNodeScope(
531
+ "researcher",
532
+ functionNode({
533
+ execute: async (ctx) => {
534
+ // Isolated read/write: updates state.researcher without colliding with state.verifier
535
+ await ctx.updateState({ notes: ["discovered leak"], confidence: 0.95 });
536
+ // Global read: inspect parent config
537
+ const target = (ctx.rootState as { targetService?: string }).targetService;
538
+ return ctx.state;
539
+ },
540
+ }),
541
+ );
542
+ ```
543
+
544
+ ### Nested subgraph isolation (`scopedSubgraphNode`)
545
+
546
+ `scopedSubgraphNode` executes an entire child workflow within an isolated state sub-namespace, automatically projecting inputs from and deliverables into `ctx.state[scope]`:
547
+
548
+ ```ts
549
+ import { scopedSubgraphNode } from "@arnilo/prism-core/runtime/workflows";
550
+
551
+ const subResearch = scopedSubgraphNode({
552
+ scope: "securitySubsystem",
553
+ workflow: securityAuditWorkflow,
554
+ input: (scopedCtx) => ({ auditTarget: scopedCtx.state.targetUrl }),
555
+ output: (childResult, scopedCtx) => ({
556
+ cveCount: childResult.outputs.scanner,
557
+ }),
558
+ });
559
+ ```
560
+
561
+ ## Declarative event-driven swarm topology
562
+
563
+ Prism provides first-class support for event-driven multi-agent swarms using cyclic supersteps and dynamic topic routing via `defineSwarmWorkflow`, `swarmRouterNode`, `publishSwarmEvent`, and `getSwarmEvents`:
564
+
565
+ ```ts
566
+ import {
567
+ defineSwarmWorkflow,
568
+ functionNode,
569
+ getSwarmEvents,
570
+ publishSwarmEvent,
571
+ withNodeScope,
572
+ } from "@arnilo/prism-core/runtime/workflows";
573
+
574
+ const swarm = defineSwarmWorkflow({
575
+ id: "support-swarm",
576
+ revision: "1",
577
+ maxSupersteps: 10,
578
+ agents: {
579
+ triage: withNodeScope("triage", triageNode),
580
+ billing: withNodeScope("billing", billingNode),
581
+ tech: withNodeScope("tech", techNode),
582
+ },
583
+ subscriptions: {
584
+ "ticket:new": ["triage"],
585
+ "ticket:billing": ["billing"],
586
+ "ticket:tech": ["tech"],
587
+ },
588
+ initialState: {
589
+ __swarmEvents: [
590
+ { topic: "ticket:new", sender: "user", payload: { type: "billing" } },
591
+ ],
592
+ },
593
+ });
594
+ ```
595
+
596
+ ### Swarm event lifecycle
597
+ 1. **Topic Subscriptions**: The central `swarmRouterNode` inspects `__swarmEvents` and dispatches each event to subscribed agents. Exact topics (`"ticket:billing"`) and wildcard prefix patterns (`"ticket:*"`, `"*"`) are supported.
598
+ 2. **Two-Phase Queueing**: Pending events are promoted to `__swarmActiveEvents` for the executing superstep wave, while newly published events accumulate in `__swarmEvents` for the next wave.
599
+ 3. **Scoped Publishing**: Agents call `publishSwarmEvent(ctx, event)` to queue new topic events. Scoped nodes write seamlessly to the root event bus without leaking into their scoped state.
600
+ 4. **Idle Drain**: When all agents finish and no further topic events are published, the swarm router selects `[]`, concluding the workflow with `status: "succeeded"`.
601
+
404
602
  ## Extension and configuration notes
405
603
 
406
604
  - Workflow semantics stay in this optional package; generic checkpoint persistence and bounded event fan-in live in core.
@@ -478,7 +676,11 @@ const graphs = collectWorkflowGraphs(hierarchicalWorkflow);
478
676
 
479
677
  ## Security and performance notes
480
678
 
481
- - Definitions require a non-empty host-authored `revision` and fail closed on cycles, unknown edges, self-edges, invalid limits, and `maxNodes` overflow. Revision and every nested revision enter the deterministic definition hash; hosts must bump revision when function/tool behavior changes. Loop `maxIterations` is required and capped at 64.
679
+ - Definitions require a non-empty host-authored `revision` and fail closed on undeclared cycles (cycles without `limits.maxSupersteps`), unknown edges, self-edges, invalid limits, and `maxNodes` overflow. Revision and every nested revision enter the deterministic definition hash; hosts must bump revision when function/tool behavior changes. Loop `maxIterations` is required and capped at 64.
680
+ - Dynamic route targets are constrained fail-closed to declared successor edges (`targets ⊆ declaredSuccessors`), preventing arbitrary node injection or dynamic traversal of undeclared paths.
681
+ - Superstep budgets (`limits.maxSupersteps`) are host-unforgeable: validated at definition time, enforced centrally by the superstep engine, and persisted in checkpoint v2 so restarts and resumes cannot tamper with or reset the superstep counter.
682
+ - Checkpoint schemaVersion 2 tracks `execution.superstep`, `execution.pending`, and `execution.maxSupersteps` so wave progression is crash-consistent across restarts.
683
+ - Replay on cyclic graphs computes downstream closure across all cyclic paths (both forward- and back-edges) and resolves upstream pre-state evidence to the latest succeeded iteration output, ensuring deterministic immutable-lineage restarts. Replay from an unexecuted or never-succeeded cyclic node fails closed.
482
684
  - Loop bodies run serially inside one scheduler node; every body output and durable iteration record is bounded/redacted with `maxNodeOutputBytes`, and the scheduler persists the completed-iteration cursor before advancing. Approved durable resumes re-enter only the incomplete iteration.
483
685
  - Fan-out length is bounded by `maxFanOut`. Independent `map` items run in a local worker pool capped by the resolved workflow `maxConcurrency` (and `options.concurrency`); output stays in input order. Abort or the first map failure stops further items. There is no extra global admission service.
484
686
  - Node outputs, shared state/history, schedule input/records, and checkpoints are byte/count/depth bounded. Checkpoint size remains the final aggregate ceiling.
@@ -503,7 +705,7 @@ Use workflows for known, durable, replayable graphs. Use optional supervisor del
503
705
 
504
706
  ## Related APIs
505
707
 
506
- - Examples: `examples/workflow-research-and-review.ts`, `examples/workflow-parallel-research.ts`, `examples/workflow-tool-approval.ts`, `examples/workflow-multimodal-document.ts`, `examples/workflow-sqlite-resume.ts`, `examples/workflow-postgres-resume.ts`, `examples/workflow-event-sink.ts`, `examples/workflow-rpc-cancel.ts`, `examples/workflow-distributed-coordinator.ts`, `examples/autonomous-coding-loop.ts` (host-loop iterate-until-done) — offline runnable demos; PostgreSQL safely skips unless `PRISM_TEST_POSTGRES_URL` is set.
708
+ - Examples: `examples/cyclic-reflection.ts`, `examples/workflow-research-and-review.ts`, `examples/workflow-parallel-research.ts`, `examples/workflow-tool-approval.ts`, `examples/workflow-multimodal-document.ts`, `examples/workflow-sqlite-resume.ts`, `examples/workflow-postgres-resume.ts`, `examples/workflow-event-sink.ts`, `examples/workflow-rpc-cancel.ts`, `examples/workflow-distributed-coordinator.ts`, `examples/autonomous-coding-loop.ts` (host-loop iterate-until-done) — offline runnable demos; PostgreSQL safely skips unless `PRISM_TEST_POSTGRES_URL` is set.
507
709
  - [Workflow orchestration primitives](history/workflow-orchestration-primitives.md): Task 0–1 inventory and locked adapter contracts
508
710
  - [Agent/session runtime](agent-session-runtime.md): `AgentSession.run()`/`stream()`, abort, subscribe
509
711
  - [Guardrails](guardrails.md): `RunWorkflowOptions.guardrails` routes tool nodes through core dispatch before policy and side effects.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arnilo/prism",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Agent harness for AI providers, agents, sessions, and tools.",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.4.2",
@@ -140,38 +140,39 @@
140
140
  "packages/hooks"
141
141
  ],
142
142
  "scripts": {
143
- "build:core": "node scripts/with-build-lock.mjs tsc",
143
+ "build:core": "bun scripts/with-build-lock.mjs tsc",
144
144
  "clean": "rm -rf dist packages/*/dist",
145
- "build": "npm run build:core && for p in @arnilo/prism-mcp @arnilo/prism-providers @arnilo/prism-memory @arnilo/prism-work @arnilo/prism-core @arnilo/prism-channels @arnilo/prism-coding-tools @arnilo/prism-ag-ui @arnilo/prism-web-tools @arnilo/prism-acp-agent @arnilo/prism-hooks; do bun run --filter \"$p\" build || exit 1; done",
145
+ "build": "bun run build:core && for p in @arnilo/prism-mcp @arnilo/prism-providers @arnilo/prism-memory @arnilo/prism-work @arnilo/prism-core @arnilo/prism-channels @arnilo/prism-coding-tools @arnilo/prism-ag-ui @arnilo/prism-web-tools @arnilo/prism-acp-agent @arnilo/prism-hooks; do bun run --filter \"$p\" build || exit 1; done",
146
146
  "typecheck": "bun run build && bun run --workspaces --if-present typecheck && tsc -p examples --noEmit",
147
- "sweep:unused": "node scripts/sweep-unused.mjs --json",
148
- "test:live": "node scripts/live-matrix.mjs",
149
- "test": "node scripts/run-all-tests.mjs",
150
- "test:coverage": "node scripts/with-build-lock.mjs bun test --coverage --timeout=0 dist/__tests__/*.test.js > node_modules/.prism-core-coverage.out 2>&1; core_exit=$?; cat node_modules/.prism-core-coverage.out; PRISM_COVERAGE_CORE_OUTPUT=node_modules/.prism-core-coverage.out PRISM_COVERAGE_CORE_EXIT=$core_exit node scripts/with-build-lock.mjs node scripts/coverage-summary.mjs && node --test scripts/phase23-coverage.test.mjs && node --test scripts/phase23-skip-manifest.test.mjs; stage_exit=$?; rm -f node_modules/.prism-core-coverage.out; exit $stage_exit",
151
- "coverage:summary": "node scripts/with-build-lock.mjs node scripts/coverage-summary.mjs",
147
+ "sweep:unused": "bun scripts/sweep-unused.mjs --json",
148
+ "test:live": "bun scripts/live-matrix.mjs",
149
+ "test": "bun scripts/run-all-tests.mjs",
150
+ "test:coverage": "bun scripts/with-build-lock.mjs bun test --coverage --timeout=0 --path-ignore-patterns=packages/** dist/__tests__/*.test.js > node_modules/.prism-core-coverage.out 2>&1; core_exit=$?; cat node_modules/.prism-core-coverage.out; PRISM_COVERAGE_CORE_OUTPUT=node_modules/.prism-core-coverage.out PRISM_COVERAGE_CORE_EXIT=$core_exit bun scripts/with-build-lock.mjs bun scripts/coverage-summary.mjs && bun test --timeout=0 scripts/phase23-coverage.test.mjs && bun test --timeout=0 scripts/phase23-skip-manifest.test.mjs; stage_exit=$?; rm -f node_modules/.prism-core-coverage.out; exit $stage_exit",
151
+ "coverage:summary": "bun scripts/with-build-lock.mjs bun scripts/coverage-summary.mjs",
152
152
  "lint": "biome lint . --reporter=sarif --reporter-file=scripts/lint-report.sarif",
153
153
  "format": "biome format --write .",
154
154
  "format:check": "biome format .",
155
- "pack:dry-run": "npm pack --dry-run && npm run pack:dry-run --workspaces --if-present",
156
- "test:postgres": "node scripts/postgres-evidence.mjs",
157
- "test:postgres:run": "node scripts/require-postgres-url.mjs && npm run test:postgres --workspace @arnilo/prism-core --if-present && npm run test:postgres --workspace @arnilo/prism-memory && npm run test:postgres --workspace @arnilo/prism-channels && node --test scripts/phase7-conformance.test.mjs scripts/phase12-restart-recovery.test.mjs scripts/phase22-conformance.test.mjs",
158
- "test:nats": "node scripts/require-nats-url.mjs && npm run test:nats --workspace @arnilo/prism-core --if-present",
159
- "release:dry-run": "npm run sdk:ready",
160
- "post-publish:smoke": "node scripts/post-publish-smoke.mjs",
161
- "release:check": "node scripts/release.mjs check",
162
- "release:publish": "node scripts/release.mjs publish",
163
- "release:evidence": "node scripts/release-skip-manifest.mjs",
164
- "sdk:ready": "npm run typecheck && npm run lint && npm run format:check && npm test && npm run test:coverage && npm run pack:dry-run && npm run release:gate",
165
- "release:gate": "node scripts/release-skip-manifest.mjs && node scripts/check-client-neutrality.mjs && node scripts/release.mjs gate",
166
- "security:threat-suites": "node --test scripts/phase8-conformance.test.mjs scripts/phase9-conformance.test.mjs scripts/phase10-conformance.test.mjs scripts/phase11-conformance.test.mjs scripts/phase20-security.test.mjs scripts/phase21-security.test.mjs scripts/phase22-security.test.mjs scripts/phase23-security.test.mjs scripts/phase38-codeql-regression.test.mjs scripts/phase40-security.test.mjs scripts/phase46-webhooks-security.test.mjs dist/__tests__/pinned-fetch.test.js packages/prism-core/dist/runtime/server/__tests__/webhooks.test.js"
155
+ "pack:dry-run": "npm pack --dry-run && bun run --filter '*' pack:dry-run",
156
+ "test:postgres": "bun scripts/postgres-evidence.mjs",
157
+ "test:postgres:run": "bun scripts/require-postgres-url.mjs && bun run --filter @arnilo/prism-core test:postgres && bun run --filter @arnilo/prism-memory test:postgres && bun run --filter @arnilo/prism-channels test:postgres && bun test --timeout=0 scripts/phase7-conformance.test.mjs scripts/phase12-restart-recovery.test.mjs scripts/phase22-conformance.test.mjs",
158
+ "test:nats": "bun scripts/require-nats-url.mjs && bun run --filter @arnilo/prism-core test:nats",
159
+ "release:dry-run": "bun run sdk:ready",
160
+ "post-publish:smoke": "bun scripts/post-publish-smoke.mjs",
161
+ "release:check": "bun scripts/release.mjs check",
162
+ "release:publish": "bun scripts/release.mjs publish",
163
+ "release:evidence": "bun scripts/release-skip-manifest.mjs",
164
+ "sdk:ready": "bun run typecheck && bun run lint && bun run format:check && bun run test && bun run test:coverage && bun run pack:dry-run && bun run release:gate",
165
+ "release:gate": "bun scripts/release-skip-manifest.mjs && bun scripts/check-client-neutrality.mjs && bun scripts/release.mjs gate",
166
+ "security:threat-suites": "bun test --timeout=0 scripts/phase8-conformance.test.mjs scripts/phase9-conformance.test.mjs scripts/phase10-conformance.test.mjs scripts/phase11-conformance.test.mjs scripts/phase20-security.test.mjs scripts/phase21-security.test.mjs scripts/phase22-security.test.mjs scripts/phase23-security.test.mjs scripts/phase38-codeql-regression.test.mjs scripts/phase40-security.test.mjs scripts/phase46-webhooks-security.test.mjs dist/__tests__/pinned-fetch.test.js packages/prism-core/dist/runtime/server/__tests__/webhooks.test.js"
167
167
  },
168
168
  "devDependencies": {
169
169
  "@biomejs/biome": "^2.5.13",
170
+ "@types/bun": "^1.4.2",
170
171
  "@types/node": "^22.20.0",
171
172
  "typescript": "^7.0.2"
172
173
  },
173
174
  "engines": {
174
- "node": ">=22"
175
+ "bun": ">=1.4.2"
175
176
  },
176
177
  "license": "MIT",
177
178
  "repository": {
@@ -5,9 +5,9 @@ Multi-tenant business worker built with Prism. Features strict tenant isolation,
5
5
  ## Quickstart
6
6
 
7
7
  ```bash
8
- npm install
9
- npm test
10
- npm start
8
+ bun install
9
+ bun test
10
+ bun run start
11
11
  ```
12
12
 
13
13
  ## Production Readiness Requirements
@@ -3,21 +3,20 @@
3
3
  "version": "0.1.0",
4
4
  "private": true,
5
5
  "type": "module",
6
+ "packageManager": "bun@1.4.2",
6
7
  "scripts": {
7
8
  "build": "tsc -p tsconfig.json",
8
9
  "typecheck": "tsc -p tsconfig.json --noEmit",
9
- "test": "npm run build && node --test dist/__tests__/agent.test.js",
10
- "start": "npm run build && node dist/index.js",
10
+ "test": "bun run build && bun test dist/__tests__/agent.test.js",
11
+ "start": "bun run build && bun dist/index.js",
11
12
  "dev": "prism dev"
12
13
  },
13
14
  "dependencies": {
14
15
  __DEPENDENCIES__
15
16
  },
16
17
  "devDependencies": {
18
+ "@types/bun": "^1.4.2",
17
19
  "@types/node": "^22.0.0",
18
20
  "typescript": "^5.7.0"
19
- },
20
- "engines": {
21
- "node": ">=20"
22
21
  }
23
22
  }
@@ -1,5 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
2
+ import { describe, it } from "bun:test";
3
3
  import { assertHostCompositionReadiness, inspectHostComposition, HostCompositionError } from "@arnilo/prism";
4
4
  import { validateApproval } from "@arnilo/prism-work/connectors";
5
5
  import type { AgentIdentity } from "@arnilo/prism";
@@ -9,7 +9,8 @@
9
9
  "declaration": true,
10
10
  "skipLibCheck": true,
11
11
  "esModuleInterop": true,
12
- "forceConsistentCasingInFileNames": true
12
+ "forceConsistentCasingInFileNames": true,
13
+ "types": ["node", "bun-types/test.d.ts"]
13
14
  },
14
15
  "include": ["src"]
15
16
  }
@@ -13,16 +13,16 @@ The agent executes an end-to-end research pipeline:
13
13
 
14
14
  ```bash
15
15
  # 1. Install dependencies
16
- npm install
16
+ bun install
17
17
 
18
18
  # 2. Run offline tests (100% offline with mock provider)
19
- npm test
19
+ bun test
20
20
 
21
21
  # 3. Start the research agent
22
- npm start
22
+ bun run start
23
23
 
24
24
  # 4. Launch local dev inspector
25
- npm run dev
25
+ bun run dev
26
26
  ```
27
27
 
28
28
  ## Architecture & Component Mapping
@@ -3,21 +3,20 @@
3
3
  "version": "0.1.0",
4
4
  "private": true,
5
5
  "type": "module",
6
+ "packageManager": "bun@1.4.2",
6
7
  "scripts": {
7
8
  "build": "tsc -p tsconfig.json",
8
9
  "typecheck": "tsc -p tsconfig.json --noEmit",
9
- "test": "npm run build && node --test dist/__tests__/research.test.js",
10
- "start": "npm run build && node dist/index.js",
10
+ "test": "bun run build && bun test dist/__tests__/research.test.js",
11
+ "start": "bun run build && bun dist/index.js",
11
12
  "dev": "prism dev"
12
13
  },
13
14
  "dependencies": {
14
15
  __DEPENDENCIES__
15
16
  },
16
17
  "devDependencies": {
18
+ "@types/bun": "^1.4.2",
17
19
  "@types/node": "^22.0.0",
18
20
  "typescript": "^5.7.0"
19
- },
20
- "engines": {
21
- "node": ">=20"
22
21
  }
23
22
  }
@@ -1,5 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
2
+ import { describe, it } from "bun:test";
3
3
  import { citation, createWebTools } from "@arnilo/prism-web-tools";
4
4
  import { createAppAgent, createPlannerAgent } from "../agent.js";
5
5
  import { createClarifyTool, createResearchSearchAdapter, formatResearchFindings } from "../tools.js";
@@ -9,7 +9,8 @@
9
9
  "strict": true,
10
10
  "noImplicitOverride": true,
11
11
  "skipLibCheck": true,
12
- "forceConsistentCasingInFileNames": true
12
+ "forceConsistentCasingInFileNames": true,
13
+ "types": ["node", "bun-types/test.d.ts"]
13
14
  },
14
15
  "include": ["src/**/*"]
15
16
  }
@@ -7,11 +7,11 @@ __PROVIDER_README_NOTE__
7
7
  ## Setup
8
8
 
9
9
  ```bash
10
- npm install
11
- npm test
10
+ bun install
11
+ bun test
12
12
  ```
13
13
 
14
- `npm test` always uses the built-in mock provider (no network, no credentials).
14
+ `bun test` always uses the built-in mock provider (no network, no credentials).
15
15
 
16
16
  __NEXT_STEPS_LIVE__
17
17
 
@@ -2,7 +2,7 @@ import type { AgentRunResult } from "@arnilo/prism";
2
2
  import { defineScorer, scoreRun } from "@arnilo/prism-evals";
3
3
 
4
4
  /**
5
- * Deterministic scorer example. Not wired into `npm start` — call after a run.
5
+ * Deterministic scorer example. Not wired into `bun run start` — call after a run.
6
6
  */
7
7
  export const greetsScorer = defineScorer({
8
8
  id: "greets",
@@ -6,7 +6,7 @@ import {
6
6
  } from "@arnilo/prism-workflows";
7
7
 
8
8
  /**
9
- * Tiny workflow example. Not wired into `npm start` — import and call from your host.
9
+ * Tiny workflow example. Not wired into `bun run start` — import and call from your host.
10
10
  */
11
11
  export async function runHelloWorkflow(name: string) {
12
12
  const workflow = defineWorkflow({
@@ -3,21 +3,20 @@
3
3
  "version": "0.0.0",
4
4
  "private": true,
5
5
  "type": "module",
6
+ "packageManager": "bun@1.4.2",
6
7
  "scripts": {
7
8
  "build": "tsc -p tsconfig.json",
8
9
  "typecheck": "tsc -p tsconfig.json --noEmit",
9
- "test": "npm run build && node --test dist/__tests__/agent.test.js",
10
- "start": "npm run build && node dist/index.js",
10
+ "test": "bun run build && bun test dist/__tests__/agent.test.js",
11
+ "start": "bun run build && bun dist/index.js",
11
12
  "dev": "prism dev"
12
13
  },
13
14
  "dependencies": {
14
15
  __DEPENDENCIES__
15
16
  },
16
17
  "devDependencies": {
18
+ "@types/bun": "^1.4.2",
17
19
  "@types/node": "^22.0.0",
18
20
  "typescript": "^5.7.0"
19
- },
20
- "engines": {
21
- "node": ">=20"
22
21
  }
23
22
  }
@@ -1,5 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
2
+ import { describe, it } from "bun:test";
3
3
  import {
4
4
  createAgent,
5
5
  createMockProvider,
@@ -9,7 +9,8 @@
9
9
  "declaration": true,
10
10
  "skipLibCheck": true,
11
11
  "esModuleInterop": true,
12
- "forceConsistentCasingInFileNames": true
12
+ "forceConsistentCasingInFileNames": true,
13
+ "types": ["node", "bun-types/test.d.ts"]
13
14
  },
14
15
  "include": ["src"]
15
16
  }
@@ -5,9 +5,9 @@ Personal workstation assistant built with Prism. Features single-user ownership,
5
5
  ## Quickstart
6
6
 
7
7
  ```bash
8
- npm install
9
- npm test
10
- npm run dev # Boots loopback dev inspector at http://127.0.0.1:4311
8
+ bun install
9
+ bun test
10
+ bun run dev # Boots loopback dev inspector at http://127.0.0.1:4311
11
11
  ```
12
12
 
13
13
  ## Security Posture