@topy-ai/maggie 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-zh-TW.md +2 -2
- package/README.md +2 -2
- package/bundled-references/agent-goal-design.md +92 -0
- package/bundled-skills/catalog.json +4 -0
- package/bundled-skills/maggie-deployment/SKILL.md +21 -1
- package/bundled-skills/maggie-design/SKILL.md +29 -1
- package/bundled-skills/maggie-spirit/SKILL.md +128 -0
- package/bundled-tools/clis/maggie_dash.py +12 -4
- package/bundled-tools/clis/maggie_spirit.py +143 -0
- package/bundled-tools/runtime/maggie_dash_pg.py +126 -0
- package/bundled-tools/runtime/maggie_dash_store.py +17 -5
- package/bundled-tools/runtime/spirit_agent.py +262 -0
- package/bundled-tools/runtime/spirit_base.py +256 -0
- package/bundled-tools/runtime/spirit_face.py +222 -0
- package/package.json +1 -1
- package/references/agent-goal-design.md +92 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""ONEMAN sprite system — the reusable animated status-sprite basic set.
|
|
3
|
+
|
|
4
|
+
Every ONEMAN agent (Core or Service) shares ONE visual language: a 17x17 flat
|
|
5
|
+
pixel motif, a fixed semantic palette tinted by the agent's accent, a catalog of
|
|
6
|
+
32 common states, and a vocabulary of animation primitives. Give this generator
|
|
7
|
+
a Motif (the agent's mark) + an accent colour and it emits the whole set in a
|
|
8
|
+
consistent style: an animated PNG frame strip per state, a still SVG per state,
|
|
9
|
+
a combined overview sheet, and states.json (frames + fps) to drive CSS/canvas.
|
|
10
|
+
|
|
11
|
+
The ONEMAN mark (a blocky O ring + central 1 bar, accent #ff1212 = topy.ai) is
|
|
12
|
+
the default motif. A new agent reuses everything here and supplies only its own
|
|
13
|
+
motif grid + accent, so the whole roster animates and reads identically.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
python3 sprite_system.py # regenerate the ONEMAN set
|
|
17
|
+
from sprite_system import Motif, generate # build another agent's set
|
|
18
|
+
generate(Motif.oneman(), accent="#ff1212", out="oneman", title="ONEMAN")
|
|
19
|
+
|
|
20
|
+
PIL only (no cairosvg).
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
import json, math, os
|
|
24
|
+
from PIL import Image, ImageDraw
|
|
25
|
+
|
|
26
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
27
|
+
OUT_BASE = HERE
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------- palette ----
|
|
30
|
+
def _rgb(h): h=h.lstrip("#"); return (int(h[0:2],16),int(h[2:4],16),int(h[4:6],16))
|
|
31
|
+
def shade(c, amt):
|
|
32
|
+
"""Lighten (amt>0) or darken (amt<0) an rgb tuple toward white/black, in %."""
|
|
33
|
+
f=lambda v: max(0,min(255,round(v+(amt/100)*((255-v) if amt>0 else v))))
|
|
34
|
+
return (f(c[0]),f(c[1]),f(c[2]))
|
|
35
|
+
|
|
36
|
+
# fixed semantic colours shared by every agent (danger/success/etc are universal)
|
|
37
|
+
SEM = {"red":_rgb("ff1212"), "green":_rgb("36c76a"), "amber":_rgb("f2b01e"),
|
|
38
|
+
"blue":_rgb("3a9bff"), "grey":_rgb("4a4a58"), "ink":_rgb("14141e"),
|
|
39
|
+
"white":_rgb("f5f5fa")}
|
|
40
|
+
|
|
41
|
+
def palette(accent_hex):
|
|
42
|
+
a=_rgb(accent_hex)
|
|
43
|
+
p=dict(SEM)
|
|
44
|
+
p.update(accent=a, acc_dim=shade(a,-52), acc_deep=shade(a,-66),
|
|
45
|
+
acc_hot=shade(a,34), acc_spark=shade(a,72),
|
|
46
|
+
grey_d=shade(SEM["grey"],-42), green_d=shade(SEM["green"],-70),
|
|
47
|
+
amber_d=shade(SEM["amber"],-78), red_deep=shade(SEM["red"],-66))
|
|
48
|
+
return p
|
|
49
|
+
|
|
50
|
+
# ------------------------------------------------------------------ motif ----
|
|
51
|
+
class Motif:
|
|
52
|
+
"""A 17x17 mark + the cell roles the animation vocabulary needs."""
|
|
53
|
+
def __init__(self, grid, core=None, dots=None, sparks=None):
|
|
54
|
+
self.grid=grid; self.N=len(grid); self.cx=self.cy=(self.N-1)/2
|
|
55
|
+
self.filled=[(x,y) for y in range(self.N) for x in range(self.N) if grid[y][x]]
|
|
56
|
+
self.core=core if core is not None else []
|
|
57
|
+
coreset=set(self.core)
|
|
58
|
+
ringc=[(x,y) for (x,y) in self.filled if (x,y) not in coreset]
|
|
59
|
+
# order ring cells by angle from top, clockwise — the animatable path
|
|
60
|
+
self.ring=sorted(ringc, key=lambda p:(math.atan2(p[1]-self.cy,p[0]-self.cx)+math.pi/2)%(2*math.pi))
|
|
61
|
+
self.dots=dots if dots is not None else []
|
|
62
|
+
self.sparks=sparks if sparks is not None else []
|
|
63
|
+
def ang(self,x,y): return (math.atan2(y-self.cy,x-self.cx))%(2*math.pi)
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def oneman():
|
|
67
|
+
g=[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0],
|
|
68
|
+
[0,0,0,0,1,1,0,1,1,1,0,1,1,0,0,0,0],[0,0,0,1,1,1,0,1,1,1,0,1,1,1,0,0,0],
|
|
69
|
+
[0,0,1,1,1,1,0,1,1,1,0,1,1,1,1,0,0],[0,1,1,1,1,0,0,1,1,1,0,0,1,1,1,1,0],
|
|
70
|
+
[0,1,1,1,0,0,0,1,1,1,0,0,0,1,1,1,0],[0,1,1,1,0,0,0,1,1,1,0,0,0,1,1,1,0],
|
|
71
|
+
[0,1,1,1,0,0,0,1,1,1,0,0,0,1,1,1,0],[0,1,1,1,0,0,0,0,0,0,0,0,0,1,1,1,0],
|
|
72
|
+
[0,1,1,1,0,0,0,0,0,0,0,0,0,1,1,1,0],[0,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1,0],
|
|
73
|
+
[0,0,1,1,1,1,0,0,0,0,0,1,1,1,1,0,0],[0,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,0],
|
|
74
|
+
[0,0,0,0,1,1,1,1,1,1,1,1,1,0,0,0,0],[0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0],
|
|
75
|
+
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]]
|
|
76
|
+
core=[(x,y) for y in range(17) for x in range(17) if g[y][x] and x in (7,8,9) and y<=9]
|
|
77
|
+
dots=[(6,10),(8,10),(10,10)]
|
|
78
|
+
sparks_lv=[[],[(8,0),(1,5),(15,5),(0,8),(16,8)],
|
|
79
|
+
[(8,0),(7,0),(9,0),(1,4),(15,4),(0,8),(16,8),(1,12),(15,12)]]
|
|
80
|
+
m=Motif(g, core=core, dots=dots, sparks=sparks_lv); return m
|
|
81
|
+
|
|
82
|
+
# --------------------------------------------------------------- badges ------
|
|
83
|
+
GLYPH = {
|
|
84
|
+
"dot":["00000","00000","00100","00000","00000"],"check":["00000","00001","00010","10100","01000"],
|
|
85
|
+
"x":["10001","01010","00100","01010","10001"],"pause":["01010","01010","01010","01010","01010"],
|
|
86
|
+
"slash":["00001","00010","00100","01000","10000"],"clock":["01110","10101","10111","10001","01110"],
|
|
87
|
+
"lock":["01110","01010","11111","11011","11111"],"shield":["11111","11111","01110","01110","00100"],
|
|
88
|
+
"up":["00100","01110","11111","00100","00100"],"gear":["01010","11111","01110","11111","01010"],
|
|
89
|
+
}
|
|
90
|
+
def put_badge(px, name, bg, fg, N):
|
|
91
|
+
for yy in range(0,6):
|
|
92
|
+
for xx in range(11, min(17,N)): px[(xx,yy)]=None
|
|
93
|
+
g=GLYPH[name]
|
|
94
|
+
for r in range(5):
|
|
95
|
+
for c in range(5): px[(12+c,r)] = fg if g[r][c]=="1" else bg
|
|
96
|
+
|
|
97
|
+
# ------------------------------------------------- animation primitives ------
|
|
98
|
+
# Each returns {(x,y): rgb|None} for frame t of nf. `col` is the state's base.
|
|
99
|
+
def _fillall(m,col): return {p:col for p in m.filled}
|
|
100
|
+
def tri(t,nf): x=(2*t/nf); return x if x<=1 else 2-x # 0..1..0 triangle
|
|
101
|
+
|
|
102
|
+
def mo_breathe(m,col,t,nf,P,amt=16):
|
|
103
|
+
return _fillall(m, shade(col, amt*tri(t,nf)))
|
|
104
|
+
def mo_breathe_slow(m,col,t,nf,P): return mo_breathe(m,col,t,nf,P,amt=10)
|
|
105
|
+
def mo_pulse(m,col,t,nf,P):
|
|
106
|
+
k=tri(t,nf); return _fillall(m, tuple(round(col[i]+(P["acc_hot"][i] if col==P["accent"] else shade(col,55)[i]-col[i])*0 + (shade(col,42)[i]-col[i])*k) for i in range(3)))
|
|
107
|
+
def mo_blink(m,col,t,nf,P): return _fillall(m, col if t%2==0 else shade(col,-60))
|
|
108
|
+
def mo_arc(m,col,t,nf,P,win=80,hot=None):
|
|
109
|
+
px=_fillall(m,P["acc_dim"]); hot=hot or P["acc_hot"]; ang0=(t/nf)*2*math.pi; w=math.radians(win)
|
|
110
|
+
for (x,y) in m.ring:
|
|
111
|
+
a=m.ang(x,y); da=min((a-ang0)%(2*math.pi),(ang0-a)%(2*math.pi))
|
|
112
|
+
if da<=w/2: px[(x,y)]=hot
|
|
113
|
+
for p in m.core: px[p]=col
|
|
114
|
+
return px
|
|
115
|
+
def mo_scan(m,col,t,nf,P): return mo_arc(m,col,t,nf,P,win=34,hot=P["acc_spark"])
|
|
116
|
+
def mo_spin(m,col,t,nf,P): return mo_arc(m,col,t,nf,P,win=54)
|
|
117
|
+
def mo_dualarc(m,col,t,nf,P):
|
|
118
|
+
px=_fillall(m,P["acc_dim"]); ang0=(t/nf)*2*math.pi; w=math.radians(70)
|
|
119
|
+
for (x,y) in m.ring:
|
|
120
|
+
a=m.ang(x,y)
|
|
121
|
+
for b in (ang0,ang0+math.pi):
|
|
122
|
+
if min((a-b)%(2*math.pi),(b-a)%(2*math.pi))<=w/2: px[(x,y)]=P["acc_hot"]
|
|
123
|
+
for p in m.core: px[p]=col
|
|
124
|
+
return px
|
|
125
|
+
def mo_fill(m,col,t,nf,P,core_from=0.0):
|
|
126
|
+
px=_fillall(m,P["acc_dim"]); prog=(t+1)/nf
|
|
127
|
+
for (x,y) in m.ring:
|
|
128
|
+
a=(m.ang(x,y)+math.pi/2)%(2*math.pi)
|
|
129
|
+
if a/(2*math.pi)<=prog: px[(x,y)]=col
|
|
130
|
+
for p in m.core:
|
|
131
|
+
if prog>=core_from: px[p]=col
|
|
132
|
+
return px
|
|
133
|
+
def mo_boot(m,col,t,nf,P): return mo_fill(m,col,t,nf,P,core_from=0.7)
|
|
134
|
+
def mo_dots(m,col,t,nf,P):
|
|
135
|
+
px=_fillall(m,col); lit=t%max(1,len(m.dots))
|
|
136
|
+
for i,p in enumerate(m.dots): px[p]=P["acc_spark"] if i==lit else P["acc_deep"]
|
|
137
|
+
return px
|
|
138
|
+
def mo_sparkout(m,col,t,nf,P):
|
|
139
|
+
px=_fillall(m,col); step=[0,1,2,1][t%4]
|
|
140
|
+
for p in m.sparks[step]: px[p]=P["acc_spark"] if step==2 else P["acc_hot"]
|
|
141
|
+
return px
|
|
142
|
+
def mo_sparkin(m,col,t,nf,P):
|
|
143
|
+
px=_fillall(m,col); step=[2,1,0,1][t%4]
|
|
144
|
+
for p in m.sparks[step]: px[p]=P["acc_spark"]
|
|
145
|
+
return px
|
|
146
|
+
def mo_celebrate(m,col,t,nf,P):
|
|
147
|
+
cyc=[P["green"],P["amber"],P["accent"],P["acc_spark"]]
|
|
148
|
+
px=_fillall(m,cyc[t%len(cyc)]); step=[0,1,2,2][t%4]
|
|
149
|
+
for p in m.sparks[step]: px[p]=P["acc_spark"] if t%2 else P["white"]
|
|
150
|
+
return px
|
|
151
|
+
|
|
152
|
+
MOTION = {"breathe":mo_breathe,"breathe_slow":mo_breathe_slow,"pulse":mo_pulse,"blink":mo_blink,
|
|
153
|
+
"arc":mo_arc,"scan":mo_scan,"spin":mo_spin,"dualarc":mo_dualarc,"fill":mo_fill,"boot":mo_boot,
|
|
154
|
+
"dots":mo_dots,"sparkout":mo_sparkout,"sparkin":mo_sparkin,"celebrate":mo_celebrate}
|
|
155
|
+
|
|
156
|
+
# ------------------------------------------------- state catalog (32) --------
|
|
157
|
+
# state: (group, colour-role, motion, badge|None, fps)
|
|
158
|
+
A="accent"
|
|
159
|
+
CATALOG = {
|
|
160
|
+
# power / lifecycle
|
|
161
|
+
"off":("power","grey","breathe_slow",None,6), "booting":("power",A,"boot",None,8),
|
|
162
|
+
"idle":("power","acc_dim","breathe",None,6), "active":("power",A,"breathe",None,7),
|
|
163
|
+
"paused":("power","amber","breathe",("pause","amber_d","white"),6),
|
|
164
|
+
"offline":("power","grey","breathe_slow",("slash","grey_d","white"),4),
|
|
165
|
+
# work / process
|
|
166
|
+
"thinking":("work",A,"arc",None,11), "loading":("work",A,"fill",None,9),
|
|
167
|
+
"syncing":("work",A,"dualarc",None,10), "writing":("work",A,"dots",None,5),
|
|
168
|
+
"streaming":("work",A,"pulse",None,6), "queued":("work","amber","breathe",("clock","amber_d","white"),6),
|
|
169
|
+
"scanning":("work",A,"scan",None,16), "deploying":("work",A,"fill",("up","ink","white"),9),
|
|
170
|
+
# communication
|
|
171
|
+
"speaking":("comms",A,"sparkout",None,5), "listening":("comms","blue","sparkin",None,5),
|
|
172
|
+
"notification":("comms",A,"pulse",("dot","white","accent"),6),
|
|
173
|
+
"mention":("comms","blue","pulse",("dot","white","blue"),6),
|
|
174
|
+
"recording":("comms","red","pulse",("dot","white","red"),7),
|
|
175
|
+
# outcomes
|
|
176
|
+
"success":("outcome","green","breathe",None,7), "warning":("outcome","amber","pulse",None,6),
|
|
177
|
+
"critical":("outcome","red","blink",None,2), "blocked":("outcome","red","blink",("lock","ink","white"),2),
|
|
178
|
+
"error":("outcome","red","blink",("x","ink","white"),2),
|
|
179
|
+
"retrying":("outcome",A,"spin",None,12),
|
|
180
|
+
# governance / domain
|
|
181
|
+
"awaiting":("gov","amber","breathe",("clock","amber_d","white"),5),
|
|
182
|
+
"approved":("gov","green","breathe",("check","white","green"),7),
|
|
183
|
+
"rejected":("gov","red","breathe",("x","ink","white"),6),
|
|
184
|
+
"secure":("gov","green","breathe",("shield","green_d","white"),7),
|
|
185
|
+
"vulnerable":("gov","red","pulse",("shield","ink","white"),6),
|
|
186
|
+
"maintenance":("gov","grey","breathe",("gear","grey_d","white"),5),
|
|
187
|
+
"celebrating":("gov","green","celebrate",None,7),
|
|
188
|
+
}
|
|
189
|
+
GROUPS=["power","work","comms","outcome","gov"]
|
|
190
|
+
|
|
191
|
+
def _color(role,P): return P[role] if role in P else P.get(role, P["accent"])
|
|
192
|
+
|
|
193
|
+
def frames_for(m,state,P):
|
|
194
|
+
grp,role,motion,badge,fps=CATALOG[state]
|
|
195
|
+
col=_color(role,P); fn=MOTION[motion]
|
|
196
|
+
nf={"breathe":10,"breathe_slow":10,"pulse":8,"blink":2,"arc":12,"scan":12,"spin":10,
|
|
197
|
+
"dualarc":12,"fill":12,"boot":10,"dots":max(2,len(m.dots)*2),"sparkout":4,"sparkin":4,"celebrate":8}[motion]
|
|
198
|
+
out=[]
|
|
199
|
+
for t in range(nf):
|
|
200
|
+
px=fn(m,col,t,nf,P)
|
|
201
|
+
if badge:
|
|
202
|
+
nm,bg,fg=badge
|
|
203
|
+
put_badge(px, nm, P[bg], P[fg], m.N)
|
|
204
|
+
out.append(px)
|
|
205
|
+
return out, fps
|
|
206
|
+
|
|
207
|
+
# ------------------------------------------------------------- renderers -----
|
|
208
|
+
def render(px,N,scale):
|
|
209
|
+
img=Image.new("RGBA",(N*scale,N*scale),(0,0,0,0)); d=ImageDraw.Draw(img)
|
|
210
|
+
for (x,y),c in px.items():
|
|
211
|
+
if c is not None: d.rectangle([x*scale,y*scale,(x+1)*scale-1,(y+1)*scale-1],fill=c+(255,))
|
|
212
|
+
return img
|
|
213
|
+
def svg_of(px,N):
|
|
214
|
+
from collections import defaultdict
|
|
215
|
+
by=defaultdict(list)
|
|
216
|
+
for (x,y),c in px.items():
|
|
217
|
+
if c is not None: by[c].append((x,y))
|
|
218
|
+
parts=[]
|
|
219
|
+
for c,cells in by.items():
|
|
220
|
+
rs="".join(f'<rect x="{x}" y="{y}" width="1" height="1"/>' for x,y in sorted(cells))
|
|
221
|
+
parts.append(f'<g fill="#%02x%02x%02x">{rs}</g>'%c)
|
|
222
|
+
return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {N} {N}" '
|
|
223
|
+
f'shape-rendering="crispEdges" role="img">{"".join(parts)}</svg>\n')
|
|
224
|
+
|
|
225
|
+
def generate(m:Motif, accent="#ff1212", out="oneman", title="ONEMAN", scale=9):
|
|
226
|
+
P=palette(accent); base=os.path.join(OUT_BASE, out); os.makedirs(base, exist_ok=True)
|
|
227
|
+
meta={"title":title,"accent":accent,"N":m.N,"scale":scale,"groups":GROUPS,"states":{}}
|
|
228
|
+
for s in CATALOG:
|
|
229
|
+
frames,fps=frames_for(m,s,P)
|
|
230
|
+
imgs=[render(px,m.N,scale) for px in frames]
|
|
231
|
+
w=sum(i.width for i in imgs); strip=Image.new("RGBA",(w,imgs[0].height),(0,0,0,0)); x=0
|
|
232
|
+
for i in imgs: strip.paste(i,(x,0)); x+=i.width
|
|
233
|
+
strip.save(os.path.join(base,f"anim-{s}.png"))
|
|
234
|
+
open(os.path.join(base,f"mark-{s}.svg"),"w").write(svg_of(frames[0],m.N)) # still fallback
|
|
235
|
+
grp,role,motion,badge,_=CATALOG[s]
|
|
236
|
+
meta["states"][s]={"group":grp,"frames":len(frames),"fps":fps,"motion":motion,
|
|
237
|
+
"badge":badge[0] if badge else None}
|
|
238
|
+
# overview sheet (representative frame 2 of each)
|
|
239
|
+
cols,rows,cell=8,4,7; tile=m.N*cell; pad=cell
|
|
240
|
+
sheet=Image.new("RGBA",(cols*tile+(cols+1)*pad,rows*tile+(rows+1)*pad),(16,16,24,255))
|
|
241
|
+
order=[s for g in GROUPS for s in CATALOG if CATALOG[s][0]==g]
|
|
242
|
+
def rep_idx(motion,n):
|
|
243
|
+
return {"blink":0,"fill":int(n*0.62),"boot":int(n*0.62),"arc":n//4,"scan":n//4,
|
|
244
|
+
"spin":n//4,"dualarc":n//4,"dots":1}.get(motion, n//2)
|
|
245
|
+
for i,s in enumerate(order):
|
|
246
|
+
fr,_=frames_for(m,s,P); im=render(fr[rep_idx(CATALOG[s][2],len(fr))],m.N,cell)
|
|
247
|
+
cx=pad+(i%cols)*(tile+pad); cy=pad+(i//cols)*(tile+pad); sheet.paste(im,(cx,cy),im)
|
|
248
|
+
sheet.save(os.path.join(base,"sprite-sheet.png"))
|
|
249
|
+
json.dump(meta, open(os.path.join(base,"states.json"),"w"), indent=1)
|
|
250
|
+
return meta
|
|
251
|
+
|
|
252
|
+
if __name__=="__main__":
|
|
253
|
+
meta=generate(Motif.oneman(), accent="#ff1212", out="oneman", title="ONEMAN")
|
|
254
|
+
print(f"{title if (title:=meta['title']) else ''}: {len(meta['states'])} states, all animated")
|
|
255
|
+
print("groups:", ", ".join(GROUPS))
|
|
256
|
+
print("states:", ", ".join(meta["states"]))
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""ONEMAN Core-Agent faces — the O mark IS the face.
|
|
3
|
+
Thin accent rim (bigger face) -> larger, varied eyes. Mixed gender; women wear
|
|
4
|
+
long hair. Distinct hair silhouette + one personality prop per agent."""
|
|
5
|
+
import math
|
|
6
|
+
from PIL import Image, ImageDraw
|
|
7
|
+
W=H=28
|
|
8
|
+
def _rgb(h):h=h.lstrip('#');return(int(h[0:2],16),int(h[2:4],16),int(h[4:6],16))
|
|
9
|
+
def shade(c,a):
|
|
10
|
+
f=lambda v:max(0,min(255,round(v+(a/100)*((255-v)if a>0 else v))));return(f(c[0]),f(c[1]),f(c[2]))
|
|
11
|
+
OUT=_rgb('14141e');WHT=_rgb('f7f7fb');EYE=_rgb('1b1b26')
|
|
12
|
+
cx,cy=14,13;R_OUT=11.0;R_IN=8.6 # thin rim -> big face
|
|
13
|
+
SKINS={"light":_rgb('f0c49a'),"mid":_rgb('d89c70'),"tan":_rgb('c6895a'),"deep":_rgb('9c6a44')}
|
|
14
|
+
def rect(px,x0,y0,x1,y1,c):
|
|
15
|
+
for y in range(max(0,y0),min(H,y1+1)):
|
|
16
|
+
for x in range(max(0,x0),min(W,x1+1)):px[(x,y)]=c
|
|
17
|
+
def ytop(x):
|
|
18
|
+
dx=x-cx;return (cy-math.sqrt(R_OUT**2-dx*dx)) if abs(dx)<=R_OUT else None
|
|
19
|
+
|
|
20
|
+
def draw_eyes(px,style,SK):
|
|
21
|
+
"""Bigger eyes, varied. Left cols cx-5..-3, right cx+3..+5."""
|
|
22
|
+
def one(side,lash=False,shut=False,small=False,wide=False):
|
|
23
|
+
s=side
|
|
24
|
+
L=[cx+ s*3,cx+s*4,cx+s*5]
|
|
25
|
+
if shut:
|
|
26
|
+
for x in L:px[(x,cy-1)]=OUT
|
|
27
|
+
if lash:px[(cx+s*6,cy-2)]=OUT
|
|
28
|
+
return
|
|
29
|
+
for x in L: # white block 3x2
|
|
30
|
+
px[(x,cy-2)]=WHT;px[(x,cy-1)]=WHT
|
|
31
|
+
pupx=cx+s*4
|
|
32
|
+
if wide:
|
|
33
|
+
px[(pupx,cy-1)]=EYE # small pupil, lots of white
|
|
34
|
+
elif small:
|
|
35
|
+
px[(pupx,cy-1)]=EYE
|
|
36
|
+
else:
|
|
37
|
+
px[(pupx,cy-1)]=EYE;px[(pupx,cy-2)]=EYE # big pupil
|
|
38
|
+
px[(cx+s*3 if s<0 else cx+s*5,cy-2)]=WHT # keep a white glint corner
|
|
39
|
+
if lash:
|
|
40
|
+
px[(cx+s*6,cy-2)]=OUT;px[(cx+s*5,cy-3)]=OUT;px[(cx+s*3,cy-2)]=OUT
|
|
41
|
+
if style=="wink":
|
|
42
|
+
one(-1);one(1,shut=True)
|
|
43
|
+
elif style=="lash":
|
|
44
|
+
one(-1,lash=True);one(1,lash=True)
|
|
45
|
+
elif style=="wide":
|
|
46
|
+
one(-1,wide=True);one(1,wide=True)
|
|
47
|
+
elif style=="small":
|
|
48
|
+
one(-1,small=True);one(1,small=True)
|
|
49
|
+
elif style=="happy":
|
|
50
|
+
for s in(-1,1): px[(cx+s*5,cy-1)]=OUT;px[(cx+s*4,cy-2)]=OUT;px[(cx+s*3,cy-1)]=OUT
|
|
51
|
+
else: # round
|
|
52
|
+
one(-1);one(1)
|
|
53
|
+
|
|
54
|
+
def hair_crown(px,HA,HAs,HAh,style):
|
|
55
|
+
def up(dx):
|
|
56
|
+
a=abs(dx)
|
|
57
|
+
return {"buzz":0,"short":1,"slick":1,"spiky":(3 if dx%2==0 else 1),
|
|
58
|
+
"longstraight":1,"longwavy":1,"ponytail":1,"longbob":1}.get(style,1)
|
|
59
|
+
def down(dx):
|
|
60
|
+
return {"buzz":1,"short":2,"slick":(3 if dx>0 else 1)}.get(style,2)
|
|
61
|
+
for x in range(cx-11,cx+12):
|
|
62
|
+
yt=ytop(x)
|
|
63
|
+
if yt is None:continue
|
|
64
|
+
base=round(yt)
|
|
65
|
+
if base>cy-2:continue
|
|
66
|
+
u=up(x-cx)
|
|
67
|
+
topy=base-u;boty=min(base+down(x-cx),cy-3)
|
|
68
|
+
for y in range(topy,boty+1):
|
|
69
|
+
if 0<=y<H:px[(x,y)]=HA
|
|
70
|
+
if 0<=topy<H:px[(x,topy)]=HAh
|
|
71
|
+
if 0<=boty<H and px.get((x,boty))==HA:px[(x,boty)]=HAs
|
|
72
|
+
|
|
73
|
+
def hair_sides(px,HA,HAs,style):
|
|
74
|
+
# women's long hair: side panels of different length/shape
|
|
75
|
+
for x in range(cx-11,cx+12):
|
|
76
|
+
dx=x-cx;a=abs(dx)
|
|
77
|
+
if a<6 or a>10:continue
|
|
78
|
+
yt=ytop(x)
|
|
79
|
+
if yt is None:continue
|
|
80
|
+
top=round(yt)
|
|
81
|
+
if style=="longstraight":L=cy+7
|
|
82
|
+
elif style=="longwavy":L=cy+6+(1 if (x%2==0) else -1)
|
|
83
|
+
elif style=="longbob":L=cy+2
|
|
84
|
+
elif style=="ponytail":L=cy
|
|
85
|
+
else:L=cy-1
|
|
86
|
+
for y in range(top,min(H,L+1)):px[(x,y)]=HA
|
|
87
|
+
if L<H:px[(x,min(H-1,L))]=HAs
|
|
88
|
+
if style=="ponytail":
|
|
89
|
+
for yy in range(cy-4,cy+5): # tail on the right
|
|
90
|
+
for xx in range(cx+9,cx+12):
|
|
91
|
+
if abs((yy-cy))<=4-(xx-(cx+9)):px[(xx,yy)]=HA
|
|
92
|
+
px[(cx+9,cy+4)]=HAs
|
|
93
|
+
px[(cx+8,cy-5)]=HA # tie base
|
|
94
|
+
|
|
95
|
+
def face(accent,hair,skin="mid",*,gender="m",hairstyle="short",eyes="round",
|
|
96
|
+
mouth="smile",prop=None,propcol="#caa472",hoodie=False,band=None,mask=None,specs=False):
|
|
97
|
+
AC=_rgb(accent);ACd=shade(AC,-30);SK=SKINS[skin];SKs=shade(SK,-20)
|
|
98
|
+
HA=_rgb(hair);HAs=shade(HA,-28);HAh=shade(HA,22)
|
|
99
|
+
px={}
|
|
100
|
+
for y in range(H):
|
|
101
|
+
for x in range(W):
|
|
102
|
+
d=math.hypot(x-cx,y-cy)
|
|
103
|
+
if d<=R_OUT:px[(x,y)]=(AC if d<R_OUT-0.9 else ACd) if d>=R_IN else SK
|
|
104
|
+
for y in range(0,cy-3):
|
|
105
|
+
for x in range(cx-2,cx+3):
|
|
106
|
+
if px.get((x,y)) in(AC,ACd):del px[(x,y)]
|
|
107
|
+
for x in range(cx-5,cx+6):
|
|
108
|
+
if px.get((x,cy+5))==SK:px[(x,cy+5)]=SKs
|
|
109
|
+
if gender=="f":hair_sides(px,HA,HAs,hairstyle) # draw long hair behind/around first
|
|
110
|
+
if hairstyle!="bald":hair_crown(px,HA,HAs,HAh,hairstyle)
|
|
111
|
+
draw_eyes(px,eyes,SK)
|
|
112
|
+
px[(cx,cy+2)]=SKs # nose
|
|
113
|
+
if mouth=="smile":
|
|
114
|
+
px[(cx-2,cy+4)]=OUT;px[(cx-1,cy+5)]=OUT;px[(cx,cy+5)]=OUT;px[(cx+1,cy+5)]=OUT;px[(cx+2,cy+4)]=OUT
|
|
115
|
+
elif mouth=="flat":rect(px,cx-2,cy+4,cx+1,cy+4,OUT)
|
|
116
|
+
elif mouth=="smirk":
|
|
117
|
+
px[(cx-1,cy+5)]=OUT;px[(cx,cy+5)]=OUT;px[(cx+1,cy+4)]=OUT;px[(cx+2,cy+3)]=OUT
|
|
118
|
+
elif mouth=="open":rect(px,cx-2,cy+4,cx+1,cy+5,OUT);px[(cx-1,cy+5)]=_rgb('7a1a12')
|
|
119
|
+
elif mouth=="lips":rect(px,cx-2,cy+4,cx+1,cy+4,_rgb('c0392b')) # lipstick
|
|
120
|
+
# props
|
|
121
|
+
if prop=="cigar":
|
|
122
|
+
PC=_rgb(propcol);rect(px,cx+2,cy+4,cx+8,cy+4,PC);px[(cx+2,cy+4)]=shade(PC,-25)
|
|
123
|
+
px[(cx+8,cy+4)]=_rgb('ff4422');px[(cx+9,cy+4)]=_rgb('ffb08a')
|
|
124
|
+
px[(cx+9,cy+2)]=_rgb('8a8a9a');px[(cx+10,cy+1)]=_rgb('6a6a7a')
|
|
125
|
+
elif prop=="shades":
|
|
126
|
+
rect(px,cx-6,cy-2,cx-2,cy,OUT);rect(px,cx+2,cy-2,cx+6,cy,OUT)
|
|
127
|
+
rect(px,cx-1,cy-2,cx+1,cy-2,OUT);px[(cx-7,cy-2)]=OUT;px[(cx+7,cy-2)]=OUT
|
|
128
|
+
px[(cx-5,cy-1)]=_rgb('3a3a4a');px[(cx+3,cy-1)]=_rgb('3a3a4a')
|
|
129
|
+
elif prop=="glasses":
|
|
130
|
+
for x in range(cx-6,cx-1):px[(x,cy-2)]=OUT;px[(x,cy)]=OUT
|
|
131
|
+
px[(cx-6,cy-1)]=OUT;px[(cx-2,cy-1)]=OUT
|
|
132
|
+
for x in range(cx+2,cx+7):px[(x,cy-2)]=OUT;px[(x,cy)]=OUT
|
|
133
|
+
px[(cx+1,cy-1)]=OUT;px[(cx+6,cy-1)]=OUT;px[(cx,cy-1)]=OUT
|
|
134
|
+
elif prop=="monocle":
|
|
135
|
+
for x in range(cx+2,cx+7):px[(x,cy-2)]=OUT;px[(x,cy)]=OUT
|
|
136
|
+
px[(cx+1,cy-1)]=OUT;px[(cx+6,cy-1)]=OUT
|
|
137
|
+
px[(cx+4,cy+1)]=_rgb('d4af37');px[(cx+4,cy+2)]=_rgb('d4af37')
|
|
138
|
+
elif prop=="pen":
|
|
139
|
+
PC=_rgb(propcol)
|
|
140
|
+
for i in range(5):
|
|
141
|
+
x=cx+8+i;y=cy-2-i
|
|
142
|
+
if 0<=x<W and 0<=y<H:px[(x,y)]=PC
|
|
143
|
+
px[(cx+8,cy-2)]=_rgb('2a2a34');px[(cx+12,cy-6)]=shade(PC,30)
|
|
144
|
+
elif prop=="headset":
|
|
145
|
+
rect(px,cx-12,cy-6,cx-12,cy+1,OUT);px[(cx-12,cy+2)]=OUT
|
|
146
|
+
rect(px,cx-13,cy+1,cx-12,cy+3,EYE);rect(px,cx-12,cy+3,cx-6,cy+4,OUT);px[(cx-6,cy+4)]=_rgb('ff4422')
|
|
147
|
+
elif prop=="earbuds":
|
|
148
|
+
px[(cx-9,cy)]=WHT;px[(cx-9,cy+1)]=WHT;px[(cx+9,cy)]=WHT;px[(cx+9,cy+1)]=WHT
|
|
149
|
+
elif prop=="bandana":
|
|
150
|
+
BC=_rgb(propcol)
|
|
151
|
+
for x in range(cx-11,cx+12):
|
|
152
|
+
yt=ytop(x)
|
|
153
|
+
if yt is None:continue
|
|
154
|
+
base=round(yt)
|
|
155
|
+
for y in range(base-1,base+3):
|
|
156
|
+
if 0<=y<H and y<=cy-3 and px.get((x,y)) not in(None,SK,SKs):px[(x,y)]=BC
|
|
157
|
+
px[(cx-10,cy-2)]=BC;px[(cx-11,cy-1)]=BC;px[(cx-11,cy)]=shade(BC,-22)
|
|
158
|
+
for x in range(cx-6,cx+7,3):px[(x,cy-5)]=shade(BC,-22)
|
|
159
|
+
# ---- combinable wearables (Founder) ----
|
|
160
|
+
if hoodie:
|
|
161
|
+
HC=_rgb('161b22')
|
|
162
|
+
for y in range(cy-2,H):
|
|
163
|
+
for x in range(W):
|
|
164
|
+
d=math.hypot(x-cx,y-cy)
|
|
165
|
+
if R_OUT<d<=R_OUT+2.4 and px.get((x,y)) is None: px[(x,y)]=HC
|
|
166
|
+
if band:
|
|
167
|
+
BC=_rgb(band); BCd=shade(BC,-24)
|
|
168
|
+
for y in range(0,cy-6): # hair crown above the (raised) band
|
|
169
|
+
for x in range(cx-9,cx+10):
|
|
170
|
+
if px.get((x,y))==SK: px[(x,y)]=HA
|
|
171
|
+
for y in (cy-6,cy-5,cy-4): # headband, raised a touch
|
|
172
|
+
for x in range(cx-10,cx+11):
|
|
173
|
+
yt=ytop(x)
|
|
174
|
+
if yt is None: continue
|
|
175
|
+
if y>=round(yt)-1 and px.get((x,y)) is not None: px[(x,y)]=BC
|
|
176
|
+
for strand in (0,1): # two tails, both streaming up-right (wind-blown)
|
|
177
|
+
for k in range(6):
|
|
178
|
+
xx=cx+8+k; yy=(cy-2-strand*2)-(k*2)//3
|
|
179
|
+
if 0<=xx<W and 0<=yy<H: px[(xx,yy)]=BC if k<3 else BCd
|
|
180
|
+
if mask:
|
|
181
|
+
MC=_rgb(mask);MCs=shade(MC,34);GL=_rgb('39d353')
|
|
182
|
+
for y in range(cy+1,cy+10): # dark mask over nose+mouth+chin (follows the face)
|
|
183
|
+
for x in range(cx-8,cx+9):
|
|
184
|
+
if px.get((x,y)) in(SK,SKs): px[(x,y)]=MC
|
|
185
|
+
for x in range(cx-5,cx+6):
|
|
186
|
+
if px.get((x,cy+1))==MC: px[(x,cy+1)]=MCs # ridge over the nose
|
|
187
|
+
for y in range(cy+2,cy+8):
|
|
188
|
+
if px.get((cx,y))==MC: px[(cx,y)]=MCs # centre seam
|
|
189
|
+
for k in range(3): px[(cx+3,cy+4+k)]=GL;px[(cx+4,cy+4+k)]=GL # green vent stripes
|
|
190
|
+
if specs:
|
|
191
|
+
LENS=_rgb('1f8a3a')
|
|
192
|
+
rect(px,cx-6,cy-2,cx-2,cy,OUT); rect(px,cx-5,cy-2,cx-3,cy-1,LENS); px[(cx-4,cy-1)]=_rgb('0c1a10')
|
|
193
|
+
rect(px,cx+2,cy-2,cx+6,cy,OUT); rect(px,cx+3,cy-2,cx+5,cy-1,LENS); px[(cx+4,cy-1)]=_rgb('0c1a10')
|
|
194
|
+
px[(cx-1,cy-1)]=OUT;px[(cx,cy-1)]=OUT;px[(cx+1,cy-1)]=OUT # bridge
|
|
195
|
+
return px
|
|
196
|
+
|
|
197
|
+
FOUNDER=("FOUNDER","#2fbf52","m",dict(hair="#141419",skin="light",hairstyle="short",eyes="happy",mouth="flat",band="#e23b3b",mask="#12201a",hoodie=True,specs=True))
|
|
198
|
+
|
|
199
|
+
def render(px,scale,bg=None):
|
|
200
|
+
img=Image.new("RGBA",(W*scale,H*scale),(bg or(0,0,0,0)));d=ImageDraw.Draw(img)
|
|
201
|
+
for (x,y),c in px.items():
|
|
202
|
+
if c is not None:d.rectangle([x*scale,y*scale,(x+1)*scale-1,(y+1)*scale-1],fill=c+(255,))
|
|
203
|
+
return img
|
|
204
|
+
|
|
205
|
+
CAST=[
|
|
206
|
+
("CEO","#f59e0b","m",dict(hair="#2a2018",skin="mid", hairstyle="slick",eyes="round",mouth="smirk",prop="cigar")),
|
|
207
|
+
("CPO","#8b5cf6","f",dict(hair="#3a2418",skin="tan", hairstyle="longwavy",eyes="lash",mouth="smile",prop="pen",propcol="#f2c75a")),
|
|
208
|
+
("CMO","#ec4899","f",dict(hair="#4a2a18",skin="light",hairstyle="longstraight",eyes="lash",mouth="lips",prop="shades")),
|
|
209
|
+
("CRO","#84cc16","m",dict(hair="#1c1c22",skin="mid", hairstyle="spiky",eyes="wide",mouth="open",prop="headset")),
|
|
210
|
+
("CTO","#3b82f6","m",dict(hair="#1c1c22",skin="tan", hairstyle="buzz",eyes="round",mouth="flat",prop="glasses")),
|
|
211
|
+
("COO","#06b6d4","f",dict(hair="#15151c",skin="deep", hairstyle="ponytail",eyes="lash",mouth="smile",prop="earbuds")),
|
|
212
|
+
("CFO","#2dd4bf","f",dict(hair="#6a4a2a",skin="light",hairstyle="longbob",eyes="lash",mouth="flat",prop="monocle")),
|
|
213
|
+
("CSO","#6366f1","m",dict(hair="#1c1c22",skin="mid", hairstyle="short",eyes="wide",mouth="flat",prop="bandana",propcol="#ef4444")),
|
|
214
|
+
]
|
|
215
|
+
if __name__=="__main__":
|
|
216
|
+
scale=10;gap=14;lblh=22;cols=4;rows=2;cw=W*scale;ch=H*scale
|
|
217
|
+
sheet=Image.new("RGBA",(cols*cw+(cols+1)*gap, rows*(ch+lblh)+gap),(13,13,20,255));d=ImageDraw.Draw(sheet)
|
|
218
|
+
for i,(nm,acc,g,p) in enumerate(CAST):
|
|
219
|
+
im=render(face(acc,gender=g,**p),scale)
|
|
220
|
+
x=gap+(i%cols)*(cw+gap);y=lblh+(i//cols)*(ch+lblh)
|
|
221
|
+
sheet.paste(im,(x,y),im);d.text((x+4,y-16),f"{nm} {'F' if g=='f' else 'M'} {p['hairstyle']}/{p['prop']}",fill=(220,220,235))
|
|
222
|
+
sheet.save("/tmp/face_preview.png");print("wrote",sheet.size)
|
package/package.json
CHANGED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Goal-led AI agent design
|
|
2
|
+
|
|
3
|
+
A reusable method for defining the agents in a multi-agent product so a single
|
|
4
|
+
human can run a company by stating intent, not by operating tools. Distilled
|
|
5
|
+
from the ONEMAN.APP cofounder build; pairs with the `maggie-spirit` skill (give
|
|
6
|
+
each designed agent a spirit/avatar) and `maggie-assistant-tools` (expose an
|
|
7
|
+
agent's capabilities safely).
|
|
8
|
+
|
|
9
|
+
## Principle
|
|
10
|
+
|
|
11
|
+
The human sets a **goal**; the agents turn it into work and keep the human in
|
|
12
|
+
the loop only for judgement and approval. Each agent owns one lane, declares the
|
|
13
|
+
**capability classes** it may use, and runs under a **risk ceiling** with
|
|
14
|
+
explicit approval gates. Nothing that is risky, public, or spends money happens
|
|
15
|
+
without a human yes.
|
|
16
|
+
|
|
17
|
+
## Define each agent with a role contract
|
|
18
|
+
|
|
19
|
+
One `role.yaml` per agent. Recommended fields:
|
|
20
|
+
|
|
21
|
+
```yaml
|
|
22
|
+
role: CTO # the lane
|
|
23
|
+
name: AI CTO # display name
|
|
24
|
+
persona: Turing # inspiration, not impersonation
|
|
25
|
+
spirit: Engineering # one-word identity (drives the spirit accent/motif)
|
|
26
|
+
summary: > # what it owns, in one or two sentences
|
|
27
|
+
Technical design, build, integrate, test and deploy. Production deployment is
|
|
28
|
+
gated behind human approval.
|
|
29
|
+
responsibilities: [design, build, integrate, test, deploy, observe]
|
|
30
|
+
constraints: [no prod deploy without approval, no secrets in source]
|
|
31
|
+
allowed_capability_classes: [app.scaffold, app.build, app.test, code.review,
|
|
32
|
+
deploy.release, integration.connect, ops.observe, job.execute]
|
|
33
|
+
default_risk_ceiling: L1 # L0 read -> L3 irreversible/public/paid
|
|
34
|
+
business_system: Execution # which paired system (e.g. with COO)
|
|
35
|
+
raci: { new_service_app: "A", security_verdict: I }
|
|
36
|
+
interfaces: { mcp: planned, cli: planned, api: planned }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- **Capability classes**, not freeform skills, are the unit of authority. They
|
|
40
|
+
are stable `area.verb` identifiers (`content.publish`, `revenue.close`,
|
|
41
|
+
`security.scan`) that a UI can list and a gate can check. These are the
|
|
42
|
+
"skills" a product surfaces per agent.
|
|
43
|
+
- **One CEO orchestrates.** The CEO owns `goal.decompose`, `mission.manage`,
|
|
44
|
+
`plan.compose`, `priority.rank`, `outcome.accept`: it turns intent into
|
|
45
|
+
missions with acceptance criteria, delegates each to the right agent, and
|
|
46
|
+
signs off - it never bypasses security or budget approval.
|
|
47
|
+
- **Security holds a non-overridable veto.** A high-risk security verdict cannot
|
|
48
|
+
be overruled by another agent.
|
|
49
|
+
- **Finance is explain-only.** Financial facts come from deterministic data
|
|
50
|
+
(billing/usage ledger, Stripe); the CFO reconciles and advises, it is not the
|
|
51
|
+
source of truth.
|
|
52
|
+
|
|
53
|
+
## Risk ceilings and approval gates
|
|
54
|
+
|
|
55
|
+
| Level | Meaning | Gate |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| L0 | read-only | none |
|
|
58
|
+
| L1 | internal writes (drafts, plans, workflows) | logged |
|
|
59
|
+
| L2 | customer-facing or public (publish, close a deal) | human approval |
|
|
60
|
+
| L3 | irreversible / paid / production (deploy, transfer) | human approval + audit |
|
|
61
|
+
|
|
62
|
+
Every agent declares a `default_risk_ceiling`; an action above it stops for an
|
|
63
|
+
explicit human yes. Log every action; make L2/L3 produce an approval card.
|
|
64
|
+
|
|
65
|
+
## The goal loop
|
|
66
|
+
|
|
67
|
+
1. The human states what the business should do.
|
|
68
|
+
2. The CEO writes missions with acceptance criteria.
|
|
69
|
+
3. Each mission is delegated to the agent whose lane and capability classes fit.
|
|
70
|
+
4. Agents work their lane; risky/public/paid steps wait for approval.
|
|
71
|
+
5. The CEO reviews outcomes against the acceptance criteria and signs off, or
|
|
72
|
+
sends the mission back.
|
|
73
|
+
6. The human stays the only human in the loop - approve, steer, ship.
|
|
74
|
+
|
|
75
|
+
## Surfacing agents to users
|
|
76
|
+
|
|
77
|
+
Each agent reads cleanly as a profile: persona name, spirit, its capability
|
|
78
|
+
classes (its "skills"), and the service apps it manages. Give it a spirit/avatar
|
|
79
|
+
with `maggie-spirit` (shared 32-state motion vocabulary, per-agent accent) so
|
|
80
|
+
status (idle, working, deploying, success, blocked, secure) reads the same
|
|
81
|
+
across the whole team. Mark an agent "shipped" only when its product is actually
|
|
82
|
+
live.
|
|
83
|
+
|
|
84
|
+
## Checklist
|
|
85
|
+
|
|
86
|
+
- [ ] One lane per agent; no two agents own the same decision.
|
|
87
|
+
- [ ] Capability classes are `area.verb`, stable, and enumerable.
|
|
88
|
+
- [ ] A single orchestrator (CEO) owns decomposition and sign-off.
|
|
89
|
+
- [ ] Security veto and finance-is-explain-only are encoded, not assumed.
|
|
90
|
+
- [ ] Every agent has a risk ceiling; L2/L3 actions gate on human approval.
|
|
91
|
+
- [ ] Persona is inspiration, not impersonation of a real person.
|
|
92
|
+
- [ ] Each agent has a spirit/avatar from the shared 32-state base set.
|