@oneie/claude 0.6.0 → 0.7.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 (111) hide show
  1. package/agents/w1-recon.md +9 -4
  2. package/agents/w2-decide.md +51 -28
  3. package/agents/w3-edit.md +70 -78
  4. package/agents/w4-verify.md +164 -69
  5. package/commands/browser.md +98 -35
  6. package/commands/cc-connect.md +6 -0
  7. package/commands/chat.md +13 -1
  8. package/commands/close.md +45 -19
  9. package/commands/create.md +20 -10
  10. package/commands/db-sync.md +5 -1
  11. package/commands/deploy.md +174 -87
  12. package/commands/do-autonomous.md +31 -20
  13. package/commands/do-improve.md +4 -4
  14. package/commands/do-show.md +4 -3
  15. package/commands/do.md +53 -13
  16. package/commands/go.md +75 -0
  17. package/commands/improve.md +4 -2
  18. package/commands/kill.md +6 -8
  19. package/commands/notify.md +15 -2
  20. package/commands/oo-push.md +24 -9
  21. package/commands/rag.md +6 -0
  22. package/commands/release.md +46 -25
  23. package/commands/restart.md +5 -4
  24. package/commands/see.md +45 -35
  25. package/commands/skill-create.md +38 -5
  26. package/commands/sync.md +21 -11
  27. package/hooks/scripts/auto-continue.sh +70 -0
  28. package/hooks/scripts/branch-pin.sh +85 -0
  29. package/hooks/scripts/git-add-guard.sh +16 -5
  30. package/hooks/scripts/post-edit-check.sh +16 -0
  31. package/hooks/scripts/session-start.sh +13 -8
  32. package/hooks/scripts/sync-priority-todo.sh +5 -0
  33. package/package.json +1 -1
  34. package/rules/api.md +9 -0
  35. package/rules/design.md +13 -3
  36. package/rules/documentation.md +30 -71
  37. package/rules/engine.md +8 -1
  38. package/rules/ui.md +5 -1
  39. package/scripts/__pycache__/do-rank.cpython-313.pyc +0 -0
  40. package/scripts/browser-check.mjs +44 -103
  41. package/scripts/cc-events-proof.sh +13 -12
  42. package/scripts/chrome.mjs +381 -0
  43. package/scripts/do-analyze.sh +109 -4
  44. package/scripts/do-auto.sh +350 -43
  45. package/scripts/do-consumer-sweep.sh +72 -0
  46. package/scripts/do-derives-check.sh +88 -0
  47. package/scripts/do-fleet.sh +50 -1
  48. package/scripts/do-killswitch-audit.py +33 -14
  49. package/scripts/do-promise-lint.sh +150 -4
  50. package/scripts/do-promise-settle.sh +131 -9
  51. package/scripts/do-prove.sh +197 -17
  52. package/scripts/do-rank.py +606 -20
  53. package/scripts/do-rubric.py +21 -2
  54. package/scripts/do-signal.sh +157 -17
  55. package/scripts/do-smoke.sh +9 -0
  56. package/scripts/do-substrate-check.sh +49 -0
  57. package/scripts/do-tasks-wire-check.sh +81 -0
  58. package/scripts/do-tier.sh +90 -5
  59. package/scripts/do-ui-gate.sh +332 -0
  60. package/scripts/do-untracked-gate.sh +231 -0
  61. package/scripts/do-walk.sh +344 -0
  62. package/scripts/factory-check.sh +747 -0
  63. package/scripts/factory-repo.sh +1361 -0
  64. package/scripts/factory-tasks-check.sh +85 -0
  65. package/scripts/outcome-pull.ts +279 -0
  66. package/scripts/promise-manifest.mjs +167 -0
  67. package/scripts/reconcile-allow.txt +5 -0
  68. package/scripts/rubric-weights.json +64 -0
  69. package/scripts/spine-canary.sh +20 -0
  70. package/scripts/typedb-cluster-status.sh +128 -0
  71. package/scripts/typedb-env.sh +182 -0
  72. package/scripts/typedb-flap-recorder.sh +85 -0
  73. package/scripts/typedb-probes/containment-probe.py +88 -0
  74. package/scripts/typedb-probes/panic-probe-version.py +92 -0
  75. package/scripts/typedb-probes/panic-probe.py +77 -0
  76. package/scripts/typedb-scratch.sh +73 -0
  77. package/scripts/urls-lint.sh +58 -0
  78. package/scripts/w4-rubric.ts +96 -12
  79. package/scripts/wf-check.mjs +75 -0
  80. package/skills/ai-ui/SKILL.md +1 -1
  81. package/skills/astro/SKILL.md +177 -132
  82. package/skills/astro/add-content-collection.md +29 -13
  83. package/skills/astro/check-build.md +19 -13
  84. package/skills/astro/create-component.md +36 -9
  85. package/skills/astro/create-page.md +39 -10
  86. package/skills/astro/optimize-performance.md +36 -8
  87. package/skills/cli/SKILL.md +254 -220
  88. package/skills/dev/SKILL.md +1 -1
  89. package/skills/directory-autofill/SKILL.md +68 -0
  90. package/skills/docs/SKILL.md +4 -4
  91. package/skills/mcp/SKILL.md +186 -156
  92. package/skills/promise-make/SKILL.md +2 -2
  93. package/skills/puck/SKILL.md +465 -250
  94. package/skills/react19/SKILL.md +196 -296
  95. package/skills/reactflow/SKILL.md +387 -412
  96. package/skills/sdk/SKILL.md +167 -106
  97. package/skills/shadcn/SKILL.md +245 -201
  98. package/skills/signal/SKILL.md +5 -0
  99. package/skills/sui/SKILL.md +6 -1
  100. package/skills/tutorial/SKILL.md +15 -0
  101. package/skills/typedb/SKILL.md +427 -217
  102. package/skills/typedb/examples/python-patterns.py +11 -4
  103. package/skills/typedb/examples/query-patterns.tql +8 -3
  104. package/skills/typedb/examples/schema-patterns.tql +4 -2
  105. package/skills/typedb/reference/migration-2x-3x.md +19 -1
  106. package/skills/typedb/reference/python-driver.md +17 -1
  107. package/skills/voice/SKILL.md +117 -0
  108. package/skills/writer/SKILL.md +29 -12
  109. package/templates/template-feature.md +26 -1
  110. package/templates/template-tests.md +4 -0
  111. package/templates/template-todo.md +65 -10
