@topolo/cli 0.12.32 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/agent-packs/README.md +341 -0
- package/agent-packs/pyproject.toml +16 -0
- package/agent-packs/requirements-test.txt +1 -0
- package/agent-packs/src/topolo_agent_pack/__init__.py +1 -0
- package/agent-packs/src/topolo_agent_pack/blueprint.py +57 -0
- package/agent-packs/src/topolo_agent_pack/cli.py +416 -0
- package/agent-packs/src/topolo_agent_pack/fresh.py +158 -0
- package/agent-packs/src/topolo_agent_pack/live.py +362 -0
- package/agent-packs/src/topolo_agent_pack/native.py +645 -0
- package/agent-packs/src/topolo_agent_pack/storage.py +271 -0
- package/agent-packs/src/topolo_agent_pack/topolo.py +321 -0
- package/agent-packs/templates/career/AGENTS.md +17 -0
- package/agent-packs/templates/career/README.md +14 -0
- package/agent-packs/templates/career/agents/intakes/career-intake/AGENTS.md +21 -0
- package/agent-packs/templates/career/agents/specialists/application-preparation/AGENTS.md +21 -0
- package/agent-packs/templates/career/agents/specialists/application-tracker/AGENTS.md +21 -0
- package/agent-packs/templates/career/agents/specialists/interview-coach/AGENTS.md +21 -0
- package/agent-packs/templates/career/agents/specialists/opportunity-research/AGENTS.md +21 -0
- package/agent-packs/templates/career/agents/specialists/profile-cv/AGENTS.md +21 -0
- package/agent-packs/templates/career/pack.json +124 -0
- package/agent-packs/templates/career/shared/PACK.md +22 -0
- package/agent-packs/templates/career/shared/TOPOLO.md +13 -0
- package/agent-packs/templates/career/shared/WORKFLOW.md +13 -0
- package/agent-packs/templates/career/skills/topolo/SKILL.md +305 -0
- package/agent-packs/templates/clients/AGENTS.md +17 -0
- package/agent-packs/templates/clients/README.md +14 -0
- package/agent-packs/templates/clients/agents/intakes/client-intake/AGENTS.md +21 -0
- package/agent-packs/templates/clients/agents/specialists/acceptance-review/AGENTS.md +21 -0
- package/agent-packs/templates/clients/agents/specialists/delivery-planner/AGENTS.md +21 -0
- package/agent-packs/templates/clients/agents/specialists/discovery/AGENTS.md +21 -0
- package/agent-packs/templates/clients/agents/specialists/proposal-writer/AGENTS.md +21 -0
- package/agent-packs/templates/clients/agents/specialists/solution-design/AGENTS.md +21 -0
- package/agent-packs/templates/clients/pack.json +126 -0
- package/agent-packs/templates/clients/shared/PACK.md +22 -0
- package/agent-packs/templates/clients/shared/TOPOLO.md +13 -0
- package/agent-packs/templates/clients/shared/WORKFLOW.md +13 -0
- package/agent-packs/templates/clients/skills/topolo/SKILL.md +305 -0
- package/agent-packs/templates/creative/AGENTS.md +17 -0
- package/agent-packs/templates/creative/README.md +14 -0
- package/agent-packs/templates/creative/agents/intakes/creative-director/AGENTS.md +21 -0
- package/agent-packs/templates/creative/agents/specialists/brand-copy/AGENTS.md +21 -0
- package/agent-packs/templates/creative/agents/specialists/media-production/AGENTS.md +21 -0
- package/agent-packs/templates/creative/agents/specialists/quality-review/AGENTS.md +21 -0
- package/agent-packs/templates/creative/agents/specialists/storytelling/AGENTS.md +21 -0
- package/agent-packs/templates/creative/agents/specialists/visual-design/AGENTS.md +21 -0
- package/agent-packs/templates/creative/pack.json +128 -0
- package/agent-packs/templates/creative/shared/PACK.md +22 -0
- package/agent-packs/templates/creative/shared/TOPOLO.md +13 -0
- package/agent-packs/templates/creative/shared/WORKFLOW.md +13 -0
- package/agent-packs/templates/creative/skills/topolo/SKILL.md +305 -0
- package/agent-packs/templates/founder/AGENTS.md +17 -0
- package/agent-packs/templates/founder/README.md +14 -0
- package/agent-packs/templates/founder/agents/intakes/founder-intake/AGENTS.md +21 -0
- package/agent-packs/templates/founder/agents/specialists/commercial-modelling/AGENTS.md +21 -0
- package/agent-packs/templates/founder/agents/specialists/customer-research/AGENTS.md +21 -0
- package/agent-packs/templates/founder/agents/specialists/decision-review/AGENTS.md +21 -0
- package/agent-packs/templates/founder/agents/specialists/partnerships/AGENTS.md +21 -0
- package/agent-packs/templates/founder/agents/specialists/strategy/AGENTS.md +21 -0
- package/agent-packs/templates/founder/pack.json +126 -0
- package/agent-packs/templates/founder/shared/PACK.md +22 -0
- package/agent-packs/templates/founder/shared/TOPOLO.md +13 -0
- package/agent-packs/templates/founder/shared/WORKFLOW.md +13 -0
- package/agent-packs/templates/founder/skills/topolo/SKILL.md +305 -0
- package/agent-packs/templates/learning/AGENTS.md +17 -0
- package/agent-packs/templates/learning/README.md +14 -0
- package/agent-packs/templates/learning/agents/intakes/learning-intake/AGENTS.md +21 -0
- package/agent-packs/templates/learning/agents/specialists/critical-reviewer/AGENTS.md +21 -0
- package/agent-packs/templates/learning/agents/specialists/curriculum-designer/AGENTS.md +21 -0
- package/agent-packs/templates/learning/agents/specialists/knowledge-steward/AGENTS.md +21 -0
- package/agent-packs/templates/learning/agents/specialists/practice-coach/AGENTS.md +21 -0
- package/agent-packs/templates/learning/agents/specialists/tutor/AGENTS.md +21 -0
- package/agent-packs/templates/learning/pack.json +124 -0
- package/agent-packs/templates/learning/shared/PACK.md +22 -0
- package/agent-packs/templates/learning/shared/TOPOLO.md +13 -0
- package/agent-packs/templates/learning/shared/WORKFLOW.md +13 -0
- package/agent-packs/templates/learning/skills/topolo/SKILL.md +305 -0
- package/agent-packs/templates/personal/AGENTS.md +17 -0
- package/agent-packs/templates/personal/README.md +14 -0
- package/agent-packs/templates/personal/agents/intakes/personal-intake/AGENTS.md +21 -0
- package/agent-packs/templates/personal/agents/specialists/correspondence/AGENTS.md +21 -0
- package/agent-packs/templates/personal/agents/specialists/document-assistant/AGENTS.md +21 -0
- package/agent-packs/templates/personal/agents/specialists/planning/AGENTS.md +21 -0
- package/agent-packs/templates/personal/agents/specialists/research/AGENTS.md +21 -0
- package/agent-packs/templates/personal/agents/specialists/travel-logistics/AGENTS.md +21 -0
- package/agent-packs/templates/personal/pack.json +125 -0
- package/agent-packs/templates/personal/shared/PACK.md +22 -0
- package/agent-packs/templates/personal/shared/TOPOLO.md +13 -0
- package/agent-packs/templates/personal/shared/WORKFLOW.md +13 -0
- package/agent-packs/templates/personal/skills/topolo/SKILL.md +305 -0
- package/agent-packs/templates/research/AGENTS.md +17 -0
- package/agent-packs/templates/research/README.md +14 -0
- package/agent-packs/templates/research/agents/intakes/research-intake/AGENTS.md +21 -0
- package/agent-packs/templates/research/agents/specialists/adversarial-review/AGENTS.md +21 -0
- package/agent-packs/templates/research/agents/specialists/data-analysis/AGENTS.md +21 -0
- package/agent-packs/templates/research/agents/specialists/source-research/AGENTS.md +21 -0
- package/agent-packs/templates/research/agents/specialists/synthesis/AGENTS.md +21 -0
- package/agent-packs/templates/research/agents/specialists/technical-research/AGENTS.md +21 -0
- package/agent-packs/templates/research/pack.json +124 -0
- package/agent-packs/templates/research/shared/PACK.md +22 -0
- package/agent-packs/templates/research/shared/TOPOLO.md +13 -0
- package/agent-packs/templates/research/shared/WORKFLOW.md +13 -0
- package/agent-packs/templates/research/skills/topolo/SKILL.md +305 -0
- package/agent-packs/templates/ventures/AGENTS.md +17 -0
- package/agent-packs/templates/ventures/README.md +14 -0
- package/agent-packs/templates/ventures/agents/intakes/venture-intake/AGENTS.md +21 -0
- package/agent-packs/templates/ventures/agents/specialists/customer-discovery/AGENTS.md +21 -0
- package/agent-packs/templates/ventures/agents/specialists/economics/AGENTS.md +21 -0
- package/agent-packs/templates/ventures/agents/specialists/experiment-design/AGENTS.md +21 -0
- package/agent-packs/templates/ventures/agents/specialists/market-research/AGENTS.md +21 -0
- package/agent-packs/templates/ventures/agents/specialists/product-concept/AGENTS.md +21 -0
- package/agent-packs/templates/ventures/pack.json +126 -0
- package/agent-packs/templates/ventures/shared/PACK.md +22 -0
- package/agent-packs/templates/ventures/shared/TOPOLO.md +13 -0
- package/agent-packs/templates/ventures/shared/WORKFLOW.md +13 -0
- package/agent-packs/templates/ventures/skills/topolo/SKILL.md +305 -0
- package/agent-packs/topolo-agent-pack +8 -0
- package/dist/index.js +119 -51
- package/package.json +7 -4
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""The topolo-agent-pack command interface and recoverable composition transactions."""
|
|
2
|
+
import argparse
|
|
3
|
+
import copy
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import pathlib
|
|
8
|
+
import selectors
|
|
9
|
+
import shutil
|
|
10
|
+
import sqlite3
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
import time
|
|
15
|
+
import uuid
|
|
16
|
+
|
|
17
|
+
from . import __version__
|
|
18
|
+
from . import native, live, fresh, blueprint
|
|
19
|
+
from .storage import Library, PackError, atomic_json, canonical, component_key, now, read_json, references, safe_id, select, sync_tree
|
|
20
|
+
|
|
21
|
+
def emit(value):
|
|
22
|
+
print(json.dumps(value, indent=2, ensure_ascii=False))
|
|
23
|
+
|
|
24
|
+
def source_components(manifest, revision, selected):
|
|
25
|
+
return [{'key':component_key(a),'sourcePack':manifest['packId'],'revision':revision,'originPack':manifest['packId'],'originRevision':revision,'threadId':a['threadId']} for a in selected]
|
|
26
|
+
|
|
27
|
+
def make_plan(lib, args):
|
|
28
|
+
active = lib.active(); existing = active.get('components',[])
|
|
29
|
+
saved=read_json(lib.root/'workbenches'/(safe_id(args.source)+'.json'),{})
|
|
30
|
+
resume=bool(args.action=='switch' and saved and not getattr(args,'fresh',False))
|
|
31
|
+
if resume:
|
|
32
|
+
candidates=[]
|
|
33
|
+
for c in saved['components']:
|
|
34
|
+
m,_=lib.load(c['sourcePack'],c['revision'])
|
|
35
|
+
candidates.extend(a for a in m['agents'] if component_key(a)==c['key'])
|
|
36
|
+
manifest={'agents':candidates,'warnings':[]}
|
|
37
|
+
chosen=select(manifest,args.sets,args.agents)
|
|
38
|
+
incoming=[c for c in saved['components'] if c['key'] in {component_key(a) for a in chosen}]
|
|
39
|
+
revision='saved-workbench'
|
|
40
|
+
else:
|
|
41
|
+
manifest, revision = lib.load(args.source)
|
|
42
|
+
chosen = select(manifest, args.sets, args.agents)
|
|
43
|
+
incoming = source_components(manifest,revision,chosen)
|
|
44
|
+
if args.action=='switch' and active.get('name')==args.source and not args.sets and not args.agents and not getattr(args,'fresh',False):
|
|
45
|
+
return {'noChange':True,'before':active}
|
|
46
|
+
before = {c['key']:c for c in existing}
|
|
47
|
+
selected_keys = {c['key'] for c in incoming}
|
|
48
|
+
remove = set()
|
|
49
|
+
if args.action == 'switch': remove = set(before)
|
|
50
|
+
elif args.action == 'replace':
|
|
51
|
+
target = getattr(args,'target',None)
|
|
52
|
+
if target:
|
|
53
|
+
parts = target.split('/')
|
|
54
|
+
if not 1 <= len(parts) <= 3: raise PackError('--target must be PACK[/SET[/AGENT]]')
|
|
55
|
+
for part in parts: safe_id(part)
|
|
56
|
+
remove = {k for k in before if k == target or k.startswith(target+'/')}
|
|
57
|
+
if not remove: raise PackError('Replacement target is not in the active composition')
|
|
58
|
+
if len(parts)==3 and len(incoming)!=1: raise PackError('An individual target accepts exactly one incoming agent')
|
|
59
|
+
if len(parts)==2 and len({a['set'] for a in chosen})!=1: raise PackError('A set target accepts exactly one incoming set')
|
|
60
|
+
elif args.agents:
|
|
61
|
+
remove=selected_keys
|
|
62
|
+
if not remove <= set(before): raise PackError('Replacement agent is not active; use add or supply --target.')
|
|
63
|
+
elif args.sets:
|
|
64
|
+
prefixes={a['pack']+'/'+a['set']+'/' for a in chosen}
|
|
65
|
+
remove={k for k in before if any(k.startswith(p) for p in prefixes)}
|
|
66
|
+
if any(not any(k.startswith(p) for k in before) for p in prefixes): raise PackError('Replacement set is not active; use add or supply --target.')
|
|
67
|
+
else: remove=set(before)
|
|
68
|
+
elif args.action != 'add': raise PackError('Unknown composition action')
|
|
69
|
+
remaining={k:v for k,v in before.items() if k not in remove}
|
|
70
|
+
added=[]
|
|
71
|
+
for c in incoming:
|
|
72
|
+
if c['key'] in remaining:
|
|
73
|
+
old=remaining[c['key']]
|
|
74
|
+
if old.get('originRevision',old['revision'])==c.get('originRevision',c['revision']) and old.get('originPack',old['sourcePack'])==c.get('originPack',c['sourcePack']): continue
|
|
75
|
+
raise PackError('add would overwrite an installed identity: '+c['key']+'. Use replace.')
|
|
76
|
+
remaining[c['key']]=c; added.append(c['key'])
|
|
77
|
+
components=list(remaining.values())
|
|
78
|
+
threads=[c['threadId'] for c in components]
|
|
79
|
+
if len(threads)!=len(set(threads)): raise PackError('Native thread identity collision across selected agents')
|
|
80
|
+
return {'operation':args.action,'source':args.source,'sourceRevision':revision,'before':active,
|
|
81
|
+
'components':components,'removed':sorted(remove),'added':added,'noChange':not remove and not added,
|
|
82
|
+
'name':args.source if args.action=='switch' or not existing else active.get('name',args.source),
|
|
83
|
+
'resumeRuntime':saved.get('runtime') if resume and not args.sets and not args.agents else None,
|
|
84
|
+
'reuseOutgoingRuntime':args.action=='switch' and not args.sets and not args.agents,
|
|
85
|
+
'runtimeCheckpointRequired':bool(existing),'warnings':manifest.get('warnings',[]),
|
|
86
|
+
'activation':'not tested by plan; --no-open commits storage only'}
|
|
87
|
+
|
|
88
|
+
def maps(values):
|
|
89
|
+
result={}
|
|
90
|
+
for value in values or []:
|
|
91
|
+
if '=' not in value: raise PackError('--map requires OLD=NEW')
|
|
92
|
+
old,new=value.split('=',1)
|
|
93
|
+
old=str(pathlib.Path(old).expanduser().resolve()); new=str(pathlib.Path(new).expanduser().resolve())
|
|
94
|
+
if not pathlib.Path(new).exists(): raise PackError('Mapped destination does not exist: '+new)
|
|
95
|
+
if old in result and result[old]!=new: raise PackError('Conflicting path mapping')
|
|
96
|
+
result[old]=new
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
def verify_backend(lib, runtime, cli):
|
|
100
|
+
"""Read back through the installed native protocol; do not load/resume or start turns."""
|
|
101
|
+
if not cli: raise PackError('Native Codex binary not found; supply --codex-bin.')
|
|
102
|
+
cli=pathlib.Path(cli).expanduser().resolve()
|
|
103
|
+
home=pathlib.Path(runtime)/'home'
|
|
104
|
+
connection=native.ro(home/'state_5.sqlite')
|
|
105
|
+
try: expected_cwds=dict(connection.execute('SELECT id,cwd FROM threads'))
|
|
106
|
+
finally: connection.close()
|
|
107
|
+
stderr=open(pathlib.Path(runtime)/'backend-verify.log','a')
|
|
108
|
+
env=os.environ.copy(); env['CODEX_HOME']=str(home)
|
|
109
|
+
for key in list(env):
|
|
110
|
+
if key.startswith('CODEX_APP_TOOLS_'): del env[key]
|
|
111
|
+
proc=subprocess.Popen([str(cli),'app-server','--listen','stdio://'],cwd=runtime,env=env,stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=stderr,bufsize=0)
|
|
112
|
+
sel=selectors.DefaultSelector(); sel.register(proc.stdout,selectors.EVENT_READ)
|
|
113
|
+
seq=0; buffer=b''
|
|
114
|
+
def request(method,params):
|
|
115
|
+
nonlocal seq,buffer
|
|
116
|
+
seq+=1; proc.stdin.write(canonical({'id':seq,'method':method,'params':params})+b'\n'); proc.stdin.flush()
|
|
117
|
+
deadline=time.monotonic()+20
|
|
118
|
+
while time.monotonic()<deadline:
|
|
119
|
+
while b'\n' in buffer:
|
|
120
|
+
line,buffer=buffer.split(b'\n',1)
|
|
121
|
+
if not line.strip(): continue
|
|
122
|
+
value=json.loads(line)
|
|
123
|
+
if value.get('id')==seq:
|
|
124
|
+
if 'error' in value: raise PackError('Native protocol '+method+': '+value['error']['message'])
|
|
125
|
+
return value['result']
|
|
126
|
+
if not sel.select(max(0,deadline-time.monotonic())): break
|
|
127
|
+
chunk=os.read(proc.stdout.fileno(),65536)
|
|
128
|
+
if not chunk: raise PackError('Native verification process exited')
|
|
129
|
+
buffer+=chunk
|
|
130
|
+
raise PackError('Native read-back timed out: '+method)
|
|
131
|
+
checked=[]
|
|
132
|
+
try:
|
|
133
|
+
init=request('initialize',{'clientInfo':{'name':'topolo_agent_pack','version':__version__},'capabilities':{'experimentalApi':True}})
|
|
134
|
+
proc.stdin.write(canonical({'method':'initialized'})+b'\n');proc.stdin.flush()
|
|
135
|
+
agents=read_json(pathlib.Path(runtime)/'agents.json')['agents']
|
|
136
|
+
for a in agents:
|
|
137
|
+
tid=a['threadId']; result=request('thread/read',{'threadId':tid,'includeTurns':False})
|
|
138
|
+
if result['thread']['id']!=tid: raise PackError('Native thread identity read-back mismatch')
|
|
139
|
+
expected=str(pathlib.Path(expected_cwds[tid]).resolve())
|
|
140
|
+
if str(pathlib.Path(result['thread']['cwd']).resolve())!=expected: raise PackError('Native working-directory read-back mismatch for '+tid+': expected '+expected+', got '+result['thread']['cwd'])
|
|
141
|
+
turns=request('thread/turns/list',{'threadId':tid,'limit':1,'itemsView':'notLoaded'})
|
|
142
|
+
items=request('thread/items/list',{'threadId':tid,'limit':1})
|
|
143
|
+
checked.append({'threadId':tid,'cwdVerified':True,'historyReadable':True,'sampledTurns':len(turns['data']),'sampledItems':len(items['data'])})
|
|
144
|
+
finally:
|
|
145
|
+
proc.terminate()
|
|
146
|
+
try: proc.wait(timeout=5)
|
|
147
|
+
except subprocess.TimeoutExpired: proc.kill(); proc.wait()
|
|
148
|
+
sel.close(); proc.stdin.close(); proc.stdout.close(); stderr.close()
|
|
149
|
+
report={'boundary':'native backend read-back; desktop rendering not verified','agents':checked,'modelTurnsStarted':False}
|
|
150
|
+
atomic_json(pathlib.Path(runtime)/'backend-proof.json',report)
|
|
151
|
+
return report
|
|
152
|
+
|
|
153
|
+
def desktop_open(runtime, app, user_data, codex_bin=None):
|
|
154
|
+
if sys.platform!='darwin': raise PackError('Desktop activation adapter currently supports macOS. Use --no-open for portable storage on this platform.')
|
|
155
|
+
app=pathlib.Path(app).expanduser().resolve()
|
|
156
|
+
if not app.is_dir(): raise PackError('Desktop application not found: '+str(app))
|
|
157
|
+
runtime=pathlib.Path(runtime); user_data=pathlib.Path(user_data); user_data.mkdir(exist_ok=True,mode=0o700)
|
|
158
|
+
lib=Library(user_data.parent);real=codex_bin or find_codex()
|
|
159
|
+
executable,home=live.prepare(lib,real)
|
|
160
|
+
subprocess.run(['/usr/bin/open','-n','--env','CODEX_HOME='+str(home),
|
|
161
|
+
'--env','CODEX_CLI_PATH='+str(executable),'--env','CODEX_PACK_HOME='+str(lib.root),
|
|
162
|
+
'--env','CODEX_PACK_REAL_CLI='+str(real),
|
|
163
|
+
'--env','CODEX_ELECTRON_USER_DATA_PATH='+str(user_data),str(app),
|
|
164
|
+
'--args','--user-data-dir='+str(user_data),'--remote-debugging-port=0','--remote-debugging-address=127.0.0.1','codex://launch'],check=True,timeout=20)
|
|
165
|
+
return {'state':'managed-window-launch-requested','app':str(app),'frontendVerified':False,'note':'One-time managed window launch. Future changes reconnect this same window. Authenticate locally if prompted.'}
|
|
166
|
+
|
|
167
|
+
def apply_plan(lib,plan,args):
|
|
168
|
+
if plan['noChange']:
|
|
169
|
+
active=lib.active()
|
|
170
|
+
if not args.no_open and not live.available(lib):
|
|
171
|
+
native.assert_idle(pathlib.Path(active['runtime'])/'home')
|
|
172
|
+
active['desktop']=desktop_open(active['runtime'],args.app,lib.root/'desktop-data',args.codex_bin)
|
|
173
|
+
active['state']=active['desktop']['state'];atomic_json(lib.root/'active.json',active)
|
|
174
|
+
return {'state':active['state'],'active':active}
|
|
175
|
+
return {'state':'unchanged','active':active}
|
|
176
|
+
if lib.pending(): raise PackError('An incomplete operation needs recovery. Run topolo-agent-pack recover before another mutation.')
|
|
177
|
+
before=plan['before']; old_runtime=pathlib.Path(before['runtime']) if before.get('runtime') else None
|
|
178
|
+
bridge=live.available(lib)
|
|
179
|
+
if bridge:
|
|
180
|
+
if not old_runtime or bridge['runtime']!=str(old_runtime): raise PackError('Live bridge and active runtime disagree; run recover before switching')
|
|
181
|
+
live.adapter(args.app)
|
|
182
|
+
ws,target=live.renderer(lib);ws.close()
|
|
183
|
+
elif old_runtime: native.assert_idle(old_runtime/'home')
|
|
184
|
+
tx=uuid.uuid4().hex; runtime=lib.root/'runtimes'/tx
|
|
185
|
+
journal={'id':tx,'phase':'started','createdAt':now(),'desktopApp':args.app,'before':before,'operation':plan['operation'],'targetRuntime':str(runtime),'plan':{k:v for k,v in plan.items() if k!='before'}}
|
|
186
|
+
jpath=lib.root/'journals'/(tx+'.json'); atomic_json(jpath,journal)
|
|
187
|
+
try:
|
|
188
|
+
if bridge:
|
|
189
|
+
journal['liveBridge']={'targetId':target,'beforePid':bridge['pid'],'desktopPid':bridge['desktopPid']};atomic_json(jpath,journal)
|
|
190
|
+
try:live.quiesce(lib)
|
|
191
|
+
except Exception:
|
|
192
|
+
current=live.available(lib)
|
|
193
|
+
if current and not current['quiesced']:journal['phase']='rolled-back'
|
|
194
|
+
raise
|
|
195
|
+
journal['phase']='quiesced';atomic_json(jpath,journal)
|
|
196
|
+
if old_runtime: native.assert_idle(old_runtime/'home')
|
|
197
|
+
if bridge:live.checkpoint_ui(lib,old_runtime)
|
|
198
|
+
if old_runtime and not plan.get('reuseOutgoingRuntime'):
|
|
199
|
+
known={a['threadId'] for a in read_json(old_runtime/'agents.json')['agents']}
|
|
200
|
+
if native.unregistered_threads(old_runtime/'home',known):
|
|
201
|
+
raise PackError('Additional independent threads need registration before partial composition. Run capture NAME --all, then switch NAME --fresh. Whole-pack switches retain these threads in their runtime.')
|
|
202
|
+
checkpoint_components=copy.deepcopy(before.get('components',[]))
|
|
203
|
+
if old_runtime:
|
|
204
|
+
if plan.get('reuseOutgoingRuntime'):
|
|
205
|
+
# The complete stopped runtime already lives in the independent
|
|
206
|
+
# pack library. Whole-pack switching can retain it in place.
|
|
207
|
+
journal['checkpoint']={'mode':'retained-runtime','runtime':str(old_runtime),'components':checkpoint_components}
|
|
208
|
+
else:
|
|
209
|
+
checkpoint_name='checkpoint-'+tx
|
|
210
|
+
snapshot,revision=native.capture(old_runtime/'home',lib,checkpoint_name,all_threads=True,template=read_json(old_runtime/'agents.json'))
|
|
211
|
+
checkpoint_components=[{**c,'sourcePack':checkpoint_name,'revision':revision} for c in checkpoint_components]
|
|
212
|
+
journal['checkpoint']={'mode':'immutable-snapshot','pack':checkpoint_name,'revision':revision,'components':checkpoint_components}
|
|
213
|
+
journal['phase']='checkpointed';atomic_json(jpath,journal)
|
|
214
|
+
if before.get('name'):
|
|
215
|
+
atomic_json(lib.root/'workbenches'/(safe_id(before['name'])+'.json'),{**before,'components':checkpoint_components})
|
|
216
|
+
checkpoint_by_key={c['key']:c for c in checkpoint_components}
|
|
217
|
+
# Unchanged agents are taken from the current installed state, never the old source template.
|
|
218
|
+
added=set(plan['added'])
|
|
219
|
+
target_components=[checkpoint_by_key.get(c['key'],c) if c['key'] not in added else c for c in plan['components']]
|
|
220
|
+
if plan.get('resumeRuntime'):
|
|
221
|
+
runtime=pathlib.Path(plan['resumeRuntime']);native.assert_idle(runtime/'home')
|
|
222
|
+
if args.mappings: raise PackError('Saved workbench paths are already bound. Use --fresh with --map to rebuild.')
|
|
223
|
+
proof=read_json(runtime/'verification.json')
|
|
224
|
+
journal['targetRuntime']=str(runtime)
|
|
225
|
+
else:
|
|
226
|
+
proof=native.materialize(lib,target_components,runtime,maps(args.mappings))
|
|
227
|
+
atomic_json(runtime/'composition.json',target_components)
|
|
228
|
+
# Authentication and local tool configuration are machine-local and
|
|
229
|
+
# deliberately kept out of portable manifests and export bundles.
|
|
230
|
+
if old_runtime and (old_runtime/'home'/'auth.json').exists():
|
|
231
|
+
shutil.copyfile(old_runtime/'home'/'auth.json',runtime/'home'/'auth.json')
|
|
232
|
+
(runtime/'home'/'auth.json').chmod(0o600)
|
|
233
|
+
local_config=lib.root/'local-config.toml'
|
|
234
|
+
if local_config.exists():
|
|
235
|
+
shutil.copyfile(local_config,runtime/'home'/'config.toml');(runtime/'home'/'config.toml').chmod(0o600)
|
|
236
|
+
if old_runtime and old_runtime!=runtime and (old_runtime/'desktop-view.json').exists():shutil.copyfile(old_runtime/'desktop-view.json',runtime/'desktop-view.json')
|
|
237
|
+
journal['phase']='staged';atomic_json(jpath,journal)
|
|
238
|
+
if old_runtime and old_runtime!=runtime and (old_runtime/'home/auth.json').exists():
|
|
239
|
+
shutil.copyfile(old_runtime/'home/auth.json',runtime/'home/auth.json');(runtime/'home/auth.json').chmod(0o600)
|
|
240
|
+
backend=verify_backend(lib,runtime,args.codex_bin)
|
|
241
|
+
if not plan.get('resumeRuntime'): sync_tree(runtime)
|
|
242
|
+
if old_runtime: native.assert_idle(old_runtime/'home')
|
|
243
|
+
if lib.active()!=before: raise PackError('Active composition changed concurrently; refusing to overwrite it.')
|
|
244
|
+
after={'name':plan['name'],'runtime':str(runtime),'components':target_components,'selectedAt':now(),'state':'backend-verified','desktop':{'state':'not-opened','frontendVerified':False},'transaction':tx}
|
|
245
|
+
journal['after']=after;journal['phase']='verified';atomic_json(jpath,journal)
|
|
246
|
+
atomic_json(lib.root/'active.json',after)
|
|
247
|
+
journal['phase']='committed';atomic_json(jpath,journal)
|
|
248
|
+
if bridge or not args.no_open:
|
|
249
|
+
after['desktop']=live.reconnect(lib,runtime,old_runtime,args.app) if bridge else desktop_open(runtime,args.app,lib.root/'desktop-data',args.codex_bin)
|
|
250
|
+
after['state']=after['desktop']['state'];atomic_json(lib.root/'active.json',after)
|
|
251
|
+
journal['after']=after;journal['phase']='complete';atomic_json(jpath,journal)
|
|
252
|
+
return {'state':after['state'],'agents':proof['agents'],'totalHistoryThreads':proof['threads'],'runtime':str(runtime),'checkpoint':journal.get('checkpoint'),
|
|
253
|
+
'desktop':after['desktop'],'backend':backend,'dependencies':proof['dependencies'],'activeGoalsPaused':proof['activeGoalsPaused']}
|
|
254
|
+
except Exception as exc:
|
|
255
|
+
journal['failure']=str(exc);atomic_json(jpath,journal)
|
|
256
|
+
if bridge and live.available(lib) and live.available(lib).get('quiesced'):
|
|
257
|
+
try:
|
|
258
|
+
current=lib.active();journal['reconnectAfterFailure']=live.reconnect(lib,current['runtime'],old_runtime,args.app);atomic_json(jpath,journal)
|
|
259
|
+
except Exception as reconnect_error:
|
|
260
|
+
journal['reconnectFailure']=str(reconnect_error);atomic_json(jpath,journal)
|
|
261
|
+
raise
|
|
262
|
+
|
|
263
|
+
def recover(lib):
|
|
264
|
+
result=[]
|
|
265
|
+
for path in sorted(lib.pending()):
|
|
266
|
+
j=read_json(path); active=lib.active(); before=j['before']; after=j.get('after')
|
|
267
|
+
previous=(active.get('runtime')==before.get('runtime') and active.get('transaction')==before.get('transaction') and active.get('components')==before.get('components'))
|
|
268
|
+
committed=bool(after and active.get('transaction')==j['id'] and active.get('runtime')==after['runtime'] and active.get('components')==after.get('components'))
|
|
269
|
+
if not previous and not committed:raise PackError('Recovery found a concurrent active-pointer change; no state overwritten. Inspect '+str(path))
|
|
270
|
+
if j.get('liveBridge'):
|
|
271
|
+
j['desktopRecovery']=live.reconnect(lib,active['runtime'],before.get('runtime'),j.get('desktopApp','/Applications/ChatGPT.app'))
|
|
272
|
+
active['desktop']=j['desktopRecovery'];active['state']=active['desktop']['state'];atomic_json(lib.root/'active.json',active)
|
|
273
|
+
if previous:
|
|
274
|
+
j['phase']='rolled-back';j['recovery']='Previous active pointer retained; staged files kept for inspection.'
|
|
275
|
+
elif after and active.get('transaction')==j['id'] and active.get('runtime')==after['runtime']:
|
|
276
|
+
if not j.get('liveBridge'): native.assert_idle(pathlib.Path(active['runtime'])/'home')
|
|
277
|
+
if not (pathlib.Path(active['runtime'])/'backend-proof.json').exists(): raise PackError('Committed runtime lacks backend proof; recovery needs inspection.')
|
|
278
|
+
j['phase']='complete';j['recovery']='Verified committed runtime retained.'+(' Same window reconnected and read back.' if j.get('liveBridge') else ' Desktop activation was not replayed.')
|
|
279
|
+
else: raise PackError('Recovery found a concurrent active-pointer change; no state overwritten. Inspect '+str(path))
|
|
280
|
+
atomic_json(path,j);result.append({'transaction':j['id'],'phase':j['phase'],'action':j['recovery']})
|
|
281
|
+
return {'recovered':result,'active':lib.active()}
|
|
282
|
+
|
|
283
|
+
def find_codex():
|
|
284
|
+
bundled=pathlib.Path('/Applications/ChatGPT.app/Contents/Resources/codex-cli/CodexCLI.app/Contents/MacOS/codex')
|
|
285
|
+
return str(bundled) if bundled.exists() else shutil.which('codex')
|
|
286
|
+
|
|
287
|
+
def parser():
|
|
288
|
+
p=argparse.ArgumentParser(description='Capture, install and compose portable Topolo Agent Packs. No cloud service or API key required for storage operations.')
|
|
289
|
+
p.add_argument('--version',action='version',version=__version__)
|
|
290
|
+
p.add_argument('--store',default=os.environ.get('CODEX_PACK_HOME',str(pathlib.Path.home()/'AgentPacks')))
|
|
291
|
+
sub=p.add_subparsers(dest='command',required=True)
|
|
292
|
+
def selection(q):
|
|
293
|
+
q.add_argument('--set',dest='sets',action='append');q.add_argument('--agent',dest='agents',action='append')
|
|
294
|
+
def activation(q):
|
|
295
|
+
q.add_argument('--no-open',action='store_true',help='Commit a backend-verified composition without opening the desktop')
|
|
296
|
+
q.add_argument('--map',dest='mappings',action='append',help='External path binding OLD=NEW (never overwrites the external destination)')
|
|
297
|
+
q.add_argument('--codex-bin',default=find_codex());q.add_argument('--app',default='/Applications/ChatGPT.app')
|
|
298
|
+
cap=sub.add_parser('capture');cap.add_argument('name');selection(cap)
|
|
299
|
+
cap.add_argument('--source-home',help='Source Codex home. Defaults to the selected managed runtime, otherwise CODEX_HOME or ~/.codex')
|
|
300
|
+
cap.add_argument('--include',action='append',help='Explicit additional workflow/asset path')
|
|
301
|
+
cap.add_argument('--all',action='store_true',help='Explicitly capture all non-archived threads when group names do not identify the pack')
|
|
302
|
+
cap.add_argument('--workspace-mode',choices=['copy','reference'],default='copy')
|
|
303
|
+
cap.add_argument('--offline',action='store_true',help='Require an idle source instead of taking a read-only live snapshot')
|
|
304
|
+
q=sub.add_parser('context',help='Extract operating context and conversation rule candidates for review');q.add_argument('source');q.add_argument('directory');selection(q)
|
|
305
|
+
q=sub.add_parser('fresh',help='Create new native agents from reviewed file-backed context, without old work history');q.add_argument('source');q.add_argument('name');q.add_argument('--context',required=True);selection(q);q.add_argument('--codex-bin',default=find_codex())
|
|
306
|
+
q=sub.add_parser('build',help='Build a new native instance from a portable authored template; no source pack required');q.add_argument('name');q.add_argument('--template',required=True);selection(q);q.add_argument('--codex-bin',default=find_codex())
|
|
307
|
+
install=sub.add_parser('install');install.add_argument('bundle');selection(install)
|
|
308
|
+
export=sub.add_parser('export');export.add_argument('name');export.add_argument('destination')
|
|
309
|
+
for action in ['add','replace','switch']:
|
|
310
|
+
q=sub.add_parser(action);q.add_argument('source');selection(q);activation(q)
|
|
311
|
+
q.set_defaults(action=action)
|
|
312
|
+
if action=='add': q.add_argument('--catalog',action='store_true',help='Build an uninstalled bundled catalog pack before adding it')
|
|
313
|
+
if action=='replace': q.add_argument('--target')
|
|
314
|
+
if action=='switch': q.add_argument('--fresh',action='store_true',help='Use the captured source instead of resuming a previously saved workbench')
|
|
315
|
+
q=sub.add_parser('plan');q.add_argument('action',choices=['add','replace','switch']);q.add_argument('source');selection(q);q.add_argument('--target');q.add_argument('--fresh',action='store_true')
|
|
316
|
+
for command in ['connect','remote-context','remember','artifact','artifact-get','remote-recover']:
|
|
317
|
+
q=sub.add_parser(command,help='Use public Topolo Agent continuity and Bytes artifact contracts')
|
|
318
|
+
q.add_argument('name');q.add_argument('--environment',choices=['development','staging','production'],required=True)
|
|
319
|
+
q.add_argument('--context-key',required=True);q.add_argument('--role',required=command not in ['connect','remote-recover'])
|
|
320
|
+
if command in ['connect','remember','artifact']:q.add_argument('--confirm',action='store_true',required=True)
|
|
321
|
+
if command=='connect':
|
|
322
|
+
q.add_argument('--roles',required=True);q.add_argument('--agent-workspace',required=True);q.add_argument('--bytes-workspace',required=True)
|
|
323
|
+
q.add_argument('--ownership',choices=['user','organization'],required=True);q.add_argument('--bytes-scope',choices=['private','org'],required=True);q.add_argument('--instance')
|
|
324
|
+
if command=='remember':
|
|
325
|
+
q.add_argument('--content',required=True);q.add_argument('--kind',choices=['instruction','knowledge','durable'],required=True)
|
|
326
|
+
q.add_argument('--provenance',required=True);q.add_argument('--visibility',choices=['private','shared'],default='private')
|
|
327
|
+
q.add_argument('--sharing-policy');q.add_argument('--expires-at',type=int)
|
|
328
|
+
if command=='artifact':
|
|
329
|
+
q.add_argument('--file',required=True);q.add_argument('--content-type',default='application/octet-stream')
|
|
330
|
+
q.add_argument('--visibility',choices=['private','shared'],default='private')
|
|
331
|
+
if command=='artifact-get':q.add_argument('--reference',required=True);q.add_argument('--destination',required=True)
|
|
332
|
+
sub.add_parser('status');q=sub.add_parser('list');q.add_argument('name',nargs='?');sub.add_parser('recover')
|
|
333
|
+
q=sub.add_parser('open');q.add_argument('--codex-bin',default=find_codex());q.add_argument('--app',default='/Applications/ChatGPT.app')
|
|
334
|
+
q=sub.add_parser('doctor');q.add_argument('name',nargs='?');q.add_argument('--codex-bin',default=find_codex())
|
|
335
|
+
return p
|
|
336
|
+
|
|
337
|
+
def main(argv=None):
|
|
338
|
+
args=parser().parse_args(argv)
|
|
339
|
+
try:
|
|
340
|
+
lib=Library(args.store)
|
|
341
|
+
if args.command in {'connect','remote-context','remember','artifact','artifact-get','remote-recover'}:
|
|
342
|
+
from . import topolo
|
|
343
|
+
with topolo.connection_lock(lib,args):result=topolo.dispatch(lib,args)
|
|
344
|
+
emit(result)
|
|
345
|
+
return 0
|
|
346
|
+
with lib.lock():
|
|
347
|
+
if args.command=='capture':
|
|
348
|
+
safe_id(args.name)
|
|
349
|
+
active=lib.active();template=None
|
|
350
|
+
if args.source_home: home=pathlib.Path(args.source_home)
|
|
351
|
+
elif active.get('runtime'):
|
|
352
|
+
home=pathlib.Path(active['runtime'])/'home';template=read_json(pathlib.Path(active['runtime'])/'agents.json')
|
|
353
|
+
else: home=pathlib.Path(os.environ.get('CODEX_HOME',str(pathlib.Path.home()/'.codex')))
|
|
354
|
+
manifest,revision=native.capture(home,lib,args.name,args.sets,args.agents,args.include,args.all,template,args.workspace_mode,live=not args.offline)
|
|
355
|
+
result={'pack':args.name,'revision':revision,'agents':len(manifest['agents']),'historyThreads':len(manifest['threads']),'warnings':manifest['warnings'],'state':'captured'}
|
|
356
|
+
elif args.command=='context':result=fresh.prepare(lib,args.source,args.directory,args.sets,args.agents)
|
|
357
|
+
elif args.command=='fresh':result=fresh.create(lib,args.source,args.name,args.context,args.codex_bin,args.sets,args.agents)
|
|
358
|
+
elif args.command=='build':result=blueprint.build(lib,args.name,args.template,args.codex_bin,args.sets,args.agents)
|
|
359
|
+
elif args.command=='install':
|
|
360
|
+
manifest,revision=lib.install(args.bundle,publish=False)
|
|
361
|
+
selected=select(manifest,args.sets,args.agents)
|
|
362
|
+
if args.sets or args.agents:
|
|
363
|
+
manifest=native.slice_manifest(lib,manifest,selected)
|
|
364
|
+
revision=lib.save(manifest)
|
|
365
|
+
result={'state':'installed-not-activated','pack':manifest['packId'],'revision':revision,'agents':len(selected),'warnings':manifest['warnings']}
|
|
366
|
+
elif args.command=='export': result=lib.export(args.name,args.destination)
|
|
367
|
+
elif args.command=='list':
|
|
368
|
+
if args.name:
|
|
369
|
+
manifest,revision=lib.load(args.name)
|
|
370
|
+
result={'pack':args.name,'revision':revision,'sets':sorted({a['set'] for a in manifest['agents']}),
|
|
371
|
+
'agents':[{'key':component_key(a),'title':a['title'],'aliases':a.get('aliases',[])} for a in manifest['agents']]}
|
|
372
|
+
else:
|
|
373
|
+
result={'packs':[{'name':path.parent.name,'revision':read_json(path)['revision']} for path in sorted((lib.root/'packs').glob('*/head.json')) if not path.parent.name.startswith('checkpoint-')],
|
|
374
|
+
'workbenches':[p.stem for p in sorted((lib.root/'workbenches').glob('*.json'))]}
|
|
375
|
+
elif args.command=='plan': result=make_plan(lib,args)
|
|
376
|
+
elif args.command in {'add','replace','switch'}:
|
|
377
|
+
if args.command=='add' and args.catalog and not (lib.root/'packs'/safe_id(args.source)/'head.json').exists():
|
|
378
|
+
template=pathlib.Path(__file__).resolve().parents[2]/'templates'/safe_id(args.source)
|
|
379
|
+
if not template.is_dir():raise PackError('Unknown catalog pack: '+args.source)
|
|
380
|
+
blueprint.build(lib,args.source,template,args.codex_bin)
|
|
381
|
+
result=apply_plan(lib,make_plan(lib,args),args)
|
|
382
|
+
elif args.command=='recover': result=recover(lib)
|
|
383
|
+
elif args.command=='open':
|
|
384
|
+
active=lib.active()
|
|
385
|
+
if not active.get('runtime'):raise PackError('Select a pack first with switch NAME')
|
|
386
|
+
if live.available(lib):result={'state':'already-open','liveBridge':live.available(lib)}
|
|
387
|
+
else:
|
|
388
|
+
native.assert_idle(pathlib.Path(active['runtime'])/'home')
|
|
389
|
+
result=desktop_open(active['runtime'],args.app,lib.root/'desktop-data',args.codex_bin)
|
|
390
|
+
active['desktop']=result;active['state']=result['state'];atomic_json(lib.root/'active.json',active)
|
|
391
|
+
elif args.command=='status':
|
|
392
|
+
active=lib.active();pids=native.busy_writers(pathlib.Path(active['runtime'])/'home') if active.get('runtime') else []
|
|
393
|
+
result={'store':str(lib.root),'active':active,'liveBridge':live.available(lib),'runtimeDatabaseHolders':pids,'recoveryRequired':[str(p) for p in lib.pending()],'frontendVerified':active.get('desktop',{}).get('frontendVerified',False)}
|
|
394
|
+
elif args.command=='doctor':
|
|
395
|
+
checked=[]
|
|
396
|
+
if args.name:
|
|
397
|
+
manifest,revision=lib.load(args.name)
|
|
398
|
+
for ref in references(manifest).values(): lib.object(ref)
|
|
399
|
+
checked.append({'pack':args.name,'revision':revision,'objectsVerified':len(references(manifest))})
|
|
400
|
+
result={'library':str(lib.root),'nativeCodex':args.codex_bin,'desktopAdapter':'macOS managed backend proxy and same-window reconnect','liveBridge':live.available(lib),
|
|
401
|
+
'recoveryRequired':bool(lib.pending()),'checked':checked,'version':__version__}
|
|
402
|
+
emit(result)
|
|
403
|
+
return 0
|
|
404
|
+
except (PackError,OSError,ValueError,sqlite_error(),subprocess.SubprocessError,zip_error()) as exc:
|
|
405
|
+
print('topolo-agent-pack: '+str(exc),file=sys.stderr)
|
|
406
|
+
return 2
|
|
407
|
+
|
|
408
|
+
def sqlite_error():
|
|
409
|
+
import sqlite3
|
|
410
|
+
return sqlite3.Error
|
|
411
|
+
|
|
412
|
+
def zip_error():
|
|
413
|
+
import zipfile
|
|
414
|
+
return zipfile.BadZipFile
|
|
415
|
+
|
|
416
|
+
if __name__=='__main__': raise SystemExit(main())
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Fresh native threads from reviewed portable context; no model calls."""
|
|
2
|
+
import json, os, pathlib, re, selectors, shutil, sqlite3, subprocess, tempfile, time
|
|
3
|
+
from . import native, __version__
|
|
4
|
+
from .storage import PackError, canonical, atomic_json, now, safe_id, select
|
|
5
|
+
|
|
6
|
+
class Backend:
|
|
7
|
+
def __init__(self,binary,home,root):
|
|
8
|
+
if not binary:raise PackError('Native Codex binary is required')
|
|
9
|
+
env=os.environ.copy();env['CODEX_HOME']=str(home)
|
|
10
|
+
for k in list(env):
|
|
11
|
+
if k.startswith('CODEX_APP_TOOLS_'):env.pop(k)
|
|
12
|
+
self.log=open(root/'seed-backend.log','ab')
|
|
13
|
+
self.proc=subprocess.Popen([str(binary),'app-server','--listen','stdio://'],cwd=root,env=env,stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=self.log,bufsize=0)
|
|
14
|
+
self.sel=selectors.DefaultSelector();self.sel.register(self.proc.stdout,selectors.EVENT_READ);self.seq=0;self.buffer=b''
|
|
15
|
+
try:
|
|
16
|
+
self.call('initialize',{'clientInfo':{'name':'topolo_agent_pack','version':__version__},'capabilities':{'experimentalApi':True}})
|
|
17
|
+
self.proc.stdin.write(canonical({'method':'initialized'})+b'\n');self.proc.stdin.flush()
|
|
18
|
+
except BaseException:
|
|
19
|
+
self.close()
|
|
20
|
+
raise
|
|
21
|
+
def call(self,method,params):
|
|
22
|
+
self.seq+=1;self.proc.stdin.write(canonical({'id':self.seq,'method':method,'params':params})+b'\n');self.proc.stdin.flush();end=time.monotonic()+30
|
|
23
|
+
while time.monotonic()<end:
|
|
24
|
+
while b'\n' in self.buffer:
|
|
25
|
+
line,self.buffer=self.buffer.split(b'\n',1)
|
|
26
|
+
if not line.strip():continue
|
|
27
|
+
value=json.loads(line)
|
|
28
|
+
if value.get('id')==self.seq:
|
|
29
|
+
if 'error' in value:raise PackError(method+': '+value['error']['message'])
|
|
30
|
+
return value['result']
|
|
31
|
+
if not self.sel.select(max(0,end-time.monotonic())):break
|
|
32
|
+
chunk=os.read(self.proc.stdout.fileno(),1024*1024)
|
|
33
|
+
if not chunk:raise PackError('Native seed backend exited')
|
|
34
|
+
self.buffer+=chunk
|
|
35
|
+
raise PackError('Native seed timed out: '+method)
|
|
36
|
+
def close(self):
|
|
37
|
+
self.proc.terminate()
|
|
38
|
+
try:self.proc.wait(timeout=5)
|
|
39
|
+
except subprocess.TimeoutExpired:self.proc.kill();self.proc.wait()
|
|
40
|
+
self.sel.close();self.proc.stdin.close();self.proc.stdout.close();self.log.close()
|
|
41
|
+
def __enter__(self):return self
|
|
42
|
+
def __exit__(self,*args):self.close()
|
|
43
|
+
|
|
44
|
+
BASE='Follow the current human request and applicable project instructions. Inspect context, proceed within authorized scope, respect owners and pauses, and verify outcomes. No task is assigned during initialization.\n'
|
|
45
|
+
|
|
46
|
+
def prepare(lib,source,directory,sets=None,agents=None):
|
|
47
|
+
m,revision=lib.load(source);chosen=select(m,sets,agents);root=pathlib.Path(directory).expanduser().resolve()
|
|
48
|
+
if root.exists():raise PackError('Context destination already exists; choose a new directory')
|
|
49
|
+
root.mkdir(parents=True,mode=0o700)
|
|
50
|
+
for folder in ['review','agents','shared']:(root/folder).mkdir()
|
|
51
|
+
common=next((d for d in m['definitions'] if d['relative']=='AGENTS.md'),None)
|
|
52
|
+
(root/'AGENTS.md').write_text(lib.object(common['content']).read_text() if common else BASE)
|
|
53
|
+
rule=re.compile(r'\b(always|never|from now on|going forward|whenever|as a rule|each time|must|instructions)\b',re.I);evidence=[]
|
|
54
|
+
for a in chosen:
|
|
55
|
+
folder=root/'agents'/a['set']/a['id'];folder.mkdir(parents=True,exist_ok=True)
|
|
56
|
+
(folder/'AGENTS.md').write_text('# '+a['title']+'\n\nRole: '+a['id']+' (set '+a['set']+').\nOperate within this role and route work outside its authority to the appropriate intake. Read shared operating context before work. Await a new assignment.\n')
|
|
57
|
+
seen=set();candidates=[];tid=a['threadId'];lineage=[]
|
|
58
|
+
while tid and tid not in seen:
|
|
59
|
+
seen.add(tid);lineage.append(tid);tid=m['threads'][tid].get('historyBase')
|
|
60
|
+
for tid in reversed(lineage):
|
|
61
|
+
with lib.object(m['threads'][tid]['rollout']).open('rb') as stream:
|
|
62
|
+
for line in stream:
|
|
63
|
+
v=json.loads(line);p=v.get('payload',{})
|
|
64
|
+
if v.get('type')!='response_item' or p.get('role')!='user':continue
|
|
65
|
+
text='\n'.join(c.get('text','') for c in p.get('content',[]) if isinstance(c,dict))
|
|
66
|
+
if text.lstrip().startswith('<') or '# AGENTS.md instructions' in text:continue
|
|
67
|
+
for paragraph in text.split('\n\n'):
|
|
68
|
+
if 50<len(paragraph)<2000 and rule.search(paragraph):candidates.append({'threadId':tid,'ordinal':v.get('ordinal'),'text':paragraph})
|
|
69
|
+
candidates=list({c['text']:c for c in candidates}.values())[-30:]
|
|
70
|
+
atomic_json(root/'review'/(a['set']+'--'+a['id']+'.json'),{'agent':a['id'],'candidates':candidates,'status':'source excerpts; not applied'})
|
|
71
|
+
evidence.append({'agent':a['id'],'sourceThreadId':a['threadId'],'candidateCount':len(candidates),'sourceWorkingDirectory':m['threads'][a['threadId']]['cwd']})
|
|
72
|
+
atomic_json(root/'review'/'provenance.json',{'source':source,'revision':revision,'agents':evidence,'createdAt':now()})
|
|
73
|
+
(root/'README.md').write_text('Edit AGENTS.md and agents/<set>/<agent-id>/AGENTS.md. Put selected portable reference documents in shared/*.md. Review excerpts under review/ and promote only reusable rules into instruction files. Review excerpts and provenance are NEVER loaded into fresh agents. Exclude old assignments, registry/status records, dispatches, leases, credentials and conversation histories. Initial role guides are scaffolds; add the necessary charters before creating a fresh pack.\n')
|
|
74
|
+
return {'state':'context-extracted-for-review','directory':str(root),'agents':len(chosen),'candidates':sum(e['candidateCount'] for e in evidence),'sourceRevision':revision}
|
|
75
|
+
|
|
76
|
+
def create(lib,source,name,directory,binary,sets=None,agents=None,authored=None):
|
|
77
|
+
safe_id(name)
|
|
78
|
+
if (lib.root/'packs'/name/'head.json').exists():raise PackError('Fresh destination already exists; choose a new name to preserve its work')
|
|
79
|
+
if lib.pending():raise PackError('Recover the pending operation first')
|
|
80
|
+
m,revision=authored if authored is not None else lib.load(source)
|
|
81
|
+
chosen=select(m,sets,agents);context=pathlib.Path(directory).expanduser().resolve();common=context/'AGENTS.md'
|
|
82
|
+
if not common.is_file() or common.is_symlink():raise PackError('Context requires a regular AGENTS.md')
|
|
83
|
+
common_text=common.read_text();selected={};presets={}
|
|
84
|
+
for a in chosen:
|
|
85
|
+
role=context/'agents'/a['set']/a['id']/'AGENTS.md'
|
|
86
|
+
if not role.is_file() or role.is_symlink():raise PackError('Missing regular role guide: '+str(role))
|
|
87
|
+
selected[(a['set'],a['id'])]=role.read_text()
|
|
88
|
+
preset=role.with_name('config.json')
|
|
89
|
+
if preset.exists():
|
|
90
|
+
if preset.is_symlink():raise PackError('Preset must be a regular file')
|
|
91
|
+
value=json.loads(preset.read_text())
|
|
92
|
+
if set(value)-{'model','reasoningEffort'} or not isinstance(value.get('model'),str) or value.get('reasoningEffort') not in {'none','minimal','low','medium','high','xhigh','max','ultra'}:raise PackError('Invalid role model preset: '+str(preset))
|
|
93
|
+
presets[(a['set'],a['id'])]=value
|
|
94
|
+
shared=[]
|
|
95
|
+
if (context/'shared').is_symlink():raise PackError('Shared context cannot be a symlink')
|
|
96
|
+
for f in sorted((context/'shared').rglob('*')):
|
|
97
|
+
if f.is_symlink():raise PackError('Context symlink is not portable: '+str(f))
|
|
98
|
+
if f.is_file():
|
|
99
|
+
if f.suffix!='.md':raise PackError('Shared starting context accepts Markdown only: '+str(f))
|
|
100
|
+
if f.stat().st_size>1024*1024:raise PackError('Starting reference exceeds 1 MiB: '+str(f))
|
|
101
|
+
shared.append((f.relative_to(context/'shared'),f.read_bytes()))
|
|
102
|
+
if len(common_text.encode())+sum(len(v.encode()) for v in selected.values())+sum(len(v) for _,v in shared)>16*1024*1024:raise PackError('Starting context exceeds 16 MiB')
|
|
103
|
+
root=pathlib.Path(tempfile.mkdtemp(prefix='.fresh-seed-',dir=lib.root));home=root/'home';home.mkdir()
|
|
104
|
+
try:
|
|
105
|
+
(home/'AGENTS.md').write_text(common_text);(home/'config.toml').write_text(''.join(k+' = '+json.dumps(v)+'\n' for k,v in m.get('config',{}).items() if k in {'model','model_reasoning_effort','personality'})+'[analytics]\nenabled=false\n')
|
|
106
|
+
for d in m['definitions']:
|
|
107
|
+
relative=pathlib.Path(d['relative'])
|
|
108
|
+
if not relative.parts or relative.parts[0]!='skills':continue
|
|
109
|
+
if relative.is_absolute() or '..' in relative.parts:raise PackError('Unsafe skill definition')
|
|
110
|
+
target=home/relative;target.parent.mkdir(parents=True,exist_ok=True);shutil.copyfile(lib.object(d['content']),target)
|
|
111
|
+
seeded=[];records=[]
|
|
112
|
+
with Backend(binary,home,root) as b:
|
|
113
|
+
for a in chosen:
|
|
114
|
+
workspace=root/'workspaces'/a['set']/a['id'];workspace.mkdir(parents=True)
|
|
115
|
+
(workspace/'AGENTS.md').write_text(common_text+'\n\n'+selected[(a['set'],a['id'])]+'\n\nStarting references are in context/. Read relevant files before work. No prior assignment or conversation is inherited.\n')
|
|
116
|
+
for relative,data in shared:
|
|
117
|
+
target=workspace/'context'/relative;target.parent.mkdir(parents=True,exist_ok=True);target.write_bytes(data)
|
|
118
|
+
preset=presets.get((a['set'],a['id']),{})
|
|
119
|
+
params={'cwd':str(workspace),'historyMode':'paginated','ephemeral':False}
|
|
120
|
+
if preset:params.update({'model':preset['model'],'config':{'model_reasoning_effort':preset['reasoningEffort']}})
|
|
121
|
+
result=b.call('thread/start',params);tid=result['thread']['id'];b.call('thread/name/set',{'threadId':tid,'name':a['title']})
|
|
122
|
+
if preset:b.call('thread/settings/update',{'threadId':tid,'model':preset['model'],'effort':preset['reasoningEffort']})
|
|
123
|
+
note='Fresh '+a['title']+'. Read AGENTS.md and relevant context/ files for operating instructions. No task is assigned. Wait for a new human request.'
|
|
124
|
+
b.call('thread/inject_items',{'threadId':tid,'items':[{'type':'message','role':'user','content':[{'type':'input_text','text':note}]}]});b.call('thread/unsubscribe',{'threadId':tid})
|
|
125
|
+
seeded.append({**a,'pack':name,'threadId':tid});records.append((tid,result['thread']['path'],note))
|
|
126
|
+
team='# Fresh pack team\n\nResolve peer identities here for human-authorized coordination. Every member starts with no task or claim. This roster grants no external messaging or product effects.\n\n'+'\n'.join('- '+a['set']+'/'+a['id']+' | '+a['title']+' | thread '+a['threadId'] for a in seeded)+'\n'
|
|
127
|
+
for a in seeded:
|
|
128
|
+
target=root/'workspaces'/a['set']/a['id']/'context'/'TEAM.md';target.parent.mkdir(parents=True,exist_ok=True);target.write_text(team)
|
|
129
|
+
# Offline native import adapter. Injected items lack the bootstrap user event
|
|
130
|
+
# required for desktop discovery. Only this private, stopped seed is edited.
|
|
131
|
+
db=sqlite3.connect(home/'state_5.sqlite')
|
|
132
|
+
try:
|
|
133
|
+
for tid,path,note in records:
|
|
134
|
+
rollout=pathlib.Path(path);lines=[json.loads(line) for line in rollout.read_bytes().splitlines()];ordinal=max(r.get('ordinal',0) for r in lines)+1
|
|
135
|
+
with rollout.open('ab') as stream:stream.write(canonical({'timestamp':now(),'ordinal':ordinal,'type':'event_msg','payload':{'type':'user_message','message':note,'images':[],'local_images':[]}})+b'\n')
|
|
136
|
+
db.execute('UPDATE threads SET has_user_event=1,first_user_message=?,preview=?,memory_mode=? WHERE id=?',(note,note,'disabled',tid))
|
|
137
|
+
db.commit()
|
|
138
|
+
finally:db.close()
|
|
139
|
+
with Backend(binary,home,root) as b:
|
|
140
|
+
listed=set();cursor=None
|
|
141
|
+
while True:
|
|
142
|
+
params={'limit':100}
|
|
143
|
+
if cursor:params['cursor']=cursor
|
|
144
|
+
page=b.call('thread/list',params);listed.update(t['id'] for t in page['data']);cursor=page.get('nextCursor')
|
|
145
|
+
if not cursor:break
|
|
146
|
+
if not {a['threadId'] for a in seeded}<=listed:raise PackError('Native catalog did not discover every fresh agent')
|
|
147
|
+
for a in seeded:
|
|
148
|
+
b.call('thread/read',{'threadId':a['threadId'],'includeTurns':False})
|
|
149
|
+
if b.call('thread/turns/list',{'threadId':a['threadId'],'limit':1,'itemsView':'notLoaded'})['data']:raise PackError('Fresh seed unexpectedly has turns')
|
|
150
|
+
manifest,rev=native.capture(home,lib,name,template={'agents':seeded,'dependencies':m.get('dependencies',{})},workspace_mode='copy',live=False)
|
|
151
|
+
manifest['startingContext']={'kind':'fresh','seedHome':str(home),'sourcePack':source,'sourceRevision':revision,'createdAt':now(),'modelTurnsStarted':False,'previousHistoryCopied':False,'reviewFilesIncluded':False,'memoryMode':'disabled'}
|
|
152
|
+
if authored is not None:
|
|
153
|
+
manifest['startingContext']['templateId']=m['packId']
|
|
154
|
+
manifest['startingContext']['templateVersion']=m['templateVersion']
|
|
155
|
+
manifest['startingContext']['templateDigest']=revision
|
|
156
|
+
rev=lib.save(manifest)
|
|
157
|
+
return {'state':'fresh-pack-created-not-activated','pack':name,'revision':rev,'agents':len(seeded),'nativeCatalogVerified':True,'modelTurnsStarted':False,'previousHistoryCopied':False,'contextDirectory':str(context)}
|
|
158
|
+
finally:shutil.rmtree(root)
|