@topolo/cli 0.13.17 → 0.13.21
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 +18 -4
- package/agent-packs/README.md +469 -1
- package/agent-packs/coverage_changed.py +1 -1
- package/agent-packs/docs/selected-artifacts.md +47 -0
- package/agent-packs/documentation/cloud-task-state.md +56 -0
- package/agent-packs/domain-contexts/README.md +25 -0
- package/agent-packs/domain-contexts/cicd.json +8 -0
- package/agent-packs/gui/feed-adapter.js +4 -0
- package/agent-packs/gui/index.html +9 -0
- package/agent-packs/gui/studio.css +14 -0
- package/agent-packs/gui/studio.js +121 -0
- package/agent-packs/gui/studio_server.py +355 -0
- package/agent-packs/roles/cicd/CLAUDE.md +24 -0
- package/agent-packs/specializations/cicd--cicd.json +5 -0
- package/agent-packs/specializations/cicd--pipeline-engineering.json +5 -0
- package/agent-packs/specializations/cicd--promotion-deployment.json +5 -0
- package/agent-packs/specializations/cicd--queue-recovery.json +5 -0
- package/agent-packs/specializations/cicd--release-verification.json +5 -0
- package/agent-packs/specializations/cicd--runner-capacity.json +5 -0
- package/agent-packs/specializations/intakes--engineering-02.json +5 -0
- package/agent-packs/specializations/intakes--engineering-03.json +5 -0
- package/agent-packs/specializations/intakes--engineering.json +5 -0
- package/agent-packs/specializations/intakes--operator-01.json +5 -0
- package/agent-packs/specializations/intakes--operator-02.json +5 -0
- package/agent-packs/specializations/operator-skills--audio-and-narration-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--brand-and-copywriting-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--capability-research-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--data-analysis-and-visualization.json +5 -0
- package/agent-packs/specializations/operator-skills--financial-evidence-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--operating-template-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--operator-skill-library.json +5 -0
- package/agent-packs/specializations/operator-skills--operator-skills.json +5 -0
- package/agent-packs/specializations/operator-skills--operator-workflow-evaluation.json +5 -0
- package/agent-packs/specializations/operator-skills--privacy-and-agreement-review.json +5 -0
- package/agent-packs/specializations/operator-skills--storytelling-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--ux-and-accessibility-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--video-production-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--visual-design-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--workflow-integration-specialist.json +5 -0
- package/agent-packs/specializations/operators--access-and-privacy.json +5 -0
- package/agent-packs/specializations/operators--accounting-evidence-review.json +5 -0
- package/agent-packs/specializations/operators--agreement-operations.json +5 -0
- package/agent-packs/specializations/operators--books-operator.json +5 -0
- package/agent-packs/specializations/operators--brand-operator.json +5 -0
- package/agent-packs/specializations/operators--commerce-operations.json +5 -0
- package/agent-packs/specializations/operators--communications-operator.json +5 -0
- package/agent-packs/specializations/operators--content-and-assets-operator.json +5 -0
- package/agent-packs/specializations/operators--customer-operations.json +5 -0
- package/agent-packs/specializations/operators--design-operator.json +5 -0
- package/agent-packs/specializations/operators--design-production-02.json +5 -0
- package/agent-packs/specializations/operators--design-production-03.json +5 -0
- package/agent-packs/specializations/operators--design-production-04.json +5 -0
- package/agent-packs/specializations/operators--device-and-feed-operator.json +5 -0
- package/agent-packs/specializations/operators--director-operator.json +5 -0
- package/agent-packs/specializations/operators--experience-review.json +5 -0
- package/agent-packs/specializations/operators--growth-operations.json +5 -0
- package/agent-packs/specializations/operators--learn-operator.json +5 -0
- package/agent-packs/specializations/operators--localization-operator.json +5 -0
- package/agent-packs/specializations/operators--operators.json +5 -0
- package/agent-packs/specializations/operators--pay-operator.json +5 -0
- package/agent-packs/specializations/operators--research-and-intelligence.json +5 -0
- package/agent-packs/specializations/operators--roadmap-operator.json +5 -0
- package/agent-packs/specializations/operators--social-content-operator.json +5 -0
- package/agent-packs/specializations/operators--talent-operations.json +5 -0
- package/agent-packs/specializations/operators--web-operator.json +5 -0
- package/agent-packs/specializations/operators--workflow-operator.json +5 -0
- package/agent-packs/specializations/specialists--adversarial-qa.json +5 -0
- package/agent-packs/specializations/specialists--backlog-dispatch.json +5 -0
- package/agent-packs/specializations/specialists--build-01.json +5 -0
- package/agent-packs/specializations/specialists--build-02.json +5 -0
- package/agent-packs/specializations/specialists--build-03.json +5 -0
- package/agent-packs/specializations/specialists--developer-environment-steward.json +5 -0
- package/agent-packs/specializations/specialists--documentation-knowledge-steward.json +5 -0
- package/agent-packs/specializations/specialists--execution-readiness.json +5 -0
- package/agent-packs/specializations/specialists--incident-triage.json +5 -0
- package/agent-packs/specializations/specialists--platform-package-steward.json +5 -0
- package/agent-packs/specializations/specialists--product-direction-01.json +5 -0
- package/agent-packs/specializations/specialists--product-direction-02.json +5 -0
- package/agent-packs/specializations/specialists--release-recovery.json +5 -0
- package/agent-packs/specializations/specialists--release-watch.json +5 -0
- package/agent-packs/specializations/specialists--roadmap-planning-01.json +5 -0
- package/agent-packs/specializations/specialists--roadmap-planning-02.json +5 -0
- package/agent-packs/specializations/specialists--security-permissions-review.json +5 -0
- package/agent-packs/specializations/specialists--system-architecture-01.json +5 -0
- package/agent-packs/specializations/specialists--system-architecture-02.json +5 -0
- package/agent-packs/specializations/specialists--translation.json +5 -0
- package/agent-packs/specializations/specialists--ux-consistency-review.json +5 -0
- package/agent-packs/specializations/specialists--workflow-improvement.json +5 -0
- package/agent-packs/specializations/specialists--worktree-retirement.json +5 -0
- package/agent-packs/src/topolo_agent_pack/cicd.py +101 -0
- package/agent-packs/src/topolo_agent_pack/claude.py +22 -7
- package/agent-packs/src/topolo_agent_pack/claude_activation.py +157 -0
- package/agent-packs/src/topolo_agent_pack/claude_completion_recovery.py +216 -0
- package/agent-packs/src/topolo_agent_pack/claude_desktop.py +181 -0
- package/agent-packs/src/topolo_agent_pack/claude_dispatch.py +240 -0
- package/agent-packs/src/topolo_agent_pack/claude_prompt.py +74 -0
- package/agent-packs/src/topolo_agent_pack/claude_runner.py +207 -0
- package/agent-packs/src/topolo_agent_pack/claude_service.py +397 -0
- package/agent-packs/src/topolo_agent_pack/cli.py +188 -18
- package/agent-packs/src/topolo_agent_pack/closed_ledger_fence.py +118 -0
- package/agent-packs/src/topolo_agent_pack/cloud_attach.py +128 -0
- package/agent-packs/src/topolo_agent_pack/cloud_cutover.py +134 -0
- package/agent-packs/src/topolo_agent_pack/cloud_delta.py +162 -0
- package/agent-packs/src/topolo_agent_pack/cloud_migrate.py +142 -0
- package/agent-packs/src/topolo_agent_pack/cloud_recovery.py +42 -0
- package/agent-packs/src/topolo_agent_pack/cloud_work.py +126 -0
- package/agent-packs/src/topolo_agent_pack/coordinator_activity.py +27 -0
- package/agent-packs/src/topolo_agent_pack/coordinator_context.py +91 -0
- package/agent-packs/src/topolo_agent_pack/coordinator_refresh.py +116 -0
- package/agent-packs/src/topolo_agent_pack/desktop_acceptance.py +45 -0
- package/agent-packs/src/topolo_agent_pack/desktop_launcher.py +114 -0
- package/agent-packs/src/topolo_agent_pack/domain_context.py +38 -0
- package/agent-packs/src/topolo_agent_pack/execution.py +104 -7
- package/agent-packs/src/topolo_agent_pack/fresh.py +1 -1
- package/agent-packs/src/topolo_agent_pack/frontdoor.py +23 -0
- package/agent-packs/src/topolo_agent_pack/linked_task_context.py +80 -0
- package/agent-packs/src/topolo_agent_pack/live.py +285 -18
- package/agent-packs/src/topolo_agent_pack/native.py +160 -4
- package/agent-packs/src/topolo_agent_pack/policy.py +8 -0
- package/agent-packs/src/topolo_agent_pack/review_context.py +33 -0
- package/agent-packs/src/topolo_agent_pack/role_catalog.py +137 -0
- package/agent-packs/src/topolo_agent_pack/role_context.py +63 -0
- package/agent-packs/src/topolo_agent_pack/role_refresh.py +50 -0
- package/agent-packs/src/topolo_agent_pack/task_models.py +11 -7
- package/agent-packs/src/topolo_agent_pack/throughput.py +149 -0
- package/agent-packs/src/topolo_agent_pack/topolo.py +7 -1
- package/agent-packs/src/topolo_agent_pack/work.py +322 -0
- package/agent-packs/src/topolo_agent_pack/work_adoption.py +122 -0
- package/agent-packs/src/topolo_agent_pack/work_artifacts.py +84 -0
- package/agent-packs/src/topolo_agent_pack/work_context.py +77 -0
- package/agent-packs/src/topolo_agent_pack/work_gui.py +51 -0
- package/agent-packs/src/topolo_agent_pack/work_native.py +414 -0
- package/agent-packs/src/topolo_agent_pack/work_wait.py +151 -0
- package/agent-packs/templates/topolo-coordinators/AGENTS.md +7 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/cicd/AGENTS.md +6 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/cicd/config.json +4 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/engineering/AGENTS.md +3 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/engineering/config.json +4 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/operator-01/AGENTS.md +3 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/operator-01/config.json +4 -0
- package/agent-packs/templates/topolo-coordinators/pack.json +56 -0
- package/dist/chunk-NYDIAYVY.js +144 -0
- package/dist/{claude-pack-backend-YEVPAOID.js → chunk-UY5LEU5C.js} +1659 -21
- package/dist/claude-desktop-profile-JCQKSY3K.js +13 -0
- package/dist/claude-pack-activation-Q25MCEPU.js +506 -0
- package/dist/claude-pack-backend-HUTKGIDS.js +90 -0
- package/dist/index.js +408 -164
- package/package.json +8 -6
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"""Event-driven task dispatch on the existing owning native connection.
|
|
2
|
+
|
|
3
|
+
No extra app server, renderer automation, schedule or parallel thread reuse.
|
|
4
|
+
"""
|
|
5
|
+
import asyncio
|
|
6
|
+
import copy
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import pathlib
|
|
10
|
+
import shutil
|
|
11
|
+
import time
|
|
12
|
+
from . import task_models, policy
|
|
13
|
+
from .execution import ExecutionGate
|
|
14
|
+
from .cicd import RELEASE_RISK_TYPES
|
|
15
|
+
from .storage import PackError, read_json, atomic_json, canonical, component_key
|
|
16
|
+
from .work import Store, require, parse_route, TERMINAL
|
|
17
|
+
|
|
18
|
+
class Broker:
|
|
19
|
+
def __init__(self,owner):
|
|
20
|
+
from .cloud_work import open_store
|
|
21
|
+
self.owner=owner;self.store=open_store(owner.lib.root);self.lock=asyncio.Lock();self.jobs=set();self.watchers=set();self.native_ids=set()
|
|
22
|
+
from .cloud_cutover import closure
|
|
23
|
+
self.cutover_closure=closure();self.cutover_record=None
|
|
24
|
+
def writable(self):
|
|
25
|
+
require(not getattr(self.owner,'quiesced',False),'Native task admission is quiesced for cutover')
|
|
26
|
+
if (self.store.root/'cutover.json').exists():
|
|
27
|
+
require(getattr(self.store,'cloud',False),'Native task admission is quiesced for cutover')
|
|
28
|
+
from .cloud_migrate import verified
|
|
29
|
+
verified(self.store.root.parent)
|
|
30
|
+
async def quiesce(self,cutover=False):
|
|
31
|
+
async with self.lock:
|
|
32
|
+
self.writable()
|
|
33
|
+
require(not any(not job.done() for job in self.jobs),'Native task broker jobs are still live')
|
|
34
|
+
require(not self.owner.active_turns and not self.owner.pending,'Native task delivery is still live')
|
|
35
|
+
from .cloud_cutover import freeze,acknowledge
|
|
36
|
+
if cutover:record=freeze(self.owner)
|
|
37
|
+
else:
|
|
38
|
+
require(all(w['state']=='archived' and w.get('nativeArchived') is True or w['state']=='failed' and not w.get('turnId') and (not w.get('nativeId') or w.get('nativeArchived') is True) for w in await self.state_call('workers')),'Native worker closure is unresolved')
|
|
39
|
+
self.owner.quiesced=True
|
|
40
|
+
await self.owner.stop_child()
|
|
41
|
+
if cutover:self.cutover_record=acknowledge(self.owner,record)
|
|
42
|
+
return self.cutover_record
|
|
43
|
+
async def state_call(self,name,*args,**kwargs):
|
|
44
|
+
from .cloud_work import READS
|
|
45
|
+
if name not in READS:self.writable()
|
|
46
|
+
method=getattr(self.store,name)
|
|
47
|
+
return await asyncio.to_thread(method,*args,**kwargs) if getattr(self.store,'cloud',False) else method(*args,**kwargs)
|
|
48
|
+
async def initialize(self):
|
|
49
|
+
self.native_ids={w['nativeId'] for w in await self.state_call('workers') if w.get('nativeId')}
|
|
50
|
+
await self.state_call('recover')
|
|
51
|
+
if read_json(self.store.root/'config.json',{}).get('frontdoorDispatch') is True:
|
|
52
|
+
from .role_context import materialize as materialize_role
|
|
53
|
+
for role in self.owner.execution.roles.values():
|
|
54
|
+
if '/intakes/' in role['key']:
|
|
55
|
+
await asyncio.to_thread(materialize_role,self.store.root,role['key'],pathlib.Path(role['cwd']))
|
|
56
|
+
async def resume_ready(self):
|
|
57
|
+
if (self.store.root/'cloud-migration.json').exists():
|
|
58
|
+
from .cloud_migrate import verified
|
|
59
|
+
verified(self.store.root.parent)
|
|
60
|
+
# Attaching this installation never transfers another installation's
|
|
61
|
+
# task custody, including otherwise resumable cloud checkpoints.
|
|
62
|
+
return
|
|
63
|
+
if not getattr(self.store,'cloud',False) or read_json(self.store.root/'config.json',{}).get('format')!='topolo-task-dispatch-v2':return
|
|
64
|
+
try:
|
|
65
|
+
tasks=await self.state_call('list')
|
|
66
|
+
if not any(t['state'] in {'queued','executing','reviewing'} for t in tasks):return
|
|
67
|
+
await self.owner.ensure_catalog()
|
|
68
|
+
for role in list(self.owner.execution.roles.values()):
|
|
69
|
+
if '/intakes/' not in role['key']:continue
|
|
70
|
+
require(role.get('installationConsent') and role['settings']['sandbox_mode']=='danger-full-access' and role['settings']['approval_policy']=='never','Fresh coordinators need qualified full-access installation consent before resumption')
|
|
71
|
+
await self.owner.qualify_organization(role['threadId'])
|
|
72
|
+
workers=await self.state_call('workers')
|
|
73
|
+
for task in tasks:
|
|
74
|
+
if task.get('ownerInstallationId')==self.store.binding['installationId'] or task.get('resumeAfterCheckpoint') is not True or task['state'] not in {'queued','executing','reviewing'}:continue
|
|
75
|
+
owned=[w for w in workers if w['taskId']==task['id']]
|
|
76
|
+
phase={'queued':'route','executing':'execute','reviewing':'review'}[task['state']]
|
|
77
|
+
if any(w['phase']==phase and w.get('attempt',1)==task.get('attempt',1) and (phase!='review' or w.get('reviewGeneration',0)==task.get('reviewGeneration',0)) for w in owned):continue
|
|
78
|
+
if not all(w['state']=='archived' and w.get('nativeArchived') is True or w['state']=='failed' and not w.get('turnId') and (not w.get('nativeId') or w.get('nativeArchived') is True) for w in owned):continue
|
|
79
|
+
await self.state_call('adopt_ready',task['id'],task['revision'])
|
|
80
|
+
self.owner.work_startup_error=None
|
|
81
|
+
await self.pump();await self.notify()
|
|
82
|
+
except Exception as exc:self.owner.work_startup_error=str(exc)
|
|
83
|
+
async def snapshot(self):
|
|
84
|
+
if getattr(self.store,'cloud',False):return await asyncio.to_thread(self.store.snapshot)
|
|
85
|
+
return {'tasks':(await self.state_call('list',)),'workers':(await self.state_call('workers',)),'events':(await self.state_call('events',)),'retainedCount':len((await self.state_call('retained',))),'authority':'local-migration-source'}
|
|
86
|
+
async def read(self,tid):
|
|
87
|
+
return {'task':await self.state_call('get',tid),'events':await self.state_call('events',tid)}
|
|
88
|
+
def config(self,channel='engineering'):
|
|
89
|
+
p=self.store.root/'config.json';require(p.is_file() and not p.is_symlink(),'Task dispatch is not installed for this runtime')
|
|
90
|
+
saved=read_json(p)
|
|
91
|
+
if saved.get('format')=='topolo-task-dispatch-v2':
|
|
92
|
+
from .role_catalog import configuration
|
|
93
|
+
require(getattr(self.store,'cloud',False),'Source-independent dispatch requires activated cloud task authority; no local fallback')
|
|
94
|
+
return configuration(self.owner,saved,channel)
|
|
95
|
+
require(set(saved)-{'authority','frontdoorDispatch'} in ({'pack','revision','templateRuntime','maxWorkers'},{'pack','revision','templateRuntime','maxWorkers','channels'}),'Invalid work dispatch configuration')
|
|
96
|
+
require('authority' not in saved or (saved['authority']=='topolo-docs' and getattr(self.store,'cloud',False)),'Task dispatch requires its selected cloud authority')
|
|
97
|
+
require(channel in {'engineering','operations','cicd'},'Unknown task channel')
|
|
98
|
+
selected=saved.get('channels',{}).get(channel)
|
|
99
|
+
if selected is None:
|
|
100
|
+
require(channel!='cicd','CI/CD task templates are not installed')
|
|
101
|
+
config={k:saved[k] for k in ['pack','revision','templateRuntime','maxWorkers']}
|
|
102
|
+
config.update(intakeRole='engineering' if channel=='engineering' else 'operator-01',reviewRole='adversarial-qa' if channel=='engineering' else 'workflow-integration-specialist')
|
|
103
|
+
else:
|
|
104
|
+
require(isinstance(selected,dict) and set(selected)=={'pack','revision','templateRuntime','intakeRole','reviewRole'},'Invalid channel binding')
|
|
105
|
+
config={**selected,'maxWorkers':saved['maxWorkers']}
|
|
106
|
+
m,revision=self.owner.lib.load(config['pack']);require(revision==config['revision'],'Role template revision changed; review before dispatch')
|
|
107
|
+
current=self.owner.lib.active();expected=config['pack']+'/intakes/'+config['intakeRole']
|
|
108
|
+
matches=[c for c in current['components'] if c['key']==expected]
|
|
109
|
+
require(len(matches)==1 and self.owner.execution.roles.get(matches[0]['threadId'],{}).get('key')==expected,'Task channel has no current owned coordinator')
|
|
110
|
+
require(type(config['maxWorkers']) is int and 1<=config['maxWorkers']<=8,'Invalid native capacity')
|
|
111
|
+
template=pathlib.Path(config['templateRuntime']);require(template.is_relative_to(self.owner.lib.root/'runtimes') and template.is_dir() and not template.is_symlink(),'Template runtime is outside owned storage')
|
|
112
|
+
model_config=read_json(self.owner.lib.root/'dynamic-models'/(config['pack']+'.json'));require(model_config['sourceRevision']==revision,'Task model policy revision is stale')
|
|
113
|
+
if (self.store.root/'cloud-migration.json').exists():
|
|
114
|
+
from .cloud_migrate import verified
|
|
115
|
+
migration=verified(self.store.root.parent);require(getattr(self.store,'cloud',False),'Verified cloud authority required')
|
|
116
|
+
selected=migration['channelRoleKeys'][channel]
|
|
117
|
+
require({component_key(a) for a in m['agents']}>=set(selected),'Reviewed channel specialization is missing')
|
|
118
|
+
m={**m,'sourceIndependent':True,'agents':[a for a in m['agents'] if component_key(a) in selected]}
|
|
119
|
+
config['catalogOnly']=True
|
|
120
|
+
return config,m,model_config
|
|
121
|
+
async def notify(self):
|
|
122
|
+
data=canonical({'sequence':(await self.state_call('events',))[-1]['sequence'] if (await self.state_call('events',)) else 0})+b'\n'
|
|
123
|
+
for writer in list(self.watchers):
|
|
124
|
+
try:writer.write(data);await writer.drain()
|
|
125
|
+
except (ConnectionError,OSError):self.watchers.discard(writer)
|
|
126
|
+
async def watch(self,reader,writer):
|
|
127
|
+
self.watchers.add(writer)
|
|
128
|
+
try:
|
|
129
|
+
writer.write(canonical({'connected':True})+b'\n');await writer.drain();await reader.read()
|
|
130
|
+
finally:self.watchers.discard(writer)
|
|
131
|
+
def launch(self,coroutine):
|
|
132
|
+
if getattr(self.owner,'quiesced',False):
|
|
133
|
+
coroutine.close();return
|
|
134
|
+
job=asyncio.create_task(coroutine);self.jobs.add(job);job.add_done_callback(self.jobs.discard)
|
|
135
|
+
async def submit(self,request):
|
|
136
|
+
self.writable()
|
|
137
|
+
self.config(request.get('channel') if isinstance(request,dict) else None);task,created=(await self.state_call('submit',request))
|
|
138
|
+
if created:self.launch(self.pump())
|
|
139
|
+
await self.notify();return {'task':task,'created':created,'delivery':'durable_request_not_completion'}
|
|
140
|
+
async def adoption_plan(self,request):
|
|
141
|
+
from .work_adoption import validate,read_evidence,plan
|
|
142
|
+
validate(request)
|
|
143
|
+
_,manifest,_=self.config(request['channel'])
|
|
144
|
+
require(getattr(self.store,'cloud',False),'Retained continuation requires activated cloud authority')
|
|
145
|
+
rows=[r for r in await self.state_call('retained') if r.get('claimId')==request['canonicalClaimId']]
|
|
146
|
+
require(len(rows)==1,'Unknown or ambiguous canonical claim; preserve custody')
|
|
147
|
+
claim=await asyncio.to_thread(read_evidence,self.store,request['checkpointRef'])
|
|
148
|
+
return plan(request,rows[0],claim,{a['id'] for a in manifest['agents'] if a['set']!='intakes'})
|
|
149
|
+
async def adopt_retained(self,request):
|
|
150
|
+
from .work_adoption import read_evidence,verify_authority
|
|
151
|
+
from . import permissions
|
|
152
|
+
async with self.lock:
|
|
153
|
+
self.writable()
|
|
154
|
+
prepared=await self.adoption_plan(request)
|
|
155
|
+
config,_,_=self.config(request['channel']);manifest,revision=self.owner.lib.load(config['pack'])
|
|
156
|
+
consent=permissions.load(self.owner.lib,config['pack'],manifest['agents'])
|
|
157
|
+
require(isinstance(consent,dict) and consent.get('pack')==config['pack'] and consent.get('sourceRevision')==revision,
|
|
158
|
+
'Current installation consent source revision differs')
|
|
159
|
+
actor=await asyncio.to_thread(self.store.client.run,['whoami'])
|
|
160
|
+
authorization=await asyncio.to_thread(read_evidence,self.store,request['authorizationRef'])
|
|
161
|
+
receipt=verify_authority(request,prepared,authorization,actor,consent,self.store.binding['installationId'])
|
|
162
|
+
current=await self.adoption_plan(request)
|
|
163
|
+
require(current==prepared,'Canonical checkpoint changed during authorization')
|
|
164
|
+
task,created=await self.state_call('adopt_retained',request,prepared,receipt)
|
|
165
|
+
if created:self.launch(self.pump())
|
|
166
|
+
await self.notify()
|
|
167
|
+
return {'task':task,'created':created,'delivery':'human_authorized_diagnostic_dispatch_pending'}
|
|
168
|
+
async def frontdoor(self,value):
|
|
169
|
+
role=self.owner.execution.roles.get(value.get('params',{}).get('threadId'),{})
|
|
170
|
+
parts=role.get('key','').split('/')
|
|
171
|
+
if len(parts)!=3 or parts[1]!='intakes':return value
|
|
172
|
+
saved=read_json(self.store.root/'config.json',{})
|
|
173
|
+
if saved.get('frontdoorDispatch') is not True:return value
|
|
174
|
+
channel={'engineering':'engineering','operator-01':'operations','cicd':'cicd'}.get(parts[2])
|
|
175
|
+
require(channel is not None,'This legacy coordinator is not an active front door')
|
|
176
|
+
from .coordinator_context import context,augment
|
|
177
|
+
selected=context(self.owner.lib.root,channel,role['threadId'],value['params'].get('clientUserMessageId'),await self.snapshot(),'codex')
|
|
178
|
+
return augment(value,selected)
|
|
179
|
+
async def respond(self,request):
|
|
180
|
+
self.writable()
|
|
181
|
+
task,created=(await self.state_call('respond',request))
|
|
182
|
+
if created and task['state']=='queued':self.launch(self.pump())
|
|
183
|
+
await self.notify();return {'task':task,'created':created}
|
|
184
|
+
async def re_review(self,request):
|
|
185
|
+
async with self.lock:
|
|
186
|
+
self.writable()
|
|
187
|
+
task,created=await self.state_call('re_review',request)
|
|
188
|
+
if created:self.launch(self.pump())
|
|
189
|
+
await self.notify()
|
|
190
|
+
return {'task':task,'created':created,'executionReplayed':False}
|
|
191
|
+
async def reconcile_unstarted(self,params):
|
|
192
|
+
require(isinstance(params,dict) and set(params)=={'workerId'},'Exact worker identity required')
|
|
193
|
+
async with self.lock:
|
|
194
|
+
self.writable()
|
|
195
|
+
worker=(await self.state_call('worker',params['workerId']));owner=self.owner
|
|
196
|
+
require(worker['state'] in {'creating','unknown','archiving'} and not worker.get('turnId') and worker.get('nativeId'),'Only an owned worker without a sent turn can reconcile here')
|
|
197
|
+
tid=worker['nativeId'];require(tid not in owner.active_turns,'Worker is active; preserve custody')
|
|
198
|
+
native=await owner.call_native('thread/read',{'threadId':tid,'includeTurns':True})
|
|
199
|
+
thread=native.get('thread',{})
|
|
200
|
+
require(thread.get('id')==tid and thread.get('cwd')==worker['cwd'] and thread.get('turns')==[],
|
|
201
|
+
'Native history is not an empty owned worker; preserve custody without replay')
|
|
202
|
+
(await self.state_call('worker_update',worker['id'],{'state':'archiving'}))
|
|
203
|
+
await owner.call_native('thread/archive',{'threadId':tid})
|
|
204
|
+
listing=await owner.call_native('thread/list',{'limit':100,'archived':False})
|
|
205
|
+
require(tid not in {r['id'] for r in listing['data']},'Unstarted worker remains active')
|
|
206
|
+
(await self.state_call('worker_update',worker['id'],{'state':'failed','nativeArchived':True,'error':'Verified empty native history; archived without retry.'}))
|
|
207
|
+
owner.execution.roles.pop(tid,None);owner.execution.verified.discard(tid);self.persist()
|
|
208
|
+
task=(await self.state_call('get',worker['taskId']));(await self.state_call('update',task['id'],task['revision'],{'state':'blocked','summary':'Verified no native turn was sent. Empty worker archived; no task replayed.'},'unstarted-worker-reconciled'))
|
|
209
|
+
await self.notify()
|
|
210
|
+
return {'workerId':worker['id'],'nativeId':tid,'nativeTurns':0,'archived':True,'replayed':False}
|
|
211
|
+
async def reconcile_route(self,params):
|
|
212
|
+
require(isinstance(params,dict) and set(params)=={'taskId'},'Exact task identity required')
|
|
213
|
+
async with self.lock:
|
|
214
|
+
self.writable()
|
|
215
|
+
task=(await self.state_call('get',params['taskId']));require(task['state']=='blocked' and task['phase']=='route','Only a blocked routing result can reconcile')
|
|
216
|
+
workers=[w for w in (await self.state_call('workers',)) if w['taskId']==task['id'] and w['phase']=='route' and w.get('attempt',1)==task.get('attempt',1)]
|
|
217
|
+
require(len(workers)==1 and workers[0]['state']=='archived','Routing worker closure is unresolved')
|
|
218
|
+
proof=workers[0]['checkpoint']
|
|
219
|
+
if 'artifact' in proof:
|
|
220
|
+
from .work_artifacts import read_checkpoint
|
|
221
|
+
payload=await asyncio.to_thread(read_checkpoint,self.store.root,proof,task['id'],workers[0]['id'])
|
|
222
|
+
require(payload['phase']=='route' and payload['nativeReceipt']['status']=='completed' and payload['nativeReceipt']['toolItems']==0,'Saved routing checkpoint needs effect reconciliation')
|
|
223
|
+
actual={'status':'completed','items':[{'type':'agentMessage','text':payload['output']}]}
|
|
224
|
+
else:
|
|
225
|
+
path=pathlib.Path(proof['path'])
|
|
226
|
+
require(path.is_relative_to(self.store.root/'checkpoints'/task['id']) and not path.is_symlink() and hashlib.sha256(path.read_bytes()).hexdigest()==proof['sha256'],'Routing checkpoint changed')
|
|
227
|
+
actual=read_json(path)['nativeTurn']
|
|
228
|
+
require(actual['status']=='completed','Routing turn did not complete')
|
|
229
|
+
output='\n'.join(i.get('text','') for i in actual.get('items',[]) if i.get('type')=='agentMessage' and i.get('phase')!='commentary')
|
|
230
|
+
_,manifest,_=self.config(task['channel']);route=parse_route(output,{a['id'] for a in manifest['agents'] if a['set']!='intakes'})
|
|
231
|
+
result=(await self.state_call('update',task['id'],task['revision'],{'state':'executing','phase':'execute','route':route,'summary':'Saved routing result reconciled; no router turn replayed.'},'routing-checkpoint-reconciled'))
|
|
232
|
+
self.launch(self.pump());await self.notify();return {'task':result,'routingReplayed':False}
|
|
233
|
+
async def pump(self):
|
|
234
|
+
async with self.lock:
|
|
235
|
+
if getattr(self.owner,'quiesced',False):
|
|
236
|
+
return
|
|
237
|
+
try:self.writable();config,manifest,models=self.config()
|
|
238
|
+
except PackError:return
|
|
239
|
+
workers=(await self.state_call('workers',));occupied=sum(w['state'] in {'reserved','creating','running','archiving','unknown'} for w in workers)
|
|
240
|
+
if occupied>=config['maxWorkers']:
|
|
241
|
+
return
|
|
242
|
+
for task in reversed((await self.state_call('list',))):
|
|
243
|
+
if occupied>=config['maxWorkers']:break
|
|
244
|
+
if task['state'] in TERMINAL:continue
|
|
245
|
+
if getattr(self.store,'cloud',False) and task.get('ownerInstallationId')!=self.store.binding['installationId']:continue
|
|
246
|
+
phase={'queued':'route','routing':'route','executing':'execute','reviewing':'review'}[task['state']]
|
|
247
|
+
if any(w['taskId']==task['id'] and w['phase']==phase and w.get('attempt',1)==task.get('attempt',1) and (phase!='review' or w.get('reviewGeneration',0)==task.get('reviewGeneration',0)) for w in workers):continue
|
|
248
|
+
try:
|
|
249
|
+
channel_config,channel_manifest,channel_models=self.config(task['channel'])
|
|
250
|
+
await self.start(task,phase,channel_config,channel_manifest,channel_models);occupied+=1
|
|
251
|
+
except Exception as exc:
|
|
252
|
+
current=await self.state_call('get',task['id'])
|
|
253
|
+
if getattr(self.store,'cloud',False) and current.get('ownerInstallationId')!=self.store.binding['installationId']:continue
|
|
254
|
+
if current['state']=='unknown':
|
|
255
|
+
await self.notify()
|
|
256
|
+
continue
|
|
257
|
+
for reserved in (await self.state_call('workers',)):
|
|
258
|
+
if reserved['taskId']==task['id'] and reserved['state']=='reserved':
|
|
259
|
+
(await self.state_call('worker_update',reserved['id'],{'state':'failed','error':str(exc)}))
|
|
260
|
+
current=(await self.state_call('get',task['id']));(await self.state_call('update',task['id'],current['revision'],{'state':'blocked','summary':str(exc)},'dispatch-blocked'));await self.notify()
|
|
261
|
+
async def start(self,task,phase,config,manifest,models):
|
|
262
|
+
owner=self.owner
|
|
263
|
+
if phase=='execute' and task.get('adoption'):
|
|
264
|
+
prepared=await self.adoption_plan(task['adoption']['request'])
|
|
265
|
+
require(prepared==task['adoption']['plan'],'Adopted checkpoint changed before native dispatch; preserve custody')
|
|
266
|
+
intake=next((r for r in owner.execution.roles.values() if r['key']==config['pack']+'/intakes/'+config['intakeRole']),None)
|
|
267
|
+
require(intake is not None,'Original coordinator is missing')
|
|
268
|
+
await owner.qualify_organization(intake['threadId'])
|
|
269
|
+
catalog=await owner.read_catalog();native_catalog={'harness':'codex','source':'native-account','connectionGeneration':str(owner.child.pid),'models':[{'model':x['model'],'efforts':[v['reasoningEffort'] for v in x['supportedReasoningEfforts']]} for x in catalog['data']]}
|
|
270
|
+
if phase=='route':
|
|
271
|
+
role_id=config['intakeRole'];model_task={'type':'route-request','complexity':'routine','risk':'normal'}
|
|
272
|
+
elif phase=='execute':role_id=task['route']['role'];model_task=task['route']['task']
|
|
273
|
+
else:
|
|
274
|
+
role_id=config['reviewRole'];model_task={**task['route']['task'],'type':'verify-task','complexity':'complex' if task['route']['task']['risk']=='security-critical' or task['channel']=='cicd' and task['route']['task']['type'] in RELEASE_RISK_TYPES else 'standard'}
|
|
275
|
+
matches=[a for a in manifest['agents'] if a['id']==role_id];require(len(matches)==1,'Role template is ambiguous');agent=matches[0];key=component_key(agent)
|
|
276
|
+
constraints=task_models.role_constraints(models['constraints'],key);resolution=task_models.resolve(models['configuration'],model_task,'codex',native_catalog,constraints)
|
|
277
|
+
require(task['channel']!='cicd' or model_task['type'] not in RELEASE_RISK_TYPES or resolution['profile']=='deep','Release-risk tasks require a deep profile')
|
|
278
|
+
worker=(await self.state_call('reserve',task['id'],phase,key,resolution,config['maxWorkers']));wid=worker['id'];cwd=pathlib.Path(owner.runtime)/'task-workspaces'/wid
|
|
279
|
+
if config.get('catalogOnly'):
|
|
280
|
+
source=copy.deepcopy(intake)
|
|
281
|
+
else:
|
|
282
|
+
source_rows=read_json(pathlib.Path(config['templateRuntime'])/'execution-policy.json')['roles'];source=next((r for r in source_rows if r['key']==key),None)
|
|
283
|
+
require(source is not None,'Discipline permission context is missing')
|
|
284
|
+
cwd.mkdir(parents=True,mode=0o700)
|
|
285
|
+
from .work_context import ensure as ensure_context
|
|
286
|
+
context_ref=await asyncio.to_thread(ensure_context,self.store.root,task)
|
|
287
|
+
if task.get('contextRef')!=context_ref:
|
|
288
|
+
current=await self.state_call('get',task['id'])
|
|
289
|
+
task=await self.state_call('update',task['id'],current['revision'],{'contextRef':context_ref,'summary':'Individual cloud task context is verified; native dispatch is next.'},'task-context-bound')
|
|
290
|
+
from .role_context import materialize as materialize_role
|
|
291
|
+
role_receipt=await asyncio.to_thread(materialize_role,self.store.root,key,cwd)
|
|
292
|
+
with (cwd/'AGENTS.md').open('a') as file:
|
|
293
|
+
file.write('\n\n## Current bounded task assignment\nThis is a new single-use worker, not a reusable role lane. The current task packet overrides older standby/intake routing examples. Do only the assigned phase. Never send peer chat messages, create schedules, take over existing claims or replay UNKNOWN effects. Your output is checkpointed before this native thread is archived. The shared task record retains continuity. Use /opt/homebrew/bin/topolo for supported platform actions when authorized.\n')
|
|
294
|
+
atomic_json(cwd/'TASK.json',task)
|
|
295
|
+
from .work_context import materialize
|
|
296
|
+
await asyncio.to_thread(materialize,self.store.root,task,cwd)
|
|
297
|
+
if phase in {'execute','review'}:
|
|
298
|
+
from .linked_task_context import materialize as materialize_linked
|
|
299
|
+
await materialize_linked(self.store,task,cwd,lambda tid:self.state_call('get',tid))
|
|
300
|
+
if config.get('catalogOnly') and phase=='review':
|
|
301
|
+
from .work_artifacts import materialize_result
|
|
302
|
+
await asyncio.to_thread(materialize_result,self.store.root,task,cwd)
|
|
303
|
+
spec=copy.deepcopy(source['settings']);spec.update(model=resolution['resolved']['model'],model_reasoning_effort=resolution['resolved']['effort'])
|
|
304
|
+
(await self.state_call('worker_update',wid,{'state':'creating','cwd':str(cwd),'roleSource':role_receipt}))
|
|
305
|
+
native=await owner.call_native('thread/start',{'cwd':str(cwd),'model':spec['model'],'approvalPolicy':spec['approval_policy'],'approvalsReviewer':spec['approvals_reviewer'],'sandbox':spec['sandbox_mode'],'config':{'model_reasoning_effort':spec['model_reasoning_effort'],'sandbox_workspace_write':spec['sandbox_workspace_write'],'apps':spec['apps']},'historyMode':'paginated','ephemeral':False,'allowProviderModelFallback':False})
|
|
306
|
+
tid=native['thread']['id'];(await self.state_call('worker_update',wid,{'nativeId':tid}))
|
|
307
|
+
self.native_ids.add(tid)
|
|
308
|
+
row={**copy.deepcopy(source),'key':config['pack']+'/tasks/'+wid,'threadId':tid,'cwd':str(cwd),'sourceRoleKey':key,'sourceRoleNativeId':source['threadId'],'sourcePermissionRoleKey':source['key'],'taskWorkerId':wid,'taskId':task['id'],'taskModelResolution':resolution,'settings':spec,'intakeKey':intake['key']}
|
|
309
|
+
candidate=ExecutionGate({'format':'topolo-codex-runtime-execution','schemaVersion':1,'roles':[row]});owner.execution.roles[tid]=candidate.roles[tid];owner.execution.set_catalog(catalog);owner.catalog_ready.set();self.persist()
|
|
310
|
+
await owner.call_native('thread/name/set',{'threadId':tid,'name':phase.capitalize()+' · '+task['objective'][:70]})
|
|
311
|
+
resumed=await owner.call_native('thread/resume',{'threadId':tid,'allowProviderModelFallback':False})
|
|
312
|
+
require(tid in owner.execution.verified,'Task model/effort did not receive native ACK')
|
|
313
|
+
require(resumed.get('model')==spec['model'] and resumed.get('reasoningEffort')==spec['model_reasoning_effort'],'Native task settings acknowledgement differs')
|
|
314
|
+
resolution['nativeACK']={'boundary':'native thread/resume settings acknowledgement','threadId':tid,'model':resumed['model'],'effort':resumed['reasoningEffort'],'providerFallbackAllowed':False}
|
|
315
|
+
await self.state_call('worker_update',wid,{'resolution':resolution})
|
|
316
|
+
owner.execution.roles[tid]['taskModelResolution']=copy.deepcopy(resolution);self.persist()
|
|
317
|
+
prompt=self.prompt(task,phase,manifest,cwd)
|
|
318
|
+
current=(await self.state_call('get',task['id']));(await self.state_call('update',task['id'],current['revision'],{'state':{'route':'routing','execute':'executing','review':'reviewing'}[phase],'phase':phase,'workerIds':current['workerIds']+[wid],'summary':{'route':'Coordinator is selecting the discipline and task profile.','execute':'A fresh task worker is carrying out the request.','review':'An independent fresh specialist is checking the result.'}[phase]},'native-resume-verified'))
|
|
319
|
+
(await self.state_call('worker_update',wid,{'state':'running'}))
|
|
320
|
+
try:
|
|
321
|
+
started=await owner.call_native('turn/start',{'threadId':tid,'input':[{'type':'text','text':prompt}]});(await self.state_call('worker_update',wid,{'turnId':started['turn']['id']}))
|
|
322
|
+
except Exception as exc:
|
|
323
|
+
(await self.state_call('worker_update',wid,{'state':'unknown','error':str(exc)}));current=(await self.state_call('get',task['id']));(await self.state_call('update',task['id'],current['revision'],{'state':'unknown','summary':'Native task delivery is uncertain; no retry will be sent.'},'delivery-unknown'));raise
|
|
324
|
+
await self.notify()
|
|
325
|
+
def prompt(self,task,phase,manifest,cwd):
|
|
326
|
+
common='Ash authorized this task through the local engineering GUI/native coordinator. Task identity: '+task['id']+'. Shared packet: '+str(cwd/'TASK.json')+'.\nObjective: '+task['objective']+'\n'
|
|
327
|
+
if phase in {'execute','review'} and (cwd/'context/linked-tasks.json').is_file():
|
|
328
|
+
common+='Read context/linked-tasks.json and its selected task/checkpoint/context/evidence files before execution or review. These are the explicitly cited existing findings from the same cloud task authority. Do not replace them with the first retained page or repeat a completed review. Missing cited tasks remain explicit; preserve their owners and effects.\n'
|
|
329
|
+
if task.get('adoption'):
|
|
330
|
+
common+='Accepted checkpoint continuation: '+json.dumps(task['adoption'])+'\nCurrent effect boundary: '+task['effectBoundary']+'\n'
|
|
331
|
+
if phase=='route':
|
|
332
|
+
roles=[{'id':a['id'],'title':a['title']} for a in manifest['agents'] if a['set']!='intakes']
|
|
333
|
+
return common+'You are the short routing phase only. Do not perform the work, use tools, contact chats or claim existing tasks. Select the suitable discipline from '+json.dumps(roles)+'. Return ONLY JSON with keys role (exact role id), task {type,complexity,risk}, acceptance (clear measurable criteria). Complexity: trivial/routine/standard/complex. Risk: low/normal/security-critical. Trivial is only low-risk format, extract-fields, reference-lookup or status-read work. Ordinary coding is routine or higher. Security-sensitive work is security-critical. Be conservative when uncertain. No markdown.'
|
|
334
|
+
if phase=='execute' and manifest.get('sourceIndependent'):
|
|
335
|
+
return common+'Read AGENTS.md, TASK.json and the relevant context. Execute only the assigned acceptance: '+task['route']['acceptance']+'. Preserve current owners, pauses and uncertain effects. Return ONLY JSON {"summary":"actual result and checks","artifacts":["absolute regular file paths inside this worker workspace only"],"references":["canonical Topolo or Git references"]}. At most sixteen artifact files; omit files when a canonical platform/Git reference is appropriate. Never include secrets, whole workspace/history exports or raw deliberation. The adapter verifies and publishes declared files before closing this worker. Do not claim delivery without actual evidence.'
|
|
336
|
+
if phase=='review' and manifest.get('sourceIndependent'):
|
|
337
|
+
common+='Read context/worker-result.json and context/result-source.json. They contain the selected cloud checkpoint and verified artifact cache paths; do not depend on any previous machine or worker directory. Evidence paths in your final review must be regular files in this reviewer workspace. '
|
|
338
|
+
if phase=='execute':return common+'Read AGENTS.md, TASK.json and relevant context. Work within the objective and acceptance: '+task['route']['acceptance']+'. Preserve existing source/task owners and explicit pauses. If an existing owner or missing authority prevents execution, report the exact blocker instead of duplicating work. Do not manage this lane or start other agents. Return a concise result, actual checks performed and exact evidence/artifact paths. A native turn finishing alone is not business completion.'
|
|
339
|
+
return common+'Independent verification phase. Do not implement changes, contact chats, modify source or perform business effects. Inspect the actual worker result and the referenced evidence against acceptance: '+task['route']['acceptance']+'. Worker result: '+json.dumps(task.get('result',{}))+'. Return ONLY JSON {"verdict":"completed"|"needs_input"|"blocked","summary":"...","evidence":["absolute existing evidence path",...]}. Choose completed only when actual inspected evidence satisfies acceptance; otherwise preserve the gap. No markdown.'
|
|
340
|
+
def persist(self):
|
|
341
|
+
owner=self.owner;atomic_json(pathlib.Path(owner.runtime)/'execution-policy.json',{'format':'topolo-codex-runtime-execution','schemaVersion':1,'roles':list(owner.execution.roles.values()),'applied':False,'modelTurnsStarted':False})
|
|
342
|
+
async def progress(self,params):
|
|
343
|
+
item=params.get('item',{})
|
|
344
|
+
if item.get('type')!='agentMessage' or item.get('phase')!='commentary' or not isinstance(item.get('text'),str) or not item['text'].strip():return
|
|
345
|
+
async with self.lock:
|
|
346
|
+
worker=next((w for w in await self.state_call('workers') if w.get('nativeId')==params.get('threadId') and w['state']=='running' and w['phase']=='execute'),None)
|
|
347
|
+
if worker:
|
|
348
|
+
await self.state_call('progress',worker['id'],item['id'],item['text'].strip()[:1600])
|
|
349
|
+
await self.notify()
|
|
350
|
+
def completed(self,tid,turn):
|
|
351
|
+
self.launch(self.completed_worker(tid,turn))
|
|
352
|
+
async def completed_worker(self,tid,turn):
|
|
353
|
+
worker=next((w for w in (await self.state_call('workers',)) if w['nativeId']==tid and w['state']=='running'),None)
|
|
354
|
+
if worker:self.launch(self.finish(worker['id'],turn))
|
|
355
|
+
async def read_turn(self,worker,turn):
|
|
356
|
+
native=await self.owner.call_native('thread/read',{'threadId':worker['nativeId'],'includeTurns':True})
|
|
357
|
+
matches=[t for t in native['thread'].get('turns',[]) if t['id']==turn['id']]
|
|
358
|
+
require(len(matches)==1,'Completed native turn receipt is unavailable')
|
|
359
|
+
return matches[0]
|
|
360
|
+
async def archive_worker(self,worker):
|
|
361
|
+
tid=worker['nativeId']
|
|
362
|
+
await self.owner.call_native('thread/archive',{'threadId':tid})
|
|
363
|
+
archived=await self.owner.call_native('thread/read',{'threadId':tid,'includeTurns':False})
|
|
364
|
+
require(archived['thread']['id']==tid,'Archive readback identity changed')
|
|
365
|
+
listing=await self.owner.call_native('thread/list',{'limit':100,'archived':False})
|
|
366
|
+
require(tid not in {r['id'] for r in listing['data']},'Worker remains in native active list')
|
|
367
|
+
return {}
|
|
368
|
+
async def finish(self,wid,turn):
|
|
369
|
+
async with self.lock:
|
|
370
|
+
worker=(await self.state_call('worker',wid))
|
|
371
|
+
if worker['state']!='running':
|
|
372
|
+
return
|
|
373
|
+
owner=self.owner;task=(await self.state_call('get',worker['taskId']));tid=worker['nativeId']
|
|
374
|
+
try:
|
|
375
|
+
actual=await self.read_turn(worker,turn)
|
|
376
|
+
text_value='\n'.join(i.get('text','') for i in actual.get('items',[]) if i.get('type')=='agentMessage' and i.get('phase')!='commentary')
|
|
377
|
+
if not text_value:text_value='\n'.join(i.get('text','') for i in actual.get('items',[]) if i.get('type')=='agentMessage')
|
|
378
|
+
portable=read_json(self.store.root/'config.json',{}).get('format')=='topolo-task-dispatch-v2';normalized=None;portable_payload=None
|
|
379
|
+
checkpoint=self.store.root/'checkpoints'/task['id']/(wid+'.json');checkpoint.parent.mkdir(parents=True,exist_ok=True)
|
|
380
|
+
if portable:
|
|
381
|
+
from .work_artifacts import checkpoint as cloud_checkpoint
|
|
382
|
+
proof,portable_payload,normalized=await asyncio.to_thread(cloud_checkpoint,self.store.root,task,worker,actual)
|
|
383
|
+
atomic_json(checkpoint,portable_payload)
|
|
384
|
+
else:
|
|
385
|
+
atomic_json(checkpoint,{'taskId':task['id'],'worker':worker,'nativeTurn':actual,'resolution':worker['resolution']});proof={'path':str(checkpoint),'sha256':hashlib.sha256(checkpoint.read_bytes()).hexdigest()}
|
|
386
|
+
(await self.state_call('worker_update',wid,{'state':'archiving','checkpoint':proof}))
|
|
387
|
+
# Persist the result before archiving; a failed archive retains
|
|
388
|
+
# its exact native identity and is never replaced by a new one.
|
|
389
|
+
current=(await self.state_call('get',task['id']));task=(await self.state_call('update',task['id'],current['revision'],{'checkpoints':current['checkpoints']+[proof]},'checkpoint-saved'))
|
|
390
|
+
retirement=await self.archive_worker(worker)
|
|
391
|
+
(await self.state_call('worker_update',wid,{'state':'archived','nativeArchived':True,**retirement}));owner.execution.roles.pop(tid,None);owner.execution.verified.discard(tid);self.persist()
|
|
392
|
+
if actual['status']!='completed':
|
|
393
|
+
raise PackError('Native worker '+actual['status']+'; checkpoint retained')
|
|
394
|
+
if portable and portable_payload['outputError']:
|
|
395
|
+
raise PackError(portable_payload['outputError'])
|
|
396
|
+
if worker['phase']=='route':
|
|
397
|
+
_,manifest,_=self.config(task['channel']);route=parse_route(text_value,{a['id'] for a in manifest['agents'] if a['set']!='intakes'});task=(await self.state_call('update',task['id'],task['revision'],{'state':'executing','phase':'execute','route':route,'summary':'Routed to '+route['role']+'; a new worker will be created.'},'coordinator-released'))
|
|
398
|
+
elif worker['phase']=='execute':
|
|
399
|
+
result={**normalized,'checkpoint':proof,'workerId':worker['id']} if portable else {'summary':text_value,'checkpoint':proof}
|
|
400
|
+
task=(await self.state_call('update',task['id'],task['revision'],{'state':'reviewing','phase':'review','result':result,'summary':'Worker checkpoint saved and lane archived; independent verification pending.'},'worker-released'))
|
|
401
|
+
else:
|
|
402
|
+
raw=text_value.strip()
|
|
403
|
+
if raw.startswith('```'):raw=raw.split('\n',1)[1].rsplit('```',1)[0]
|
|
404
|
+
review=normalized if portable else json.loads(raw);require(set(review)=={'verdict','summary','evidence'} and review['verdict'] in {'completed','needs_input','blocked'} and isinstance(review['evidence'],list),'Invalid independent verification receipt')
|
|
405
|
+
evidence=[]
|
|
406
|
+
for value in ([] if portable else review['evidence']):
|
|
407
|
+
p=pathlib.Path(value);require(p.is_absolute() and p.is_file() and not p.is_symlink(),'Verification evidence is missing');evidence.append({'path':str(p),'sha256':hashlib.sha256(p.read_bytes()).hexdigest()})
|
|
408
|
+
if portable:evidence=review['evidence']
|
|
409
|
+
if review['verdict']=='completed':require(bool(evidence),'Completion requires inspected evidence files')
|
|
410
|
+
task=(await self.state_call('update',task['id'],task['revision'],{'state':review['verdict'],'summary':review['summary'],'verification':{**review,'evidence':evidence,'nativeCheckpoint':proof}},'verification-recorded'))
|
|
411
|
+
except Exception as exc:
|
|
412
|
+
current=(await self.state_call('get',task['id']));(await self.state_call('update',task['id'],current['revision'],{'state':'blocked','summary':str(exc)},'completion-blocked'))
|
|
413
|
+
await self.notify()
|
|
414
|
+
self.launch(self.pump())
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Bounded read-only native task following; no dispatcher, retry or model turn."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import socket
|
|
5
|
+
import stat
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
|
|
9
|
+
from .storage import PackError, canonical, read_json
|
|
10
|
+
from .work import TERMINAL, require
|
|
11
|
+
|
|
12
|
+
LIMIT = 1024 * 1024
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _remaining(deadline):
|
|
16
|
+
remaining = deadline - time.monotonic()
|
|
17
|
+
if remaining <= 0:
|
|
18
|
+
raise TimeoutError('Task wait deadline reached')
|
|
19
|
+
return remaining
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _record(lib):
|
|
23
|
+
path = lib.root / 'live-bridge.json'
|
|
24
|
+
info = path.lstat()
|
|
25
|
+
require(stat.S_ISREG(info.st_mode) and info.st_uid == os.getuid()
|
|
26
|
+
and not info.st_mode & 0o077 and not path.is_symlink(),
|
|
27
|
+
'Task wait requires the privately owned native connection')
|
|
28
|
+
value = read_json(path)
|
|
29
|
+
require(isinstance(value, dict) and isinstance(value.get('token'), str) and value['token']
|
|
30
|
+
and isinstance(value.get('socket'), str) and os.path.isabs(value['socket']),
|
|
31
|
+
'Task wait native connection is unavailable')
|
|
32
|
+
info = os.lstat(value['socket'])
|
|
33
|
+
require(stat.S_ISSOCK(info.st_mode) and info.st_uid == os.getuid()
|
|
34
|
+
and not info.st_mode & 0o077, 'Task wait native socket ownership differs')
|
|
35
|
+
return value
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _Connection:
|
|
39
|
+
def __init__(self, lib, record, operation, params, deadline):
|
|
40
|
+
require(_record(lib) == record, 'Native owner changed during task wait; reconnect explicitly')
|
|
41
|
+
self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
42
|
+
self.buffer = b''
|
|
43
|
+
try:
|
|
44
|
+
self.socket.settimeout(_remaining(deadline))
|
|
45
|
+
self.socket.connect(record['socket'])
|
|
46
|
+
self.socket.settimeout(_remaining(deadline))
|
|
47
|
+
self.socket.sendall(canonical({'token': record['token'], 'operation': operation, 'params': params}) + b'\n')
|
|
48
|
+
except BaseException:
|
|
49
|
+
self.socket.close()
|
|
50
|
+
raise
|
|
51
|
+
|
|
52
|
+
def __enter__(self):
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def __exit__(self, *_args):
|
|
56
|
+
self.socket.close()
|
|
57
|
+
|
|
58
|
+
def read(self, deadline):
|
|
59
|
+
while b'\n' not in self.buffer:
|
|
60
|
+
require(len(self.buffer) <= LIMIT, 'Task wait response exceeds its boundary')
|
|
61
|
+
self.socket.settimeout(_remaining(deadline))
|
|
62
|
+
chunk = self.socket.recv(min(65536, LIMIT + 1 - len(self.buffer)))
|
|
63
|
+
require(chunk, 'Native task connection closed before its reply')
|
|
64
|
+
self.buffer += chunk
|
|
65
|
+
line, self.buffer = self.buffer.split(b'\n', 1)
|
|
66
|
+
require(len(line) <= LIMIT, 'Task wait response exceeds its boundary')
|
|
67
|
+
value = json.loads(line)
|
|
68
|
+
require(isinstance(value, dict), 'Native task reply is not an object')
|
|
69
|
+
require('error' not in value, 'Native task read refused: ' + str(value.get('error')))
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _snapshot(lib, record, task_id, deadline):
|
|
74
|
+
with _Connection(lib, record, 'work-read', {'taskId': task_id}, deadline) as connection:
|
|
75
|
+
value = connection.read(deadline)
|
|
76
|
+
require(set(value) == {'task', 'events'} and isinstance(value['task'], dict)
|
|
77
|
+
and value['task'].get('id') == task_id and isinstance(value['events'], list),
|
|
78
|
+
'Task wait readback identity differs')
|
|
79
|
+
require(type(value['task'].get('revision')) is int and value['task']['revision'] >= 1,
|
|
80
|
+
'Task wait revision is invalid')
|
|
81
|
+
events = []
|
|
82
|
+
for event in value['events']:
|
|
83
|
+
require(isinstance(event, dict), 'Task wait event is invalid')
|
|
84
|
+
if event.get('taskId') != task_id:
|
|
85
|
+
continue
|
|
86
|
+
require(type(event.get('sequence')) is int and event['sequence'] > 0,
|
|
87
|
+
'Task wait event sequence is invalid')
|
|
88
|
+
events.append(event)
|
|
89
|
+
require(len({event['sequence'] for event in events}) == len(events), 'Task event sequence repeats')
|
|
90
|
+
return {'task': value['task'], 'events': sorted(events, key=lambda event: event['sequence'])}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _result(task_id, snapshot, revision, sequence, timed_out):
|
|
94
|
+
if snapshot is None:
|
|
95
|
+
return {'taskId': task_id, 'task': None, 'events': [],
|
|
96
|
+
'cursor': {'revision': revision, 'sequence': sequence},
|
|
97
|
+
'changed': False, 'terminal': False, 'timedOut': True,
|
|
98
|
+
'snapshotAvailable': False, 'deliveryId': None}
|
|
99
|
+
task, events = snapshot['task'], snapshot['events']
|
|
100
|
+
current = max((event['sequence'] for event in events), default=0)
|
|
101
|
+
require(revision <= task['revision'] and sequence <= current,
|
|
102
|
+
'Task wait cursor is ahead of the canonical task')
|
|
103
|
+
return {'taskId': task_id, 'task': task,
|
|
104
|
+
'events': [event for event in events if event['sequence'] > sequence],
|
|
105
|
+
'cursor': {'revision': task['revision'], 'sequence': current},
|
|
106
|
+
'changed': task['revision'] > revision or current > sequence,
|
|
107
|
+
'terminal': task['state'] in TERMINAL, 'timedOut': timed_out,
|
|
108
|
+
'snapshotAvailable': True,
|
|
109
|
+
'deliveryId': task_id + ':' + str(task['revision']) + ':' + str(current)}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def wait(lib, value):
|
|
113
|
+
"""Follow one existing task within the caller's real native tool turn.
|
|
114
|
+
|
|
115
|
+
Repeat only on an explicit subsequent tool call with the returned cursor.
|
|
116
|
+
A timeout, native cancellation or disconnect never cancels/adopts work.
|
|
117
|
+
"""
|
|
118
|
+
require(isinstance(value, dict) and set(value) == {'taskId', 'afterRevision', 'afterSequence', 'timeoutSeconds'},
|
|
119
|
+
'Exact bounded task wait request required')
|
|
120
|
+
task_id = value['taskId']
|
|
121
|
+
try:
|
|
122
|
+
valid_id = isinstance(task_id, str) and str(uuid.UUID(task_id)) == task_id
|
|
123
|
+
except ValueError:
|
|
124
|
+
valid_id = False
|
|
125
|
+
require(valid_id, 'Task wait requires the exact canonical task UUID')
|
|
126
|
+
revision, sequence, seconds = value['afterRevision'], value['afterSequence'], value['timeoutSeconds']
|
|
127
|
+
require(type(revision) is int and revision >= 0 and type(sequence) is int and sequence >= 0
|
|
128
|
+
and type(seconds) is int and 1 <= seconds <= 30, 'Task wait cursor or deadline is invalid')
|
|
129
|
+
deadline = time.monotonic() + seconds
|
|
130
|
+
record = _record(lib)
|
|
131
|
+
snapshot = None
|
|
132
|
+
try:
|
|
133
|
+
snapshot = _snapshot(lib, record, task_id, deadline)
|
|
134
|
+
result = _result(task_id, snapshot, revision, sequence, False)
|
|
135
|
+
if result['changed'] or result['terminal']:
|
|
136
|
+
return result
|
|
137
|
+
with _Connection(lib, record, 'work-watch', {}, deadline) as connection:
|
|
138
|
+
require(connection.read(deadline) == {'connected': True}, 'Native task watch was not acknowledged')
|
|
139
|
+
# Subscribe first, then re-read. A completion between the first read
|
|
140
|
+
# and watcher registration cannot disappear into a notification gap.
|
|
141
|
+
snapshot = _snapshot(lib, record, task_id, deadline)
|
|
142
|
+
while True:
|
|
143
|
+
result = _result(task_id, snapshot, revision, sequence, False)
|
|
144
|
+
if result['changed'] or result['terminal']:
|
|
145
|
+
return result
|
|
146
|
+
event = connection.read(deadline)
|
|
147
|
+
require(set(event) == {'sequence'} and type(event['sequence']) is int and event['sequence'] >= 0,
|
|
148
|
+
'Native task notification is invalid')
|
|
149
|
+
snapshot = _snapshot(lib, record, task_id, deadline)
|
|
150
|
+
except TimeoutError:
|
|
151
|
+
return _result(task_id, snapshot, revision, sequence, True)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Topolo task execution rules
|
|
2
|
+
|
|
3
|
+
Load only the current specialization from Topolo Agent and the relevant canonical task and Docs/Bytes packets. This machine's conversation history is not task context. An empty workspace grants no permission to repeat, adopt or resume existing work.
|
|
4
|
+
|
|
5
|
+
Preserve task, claim, owner, intent and checkpoint identities. Respect explicit holds and uncertain effects. Use supported credential-scoped Topolo actions and the governed release controller. Keep credentials out of ordinary context and evidence. Record meaningful outcomes and evidence in the existing task; distinguish reported progress, native settings acknowledgement, execution, independent verification and deployment.
|
|
6
|
+
|
|
7
|
+
The three human front doors acknowledge broker receipts and remain available. Their local specialization charters define that boundary. Temporary task workers execute only their assigned phase and are archived after checkpointing; they do not become reusable lanes. Never load a whole role export, infer completion from a finished model turn, or create schedules or peer-message loops.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Topolo CI/CD
|
|
2
|
+
|
|
3
|
+
Acknowledge the queued request and remain available. If the adapter has not recorded a task, report that dispatch is unavailable; do not take over the implementation. Cloud specialization and task references must be qualified before model work.
|
|
4
|
+
|
|
5
|
+
## Release-controller operating context
|
|
6
|
+
The controller is the restricted internal service in Topolo-io/TopoloReleaseController, separate from the public application registry and Deploy Orchestrator. Read context/release-controller.md before routing or diagnosis: it is the selected immutable operating runbook, not task history. Current controller access, fleet endpoints and exact intent/job checkpoints belong in the selected task context. Dispatch immediate read-only diagnostics under the existing request; do not ask the human to re-explain the controller or approve an ordinary read. Use the existing broker, preserve owners and record precise missing observer access rather than guessing hosts or creating duplicate work. The submission gateway and native authority observer expose different read operations; follow the bound runbook. Mutations retain their existing release, review and effect gates.
|