@@ -1,47 +1,74 @@
1
1
  ---
2
2
  name: reactflow
3
- description: Build interactive node-based visualizations for envelope chains using ReactFlow with custom nodes, edges, and dark theme
3
+ description: Node-based graph surfaces in `one.ie/web` built on @xyflow/react v12 — WorkflowFlow, OrgChartView, SignalGraph, LifecycleFlow, PathGraph, TypesCanvas. Covers the v12 NodeProps/EdgeProps generic pattern the repo actually uses, node/edge type registries, 6-token styling inside a canvas, dagre auto-layout, and client:only mounting. Use when adding a custom node or edge, wiring a nodeTypes registry, fixing a graph that renders unstyled or fails tsc, or laying out substrate paths.
4
4
  user-invocable: true
5
5
  allowed-tools: Read, Write, Edit, Glob, Grep, Bash
6
6
  ---
7
7
 
8
8
  # ReactFlow Development
9
9
 
10
- Create interactive flow visualizations for the Envelope System using ReactFlow.
10
+ Interactive graph surfaces in `one.ie/web`, built on `@xyflow/react` **12.10.2**
11
+ (declared `^12.10.2`).
12
+
13
+ This is a live, heavily-used dependency — **25 files** under
14
+ `one.ie/web/src/components/` import it. Read one of them before writing a new
15
+ graph; the conventions below are extracted from them, not from upstream docs.
16
+
17
+ | Surface | Entry | Node registry |
18
+ |---|---|---|
19
+ | Workflow canvas | `components/workflows/WorkflowFlow.tsx` | `NODE_TYPES` — 8 step kinds + `chain` + `route` |
20
+ | Org chart | `components/org/OrgChartView.tsx` | `{ role: RoleNode }` |
21
+ | Signal graph | `components/org/SignalGraph.tsx` | `{ role: RoleNode }` · `{ path: PathEdge }` |
22
+ | Lifecycle | `components/lifecycle/LifecycleFlow.tsx` | 8 node types · `{ lifecycle: LifecycleEdge }` |
23
+ | Paths | `components/paths/PathGraph.tsx` + `path-flow.ts` | substrate strength/resistance |
24
+ | Types canvas | `components/data/TypesCanvas.tsx` | dagre auto-layout |
25
+ | Router brain | `components/do/RouterBrain.tsx` | `{ shape, model }` · `{ 'signal-edge': SignalEdge }` |
26
+ | AI canvas | `components/ai-elements/{canvas,node,edge,controls}.tsx` | generic primitives |
11
27
 
12
28
  ## Works With
13
29
 
14
30
  | Skill | Load when |
15
31
  |------------|----------------------------------------------------------------------------------------------|
16
32
  | `/react19` | Custom nodes/edges are React components — ref-as-prop, transitions for non-blocking updates. |
17
- | `/typedb` | Edge weights render `path.strength - path.resistance`; query highways from TypeDB for layout. |
18
- | `/signal` | Every node/edge click emits `ui:graph:<action>` graph interactions feed pheromone. |
19
- | `/astro` | Graph pages are Astro routes — hydrate with `client:only="react"` (canvas can't SSR). |
20
- | `/shadcn` | Side panels, node detail sheets, toolbars shadcn primitives on dark-theme tokens. |
33
+ | `/typedb` | Edge weights render `path.strength` / `path.resistance`; query highways for layout. |
34
+ | `/astro` | Graph pages are Astro routes — mount `client:only="react"` (the canvas cannot SSR). |
35
+ | `/shadcn` | Side panels, node detail sheets, toolbars. |
36
+ | `/puck` | `OrgChartView` and `WorkflowFlow` are lazy-loaded as Puck blocks in `lib/puck/config.tsx`. |
37
+
38
+ Every node/edge click emits `emitClick('ui:<surface>:<action>')` from
39
+ `@/lib/ui-signal` — see `.claude/rules/ui.md`. `rules/design.md` auto-loads on
40
+ these files too: a canvas is not exempt from the 6-token system.
21
41
 
22
42
  ## When to Use This Skill
23
43
 
24
- - Visualize envelope chains as node graphs
25
- - Create custom node types for agents and envelopes
26
- - Build interactive flow editors
27
- - Implement animated edge connections
28
- - Add pan, zoom, and minimap features
44
+ - Add a custom node or edge type to an existing canvas
45
+ - Build a new graph surface over substrate data
46
+ - Fix a node component that fails `tsc` on `NodeProps`
47
+ - Lay out a graph with dagre or a hand-rolled tier layout
48
+ - Style a canvas so it follows the workspace brand tokens
29
49
 
30
50
  ## Installation
31
51
 
32
- ```bash
33
- bun add @xyflow/react
52
+ Already installed — do not re-add:
53
+
54
+ ```
55
+ @xyflow/react ^12.10.2 (one.ie/web/package.json)
56
+ @dagrejs/dagre ^3.0.0 (used by components/data/TypesCanvas.tsx)
34
57
  ```
35
58
 
36
59
  ## Core Setup
37
60
 
61
+ Register node and edge types as **module-level constants**. Defining them inside
62
+ the component body creates a new object identity every render and ReactFlow
63
+ remounts every node.
64
+
38
65
  ```tsx
39
- // src/components/flow/FlowCanvas.tsx
40
66
  import {
41
67
  ReactFlow,
42
68
  Background,
43
69
  Controls,
44
70
  MiniMap,
71
+ Panel,
45
72
  useNodesState,
46
73
  useEdgesState,
47
74
  type Node,
@@ -49,318 +76,274 @@ import {
49
76
  } from '@xyflow/react';
50
77
  import '@xyflow/react/dist/style.css';
51
78
 
79
+ import { TriggerNode } from './nodes/TriggerNode';
52
80
  import { AgentNode } from './nodes/AgentNode';
53
- import { EnvelopeNode } from './nodes/EnvelopeNode';
54
81
 
55
- // Register custom node types
56
- const nodeTypes = {
57
- agent: AgentNode,
58
- envelope: EnvelopeNode,
59
- };
82
+ const NODE_TYPES = { trigger: TriggerNode, agent: AgentNode };
60
83
 
61
- export function FlowCanvas() {
62
- const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
63
- const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
84
+ export function FlowCanvas({ initialNodes, initialEdges }: {
85
+ initialNodes: Node[];
86
+ initialEdges: Edge[];
87
+ }) {
88
+ const [nodes, , onNodesChange] = useNodesState(initialNodes);
89
+ const [edges, , onEdgesChange] = useEdgesState(initialEdges);
64
90
 
65
91
  return (
66
- <div className="h-full w-full bg-[#0a0a0f]">
92
+ <div className="h-full w-full bg-background">
67
93
  <ReactFlow
68
94
  nodes={nodes}
69
95
  edges={edges}
70
96
  onNodesChange={onNodesChange}
71
97
  onEdgesChange={onEdgesChange}
72
- nodeTypes={nodeTypes}
98
+ nodeTypes={NODE_TYPES}
73
99
  fitView
74
- className="bg-[#0a0a0f]"
100
+ panOnScroll
101
+ proOptions={{ hideAttribution: true }}
75
102
  >
76
- <Background color="#1e293b" gap={20} />
77
- <Controls className="bg-[#0f0f14] border-[#1e293b]" />
78
- <MiniMap
79
- nodeColor={(node) => {
80
- if (node.type === 'agent') return '#3b82f6';
81
- return '#22c55e';
82
- }}
83
- className="bg-[#0f0f14] border-[#1e293b]"
84
- />
103
+ <Background bgColor="var(--sidebar)" />
104
+ <Controls showInteractive={false} />
105
+ <MiniMap pannable zoomable />
85
106
  </ReactFlow>
86
107
  </div>
87
108
  );
88
109
  }
89
110
  ```
90
111
 
112
+ `proOptions={{ hideAttribution: true }}` is on every canvas in the repo.
113
+ `<Background>` takes `bgColor` as a CSS value — pass a token var, never a hex.
114
+
91
115
  ## Custom Nodes
92
116
 
93
- ### Agent Node
117
+ ### The v12 typing pattern
118
+
119
+ `NodeProps` in v12 is **not** generic over your data interface. `NodeProps<T>`
120
+ expects a `Node` type, so `NodeProps<AgentNodeData>` fails to compile. Every node
121
+ in this repo uses the same intersection instead:
94
122
 
95
123
  ```tsx
96
- // src/components/flow/nodes/AgentNode.tsx
97
124
  import { Handle, Position, type NodeProps } from '@xyflow/react';
98
- import { Badge } from '@/components/ui/badge';
99
125
 
100
- interface AgentNodeData {
126
+ export interface StepNodeData {
101
127
  name: string;
102
- status: 'ready' | 'waiting' | 'idle' | 'error';
103
- actionCount: number;
128
+ kind: string;
129
+ paused?: boolean;
130
+ [key: string]: unknown; // required — v12 constrains node data to Record<string, unknown>
104
131
  }
105
132
 
106
- const statusColors = {
107
- ready: 'bg-green-500',
108
- waiting: 'bg-amber-500 animate-pulse',
109
- idle: 'bg-slate-500',
110
- error: 'bg-red-500',
111
- };
133
+ export type StepNodeProps = Omit<NodeProps, 'data'> & { data: StepNodeData };
134
+ ```
112
135
 
113
- export function AgentNode({ data }: NodeProps<AgentNodeData>) {
114
- return (
115
- <div className="px-4 py-3 bg-[#0f0f14] border border-[#1e293b] rounded-lg min-w-[150px]">
116
- {/* Input handle */}
117
- <Handle
118
- type="target"
119
- position={Position.Left}
120
- className="!bg-blue-500 !w-3 !h-3"
121
- />
122
-
123
- {/* Content */}
124
- <div className="flex items-center gap-2 mb-2">
125
- <span className={`w-2 h-2 rounded-full ${statusColors[data.status]}`} />
126
- <span className="font-medium text-white">{data.name}</span>
127
- </div>
128
-
129
- <div className="text-xs text-slate-400">
130
- {data.actionCount} actions
131
- </div>
132
-
133
- {/* Output handle */}
134
- <Handle
135
- type="source"
136
- position={Position.Right}
137
- className="!bg-green-500 !w-3 !h-3"
138
- />
139
- </div>
140
- );
136
+ The `[key: string]: unknown` index signature is mandatory. Without it TypeScript
137
+ rejects the type when ReactFlow assigns it into `Node['data']`.
138
+
139
+ For edges the equivalent is an interface extension:
140
+
141
+ ```tsx
142
+ import type { EdgeProps } from '@xyflow/react';
143
+
144
+ export interface SignalEdgeData {
145
+ strength: number;
146
+ resistance: number;
147
+ traversals: number;
148
+ isHighway?: boolean;
149
+ [key: string]: unknown;
150
+ }
151
+
152
+ interface Props extends EdgeProps {
153
+ data: SignalEdgeData;
141
154
  }
142
155
  ```
143
156
 
144
- ### Envelope Node
157
+ `data` can arrive undefined on a freshly-connected edge — every real edge in the
158
+ repo defaults it: `const d = data ?? { strength: 1, resistance: 0, traversals: 0 }`.
159
+
160
+ ### A step node
161
+
162
+ Nodes share a presentational shell so the card, badge, and handle styling live in
163
+ one place. `components/workflows/nodes/StepNodeShell.tsx` is that shell; the eight
164
+ kind nodes are ~8 lines each.
145
165
 
146
166
  ```tsx
147
- // src/components/flow/nodes/EnvelopeNode.tsx
148
- import { Handle, Position, type NodeProps } from '@xyflow/react';
149
- import { Badge } from '@/components/ui/badge';
167
+ // components/workflows/nodes/TriggerNode.tsx — the whole file
168
+ import { STEP_VISUAL } from '../step-visual';
169
+ import { StepNodeShell, type StepNodeProps } from './StepNodeShell';
170
+
171
+ const { icon, tone, label } = STEP_VISUAL.trigger;
150
172
 
151
- interface EnvelopeNodeData {
152
- id: string;
153
- action: string;
154
- status: 'pending' | 'resolved' | 'rejected';
155
- hasCallback: boolean;
173
+ /** Trigger — the workflow's entry point. No inbound handle (nothing precedes it). */
174
+ export function TriggerNode({ data }: StepNodeProps) {
175
+ return <StepNodeShell icon={icon} label={label} accent={tone} data={data} hasTarget={false} />;
156
176
  }
177
+ ```
157
178
 
158
- const statusColors = {
159
- pending: 'border-amber-500/50 bg-amber-500/10',
160
- resolved: 'border-green-500/50 bg-green-500/10',
161
- rejected: 'border-red-500/50 bg-red-500/10',
162
- };
179
+ The shell renders a token-styled card and a shared `StepHandle`:
163
180
 
164
- export function EnvelopeNode({ data }: NodeProps<EnvelopeNodeData>) {
181
+ ```tsx
182
+ import { Handle, Position } from '@xyflow/react';
183
+ import type { LucideIcon } from 'lucide-react';
184
+
185
+ export function StepHandle({ type, position, id, color }: {
186
+ type: 'source' | 'target';
187
+ position: Position;
188
+ id?: string;
189
+ color: string;
190
+ }) {
191
+ const edge = position === Position.Top ? { top: -5 } : { bottom: -5 };
165
192
  return (
166
- <div className={`px-4 py-3 border rounded-lg min-w-[180px] ${statusColors[data.status]}`}>
167
- {/* Input handle */}
168
- <Handle
169
- type="target"
170
- position={Position.Left}
171
- className="!bg-blue-500 !w-3 !h-3"
172
- />
173
-
174
- {/* Content */}
175
- <div className="flex items-center gap-2 mb-1">
176
- <span className="text-slate-400">envelope</span>
177
- <span className="font-mono text-blue-400 text-xs">{data.id}</span>
178
- </div>
179
-
180
- <div className="font-mono text-sm text-green-400">
181
- {data.action}
182
- </div>
183
-
184
- <Badge
185
- variant="outline"
186
- className={`mt-2 ${
187
- data.status === 'resolved' ? 'text-green-400' :
188
- data.status === 'pending' ? 'text-amber-400' : 'text-red-400'
189
- }`}
190
- >
191
- {data.status}
192
- </Badge>
193
-
194
- {/* Output handle (only if has callback) */}
195
- {data.hasCallback && (
196
- <Handle
197
- type="source"
198
- position={Position.Right}
199
- className="!bg-green-500 !w-3 !h-3"
200
- />
201
- )}
202
- </div>
193
+ <Handle
194
+ type={type}
195
+ id={id}
196
+ position={position}
197
+ style={{
198
+ width: 9,
199
+ height: 9,
200
+ background: color,
201
+ border: '2px solid var(--color-background)',
202
+ ...edge,
203
+ }}
204
+ />
203
205
  );
204
206
  }
205
207
  ```
206
208
 
207
- ## Converting Runtime to Flow
209
+ Node colors come from the token vars, resolved from an accent name. The shell
210
+ receives the lucide component as a prop and renames it to a capitalized local so
211
+ JSX treats it as a component, not an intrinsic element:
208
212
 
209
213
  ```tsx
210
- // src/components/flow/utils/runtimeToFlow.ts
211
- import type { Node, Edge } from '@xyflow/react';
212
- import type { Runtime } from '@/engine/Runtime';
213
- import type { Envelope, Agent } from '@/engine/types';
214
+ // inside StepNodeShell({ icon: Icon, accent, ... }: { icon: LucideIcon; accent: Accent; ... })
215
+ type Accent = 'primary' | 'secondary' | 'tertiary' | 'font';
216
+ const color = `var(--color-${accent})`;
217
+
218
+ <article
219
+ className="bg-background border rounded-2xl w-[200px] flex flex-col relative"
220
+ style={{ borderColor: 'var(--color-border)', boxShadow: 'var(--shadow-card)' }}
221
+ >
222
+ <span style={{ background: `color-mix(in oklab, ${color} 18%, transparent)`, color }}>
223
+ <Icon size={15} strokeWidth={1.75} />
224
+ </span>
225
+ </article>
226
+ ```
214
227
 
215
- interface FlowData {
216
- nodes: Node[];
217
- edges: Edge[];
218
- }
228
+ Never write a hex or a Tailwind palette class inside a node. `PathGraph.tsx`
229
+ still carries four hex literals (lines 27–29, 55) — it predates the design hook
230
+ and is a known offender, not a pattern to copy.
219
231
 
220
- export function runtimeToFlow(runtime: Runtime): FlowData {
221
- const nodes: Node[] = [];
222
- const edges: Edge[] = [];
223
-
224
- const agents = Array.from(runtime.router.agents.values());
225
- const allEnvelopes = agents.flatMap(a => a.envelopes);
226
-
227
- // Position agents in a row at the top
228
- agents.forEach((agent, index) => {
229
- nodes.push({
230
- id: agent.id,
231
- type: 'agent',
232
- position: { x: index * 250, y: 0 },
233
- data: {
234
- name: agent.name,
235
- status: agent.status,
236
- actionCount: Object.keys(agent.actions).length,
237
- },
238
- });
239
- });
232
+ ## Building the graph from substrate data
240
233
 
241
- // Position envelopes below their target agents
242
- allEnvelopes.forEach((envelope, index) => {
243
- const targetAgent = agents.find(a => a.id === envelope.metadata?.receiver);
244
- const agentIndex = targetAgent ? agents.indexOf(targetAgent) : 0;
245
-
246
- nodes.push({
247
- id: envelope.id,
248
- type: 'envelope',
249
- position: {
250
- x: agentIndex * 250 + 50,
251
- y: 150 + (index % 3) * 120,
252
- },
253
- data: {
254
- id: envelope.id.slice(-6),
255
- action: envelope.env.action,
256
- status: envelope.payload.status,
257
- hasCallback: !!envelope.callback,
258
- },
259
- });
260
-
261
- // Edge from sender to envelope
262
- if (envelope.metadata?.sender) {
263
- edges.push({
264
- id: `${envelope.metadata.sender}-${envelope.id}`,
265
- source: envelope.metadata.sender,
266
- target: envelope.id,
267
- animated: envelope.payload.status === 'pending',
268
- style: { stroke: '#3b82f6' },
269
- });
270
- }
271
-
272
- // Edge from envelope to receiver
273
- if (envelope.metadata?.receiver) {
274
- edges.push({
275
- id: `${envelope.id}-${envelope.metadata.receiver}`,
276
- source: envelope.id,
277
- target: envelope.metadata.receiver,
278
- animated: envelope.payload.status === 'pending',
279
- style: { stroke: '#22c55e' },
280
- });
281
- }
282
-
283
- // Edge to callback envelope
284
- if (envelope.callback) {
285
- edges.push({
286
- id: `${envelope.id}-callback`,
287
- source: envelope.id,
288
- target: envelope.callback.id,
289
- animated: true,
290
- style: { stroke: '#eab308', strokeDasharray: '5,5' },
291
- label: 'callback',
292
- labelStyle: { fill: '#eab308', fontSize: 10 },
293
- });
294
- }
295
- });
234
+ Graph data is derived in a **pure module** with no React and no xyflow runtime
235
+ import, so it is unit-testable. `components/paths/path-flow.ts` is the reference:
236
+
237
+ ```ts
238
+ import type { Edge, Node } from '@xyflow/react';
239
+
240
+ export interface PathRow {
241
+ from: string;
242
+ to: string;
243
+ fromName?: string;
244
+ toName?: string;
245
+ strength: number;
246
+ resistance: number;
247
+ traversals: number;
248
+ }
249
+
250
+ export function toFlow(paths: PathRow[]): { nodes: Node[]; edges: Edge[] } {
251
+ const actors = [...new Set(paths.flatMap(p => [p.from, p.to]))];
252
+
253
+ const nodes: Node[] = actors.map((id, i) => ({
254
+ id,
255
+ type: 'role',
256
+ position: { x: (i % 4) * 260, y: Math.floor(i / 4) * 200 },
257
+ data: { name: id },
258
+ }));
259
+
260
+ const edges: Edge[] = paths.map(p => ({
261
+ id: `${p.from}-${p.to}`,
262
+ source: p.from,
263
+ target: p.to,
264
+ type: 'path',
265
+ animated: p.traversals > 0,
266
+ data: { strength: p.strength, resistance: p.resistance, traversals: p.traversals },
267
+ }));
296
268
 
297
269
  return { nodes, edges };
298
270
  }
299
271
  ```
300
272
 
273
+ Edge weight maps to stroke width and token color — the visual encoding of
274
+ `strength` vs `resistance`, straight from `components/org/SignalEdge.tsx`:
275
+
276
+ ```tsx
277
+ function strokeWidth(strength: number): number {
278
+ return Math.max(1, Math.min(8, Math.log2(strength + 1) * 2));
279
+ }
280
+
281
+ function strokeColor(data: SignalEdgeData): string {
282
+ if (data.isHighway) return 'var(--color-tertiary)';
283
+ if (data.resistance > data.strength) return 'var(--color-secondary)';
284
+ return 'var(--color-primary)';
285
+ }
286
+ ```
287
+
301
288
  ## Interactive Flow Canvas
302
289
 
290
+ An editable canvas adds `onConnect`, a click handler, and gating props. Note the
291
+ handler signature: `onNodeClick` receives `(event, node)` and `node` must be
292
+ imported as a type — omitting the import is the most common tsc failure here.
293
+
303
294
  ```tsx
304
- // src/components/flow/InteractiveFlowCanvas.tsx
305
- import { useCallback, useEffect } from 'react';
295
+ import { useCallback } from 'react';
306
296
  import {
307
297
  ReactFlow,
308
298
  Background,
309
299
  Controls,
310
300
  MiniMap,
301
+ Panel,
302
+ addEdge,
311
303
  useNodesState,
312
304
  useEdgesState,
313
- addEdge,
314
305
  type Connection,
306
+ type Node,
307
+ type Edge,
315
308
  } from '@xyflow/react';
316
309
  import '@xyflow/react/dist/style.css';
317
-
310
+ import { emitClick } from '@/lib/ui-signal';
311
+ import { TriggerNode } from './nodes/TriggerNode';
318
312
  import { AgentNode } from './nodes/AgentNode';
319
- import { EnvelopeNode } from './nodes/EnvelopeNode';
320
- import { runtimeToFlow } from './utils/runtimeToFlow';
321
- import type { Runtime } from '@/engine/Runtime';
313
+ import { PathEdge } from './PathEdge';
322
314
 
323
- const nodeTypes = {
324
- agent: AgentNode,
325
- envelope: EnvelopeNode,
326
- };
315
+ const NODE_TYPES = { trigger: TriggerNode, agent: AgentNode };
316
+ const EDGE_TYPES = { path: PathEdge };
327
317
 
328
318
  interface Props {
329
- runtime: Runtime;
330
- onEnvelopeClick?: (envelopeId: string) => void;
319
+ initialNodes: Node[];
320
+ initialEdges: Edge[];
321
+ editable?: boolean;
322
+ onNodeOpen?: (id: string) => void;
331
323
  }
332
324
 
333
- export function InteractiveFlowCanvas({ runtime, onEnvelopeClick }: Props) {
334
- const [nodes, setNodes, onNodesChange] = useNodesState([]);
335
- const [edges, setEdges, onEdgesChange] = useEdgesState([]);
336
-
337
- // Update flow when runtime changes
338
- useEffect(() => {
339
- const { nodes: newNodes, edges: newEdges } = runtimeToFlow(runtime);
340
- setNodes(newNodes);
341
- setEdges(newEdges);
342
- }, [runtime, setNodes, setEdges]);
325
+ export function InteractiveFlowCanvas({
326
+ initialNodes,
327
+ initialEdges,
328
+ editable = false,
329
+ onNodeOpen,
330
+ }: Props) {
331
+ const [nodes, , onNodesChange] = useNodesState(initialNodes);
332
+ const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
343
333
 
344
- // Handle new connections
345
334
  const onConnect = useCallback(
346
335
  (connection: Connection) => {
347
- setEdges((eds) => addEdge({
348
- ...connection,
349
- animated: true,
350
- style: { stroke: '#3b82f6' },
351
- }, eds));
336
+ setEdges(eds => addEdge({ ...connection, animated: true }, eds));
352
337
  },
353
338
  [setEdges]
354
339
  );
355
340
 
356
- // Handle node clicks
357
341
  const onNodeClick = useCallback(
358
342
  (_: React.MouseEvent, node: Node) => {
359
- if (node.type === 'envelope' && onEnvelopeClick) {
360
- onEnvelopeClick(node.id);
361
- }
343
+ emitClick('ui:graph:open', { id: node.id });
344
+ onNodeOpen?.(node.id);
362
345
  },
363
- [onEnvelopeClick]
346
+ [onNodeOpen]
364
347
  );
365
348
 
366
349
  return (
@@ -372,241 +355,233 @@ export function InteractiveFlowCanvas({ runtime, onEnvelopeClick }: Props) {
372
355
  onEdgesChange={onEdgesChange}
373
356
  onConnect={onConnect}
374
357
  onNodeClick={onNodeClick}
375
- nodeTypes={nodeTypes}
358
+ nodeTypes={NODE_TYPES}
359
+ edgeTypes={EDGE_TYPES}
360
+ nodesDraggable={editable}
361
+ nodesConnectable={editable}
362
+ edgesReconnectable={false}
363
+ deleteKeyCode={editable ? ['Backspace', 'Delete'] : null}
376
364
  fitView
377
- fitViewOptions={{ padding: 0.2 }}
378
- className="bg-[#0a0a0f]"
365
+ panOnScroll
366
+ proOptions={{ hideAttribution: true }}
379
367
  >
380
- <Background
381
- color="#1e293b"
382
- gap={20}
383
- size={1}
384
- />
385
- <Controls
386
- showZoom
387
- showFitView
388
- showInteractive
389
- className="!bg-[#0f0f14] !border-[#1e293b] !shadow-lg"
390
- />
391
- <MiniMap
392
- nodeColor={(node) => {
393
- if (node.type === 'agent') return '#3b82f6';
394
- if (node.data?.status === 'resolved') return '#22c55e';
395
- if (node.data?.status === 'pending') return '#eab308';
396
- return '#64748b';
397
- }}
398
- className="!bg-[#0f0f14] !border-[#1e293b]"
399
- maskColor="rgba(10, 10, 15, 0.8)"
400
- />
368
+ <Background bgColor="var(--sidebar)" />
369
+ <MiniMap pannable zoomable />
370
+ <Controls showInteractive={false} />
371
+ {editable && <Panel position="top-left">{/* toolbar */}</Panel>}
401
372
  </ReactFlow>
402
373
  </div>
403
374
  );
404
375
  }
405
376
  ```
406
377
 
378
+ `deleteKeyCode={null}` is how a read-only canvas blocks node deletion — there is
379
+ no `deletable` prop on `<ReactFlow>`.
380
+
407
381
  ## Custom Edges
408
382
 
383
+ `LifecycleFlow.tsx` picks its path function from edge data and animates a dot
384
+ along it with SVG `animateMotion`:
385
+
409
386
  ```tsx
410
- // src/components/flow/edges/CallbackEdge.tsx
411
387
  import {
412
388
  BaseEdge,
413
389
  EdgeLabelRenderer,
414
390
  getBezierPath,
391
+ getStraightPath,
415
392
  type EdgeProps,
416
393
  } from '@xyflow/react';
417
394
 
418
- export function CallbackEdge({
419
- id,
420
- sourceX,
421
- sourceY,
422
- targetX,
423
- targetY,
424
- sourcePosition,
425
- targetPosition,
426
- style = {},
427
- markerEnd,
395
+ interface EdgeData extends Record<string, unknown> {
396
+ kind: 'forward' | 'loop';
397
+ flow?: boolean;
398
+ showLabel?: boolean;
399
+ label?: string;
400
+ }
401
+
402
+ function LifecycleEdge({
403
+ sourceX, sourceY, targetX, targetY,
404
+ sourcePosition, targetPosition, markerEnd, style, data,
428
405
  }: EdgeProps) {
429
- const [edgePath, labelX, labelY] = getBezierPath({
430
- sourceX,
431
- sourceY,
432
- sourcePosition,
433
- targetX,
434
- targetY,
435
- targetPosition,
436
- });
406
+ const d = data as EdgeData | undefined;
407
+ const [path, labelX, labelY] =
408
+ d?.kind === 'forward'
409
+ ? getStraightPath({ sourceX, sourceY, targetX, targetY })
410
+ : getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition });
437
411
 
438
412
  return (
439
413
  <>
440
- <BaseEdge
441
- path={edgePath}
442
- markerEnd={markerEnd}
443
- style={{
444
- ...style,
445
- stroke: '#eab308',
446
- strokeWidth: 2,
447
- strokeDasharray: '5,5',
448
- }}
449
- />
450
- <EdgeLabelRenderer>
451
- <div
452
- style={{
453
- position: 'absolute',
454
- transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
455
- pointerEvents: 'all',
456
- }}
457
- className="px-2 py-1 bg-[#0f0f14] border border-[#1e293b] rounded text-xs text-amber-400"
458
- >
459
- callback
460
- </div>
461
- </EdgeLabelRenderer>
414
+ <BaseEdge path={path} markerEnd={markerEnd} style={style} />
415
+ {d?.flow && (
416
+ <circle r={3.5} fill="var(--color-primary)">
417
+ <animateMotion dur="2.4s" repeatCount="indefinite" path={path} />
418
+ </circle>
419
+ )}
420
+ {d?.showLabel && d?.label && (
421
+ <EdgeLabelRenderer>
422
+ <div
423
+ className="pointer-events-none px-2 py-0.5 rounded-full text-xs font-semibold bg-background border text-font/80 whitespace-nowrap"
424
+ style={{
425
+ position: 'absolute',
426
+ transform: `translate(-50%,-50%) translate(${labelX}px,${labelY}px)`,
427
+ borderColor: 'var(--color-border)',
428
+ boxShadow: 'var(--shadow-card)',
429
+ }}
430
+ >
431
+ {d.label}
432
+ </div>
433
+ </EdgeLabelRenderer>
434
+ )}
462
435
  </>
463
436
  );
464
437
  }
465
438
  ```
466
439
 
440
+ `getBezierPath` needs `sourcePosition` and `targetPosition`; `getStraightPath`
441
+ takes only the four coordinates. Passing the extra keys to `getStraightPath` is
442
+ harmless but passing too few to `getBezierPath` yields a degenerate path.
443
+
467
444
  ## Edge Types Registration
468
445
 
469
446
  ```tsx
470
- import { CallbackEdge } from './edges/CallbackEdge';
447
+ import { LifecycleEdge } from './LifecycleEdge';
471
448
 
472
- const edgeTypes = {
473
- callback: CallbackEdge,
474
- };
449
+ const EDGE_TYPES = { lifecycle: LifecycleEdge };
475
450
 
476
- // In ReactFlow component
477
- <ReactFlow
478
- edgeTypes={edgeTypes}
479
- // ...
480
- />
451
+ <ReactFlow edgeTypes={EDGE_TYPES} /* ... */ />
481
452
  ```
482
453
 
454
+ Module scope, same as `NODE_TYPES`. An edge opts in via `type: 'lifecycle'` in
455
+ its `Edge` object.
456
+
483
457
  ## Auto-Layout with dagre
484
458
 
485
- ```bash
486
- bun add @dagrejs/dagre
487
- ```
459
+ `components/data/TypesCanvas.tsx` is the only dagre consumer. It falls back to a
460
+ grid when there are no edges — dagre on a disconnected graph stacks everything at
461
+ the origin:
488
462
 
489
- ```tsx
490
- // src/components/flow/utils/autoLayout.ts
463
+ ```ts
491
464
  import dagre from '@dagrejs/dagre';
492
465
  import type { Node, Edge } from '@xyflow/react';
493
466
 
494
- const dagreGraph = new dagre.graphlib.Graph();
495
- dagreGraph.setDefaultEdgeLabel(() => ({}));
467
+ const NODE_WIDTH = 240;
468
+ const NODE_HEIGHT = 160;
469
+
470
+ function dagreLayout(nodes: Node[], edges: Edge[]): Node[] {
471
+ const g = new dagre.graphlib.Graph();
472
+ g.setDefaultEdgeLabel(() => ({}));
473
+ g.setGraph({ rankdir: 'LR', ranksep: 120, nodesep: 60 });
474
+ for (const node of nodes) g.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
475
+ for (const edge of edges) g.setEdge(edge.source, edge.target);
476
+ dagre.layout(g);
477
+ return nodes.map(node => {
478
+ const { x, y } = g.node(node.id);
479
+ return { ...node, position: { x: x - NODE_WIDTH / 2, y: y - NODE_HEIGHT / 2 } };
480
+ });
481
+ }
496
482
 
497
- const nodeWidth = 180;
498
- const nodeHeight = 100;
483
+ const GRID_COLS = 3;
499
484
 
500
- export function getLayoutedElements(
501
- nodes: Node[],
502
- edges: Edge[],
503
- direction: 'TB' | 'LR' = 'LR'
504
- ) {
505
- dagreGraph.setGraph({ rankdir: direction });
485
+ function gridLayout(nodes: Node[]): Node[] {
486
+ return nodes.map((node, i) => ({
487
+ ...node,
488
+ position: { x: (i % GRID_COLS) * 300 + 32, y: Math.floor(i / GRID_COLS) * 220 + 32 },
489
+ }));
490
+ }
506
491
 
507
- nodes.forEach((node) => {
508
- dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
509
- });
492
+ export function layoutNodes(nodes: Node[], edges: Edge[]): Node[] {
493
+ return edges.length === 0 ? gridLayout(nodes) : dagreLayout(nodes, edges);
494
+ }
495
+ ```
510
496
 
511
- edges.forEach((edge) => {
512
- dagreGraph.setEdge(edge.source, edge.target);
513
- });
497
+ Build the graph fresh per call — a module-level `dagre.graphlib.Graph()` reused
498
+ across renders accumulates stale nodes.
514
499
 
515
- dagre.layout(dagreGraph);
516
-
517
- const layoutedNodes = nodes.map((node) => {
518
- const nodeWithPosition = dagreGraph.node(node.id);
519
- return {
520
- ...node,
521
- position: {
522
- x: nodeWithPosition.x - nodeWidth / 2,
523
- y: nodeWithPosition.y - nodeHeight / 2,
524
- },
525
- };
526
- });
500
+ For a known hierarchy, skip dagre. `OrgChartView` and `SignalGraph` place tiers
501
+ on fixed rows:
527
502
 
528
- return { nodes: layoutedNodes, edges };
529
- }
503
+ ```ts
504
+ const ROW_Y: Record<Tier, number> = {
505
+ chairman: 0, ceo: 260, director: 560, specialist: 880,
506
+ };
530
507
  ```
531
508
 
532
509
  ## Astro Page with ReactFlow
533
510
 
511
+ Always `client:only="react"` — the canvas measures the DOM and cannot SSR.
512
+ `client:load` renders an empty container on the server and flashes.
513
+
534
514
  ```astro
535
515
  ---
536
- // src/pages/visualise.astro
516
+ // src/pages/org/index.astro
517
+ export const prerender = false;
537
518
  import Layout from "@/layouts/Layout.astro";
538
- import { InteractiveFlowCanvas } from "@/components/flow/InteractiveFlowCanvas";
519
+ import { OrgChartView } from "@/components/org/OrgChartView";
539
520
  ---
540
521
 
541
- <Layout title="Envelope Flow Visualization">
522
+ <Layout title="Org chart">
542
523
  <div class="h-screen">
543
- <InteractiveFlowCanvas client:load />
524
+ <OrgChartView chart="complete" client:only="react" />
544
525
  </div>
545
526
  </Layout>
546
-
547
- <style>
548
- /* ReactFlow dark theme overrides */
549
- :global(.react-flow__node) {
550
- font-family: ui-monospace, monospace;
551
- }
552
-
553
- :global(.react-flow__controls button) {
554
- background-color: #0f0f14 !important;
555
- border-color: #1e293b !important;
556
- color: white !important;
557
- }
558
-
559
- :global(.react-flow__controls button:hover) {
560
- background-color: #1e293b !important;
561
- }
562
- </style>
563
527
  ```
564
528
 
529
+ The parent must have a resolved height. ReactFlow measures its container; inside
530
+ a `h-auto` wrapper it collapses to zero and renders nothing.
531
+
565
532
  ## Real-time Updates
566
533
 
534
+ Do not poll and do not hand-roll an event emitter. A resolver mutation anywhere —
535
+ UI, chat, MCP, CLI — broadcasts one SSE frame that every mounted surface on that
536
+ dimension receives:
537
+
567
538
  ```tsx
568
- // Update flow when runtime emits events
569
- import { useEffect } from 'react';
570
-
571
- export function useRuntimeSync(
572
- runtime: Runtime,
573
- setNodes: (nodes: Node[]) => void,
574
- setEdges: (edges: Edge[]) => void
575
- ) {
576
- useEffect(() => {
577
- const updateFlow = () => {
578
- const { nodes, edges } = runtimeToFlow(runtime);
579
- setNodes(nodes);
580
- setEdges(edges);
581
- };
582
-
583
- // Initial render
584
- updateFlow();
585
-
586
- // Subscribe to runtime events
587
- runtime.on('envelope:sent', updateFlow);
588
- runtime.on('envelope:resolved', updateFlow);
589
- runtime.on('agent:statusChange', updateFlow);
590
-
591
- return () => {
592
- runtime.off('envelope:sent', updateFlow);
593
- runtime.off('envelope:resolved', updateFlow);
594
- runtime.off('agent:statusChange', updateFlow);
595
- };
596
- }, [runtime, setNodes, setEdges]);
539
+ import { useCallback } from 'react';
540
+ import { useNodesState, useEdgesState, ReactFlow, type Node, type Edge } from '@xyflow/react';
541
+ import { useSurfaceRefresh } from '@/lib/use-surface-refresh';
542
+ import { buildGraph, type WorkflowDef } from './build-graph';
543
+
544
+ export function WorkflowCanvas({ slug, workflowId }: { slug: string; workflowId: string }) {
545
+ const [nodes, setNodes] = useNodesState<Node>([]);
546
+ const [edges, setEdges] = useEdgesState<Edge>([]);
547
+
548
+ const reload = useCallback(() => {
549
+ fetch(`/api/workflows/${workflowId}?slug=${slug}`)
550
+ .then(r => r.json() as Promise<{ data?: WorkflowDef }>)
551
+ .then(d => {
552
+ if (!d.data) return;
553
+ const g = buildGraph(d.data);
554
+ setNodes(g.nodes);
555
+ setEdges(g.edges);
556
+ })
557
+ .catch(() => {});
558
+ }, [slug, workflowId, setNodes, setEdges]);
559
+
560
+ useSurfaceRefresh(slug, 'workflows', reload);
561
+
562
+ return <ReactFlow nodes={nodes} edges={edges} /* ... */ />;
597
563
  }
598
564
  ```
599
565
 
600
566
  ## Best Practices
601
567
 
602
- 1. **Memoize custom nodes**: Use `React.memo` to prevent re-renders
603
- 2. **Use fitView**: Auto-fit the view after layout changes
604
- 3. **Animate pending edges**: Show activity with `animated` prop
605
- 4. **Dark theme CSS**: Override ReactFlow default styles
606
- 5. **Custom handles**: Style handles to match your theme
607
- 6. **Auto-layout**: Use dagre for complex graphs
568
+ 1. **Registries at module scope**: `NODE_TYPES` / `EDGE_TYPES` defined in the
569
+ component body remount every node on every render.
570
+ 2. **`Omit<NodeProps,'data'> & { data: T }`**: the v12 pattern. `NodeProps<T>`
571
+ does not compile with a bare data interface.
572
+ 3. **Index-signature your data types**: `[key: string]: unknown`, or the type is
573
+ rejected at `Node['data']`.
574
+ 4. **Tokens inside the canvas too**: `var(--color-primary)`, `color-mix(...)`,
575
+ `var(--color-border)`. Never a hex, never a palette class.
576
+ 5. **`client:only="react"`, sized parent**: the canvas cannot SSR and collapses
577
+ without a resolved height.
578
+ 6. **Derive the graph in a pure module**: no React, no xyflow runtime import — so
579
+ it can be unit-tested.
580
+ 7. **Default undefined edge `data`**: a newly-connected edge has none.
581
+ 8. **`emitClick` before the handler**: node and edge clicks are signals.
582
+ 9. **`proOptions={{ hideAttribution: true }}`**: on every canvas in the repo.
608
583
 
609
584
  ---
610
585
 
611
- **Version**: 1.0.0
612
- **Tech**: @xyflow/react 12+
586
+ **Tech**: @xyflow/react 12.10.2 · @dagrejs/dagre 3.0 · React 19.2.6 · Astro 6.3.7
587
+ **Tree**: `one.ie/web` — 25 importing files under `src/components/`