@topolo/cli 0.13.19 → 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.
Files changed (142) hide show
  1. package/agent-packs/README.md +421 -1
  2. package/agent-packs/docs/selected-artifacts.md +47 -0
  3. package/agent-packs/documentation/cloud-task-state.md +56 -0
  4. package/agent-packs/domain-contexts/README.md +25 -0
  5. package/agent-packs/domain-contexts/cicd.json +8 -0
  6. package/agent-packs/gui/feed-adapter.js +4 -0
  7. package/agent-packs/gui/index.html +9 -0
  8. package/agent-packs/gui/studio.css +14 -0
  9. package/agent-packs/gui/studio.js +121 -0
  10. package/agent-packs/gui/studio_server.py +355 -0
  11. package/agent-packs/roles/cicd/CLAUDE.md +24 -0
  12. package/agent-packs/specializations/cicd--cicd.json +5 -0
  13. package/agent-packs/specializations/cicd--pipeline-engineering.json +5 -0
  14. package/agent-packs/specializations/cicd--promotion-deployment.json +5 -0
  15. package/agent-packs/specializations/cicd--queue-recovery.json +5 -0
  16. package/agent-packs/specializations/cicd--release-verification.json +5 -0
  17. package/agent-packs/specializations/cicd--runner-capacity.json +5 -0
  18. package/agent-packs/specializations/intakes--engineering-02.json +5 -0
  19. package/agent-packs/specializations/intakes--engineering-03.json +5 -0
  20. package/agent-packs/specializations/intakes--engineering.json +5 -0
  21. package/agent-packs/specializations/intakes--operator-01.json +5 -0
  22. package/agent-packs/specializations/intakes--operator-02.json +5 -0
  23. package/agent-packs/specializations/operator-skills--audio-and-narration-specialist.json +5 -0
  24. package/agent-packs/specializations/operator-skills--brand-and-copywriting-specialist.json +5 -0
  25. package/agent-packs/specializations/operator-skills--capability-research-specialist.json +5 -0
  26. package/agent-packs/specializations/operator-skills--data-analysis-and-visualization.json +5 -0
  27. package/agent-packs/specializations/operator-skills--financial-evidence-specialist.json +5 -0
  28. package/agent-packs/specializations/operator-skills--operating-template-specialist.json +5 -0
  29. package/agent-packs/specializations/operator-skills--operator-skill-library.json +5 -0
  30. package/agent-packs/specializations/operator-skills--operator-skills.json +5 -0
  31. package/agent-packs/specializations/operator-skills--operator-workflow-evaluation.json +5 -0
  32. package/agent-packs/specializations/operator-skills--privacy-and-agreement-review.json +5 -0
  33. package/agent-packs/specializations/operator-skills--storytelling-specialist.json +5 -0
  34. package/agent-packs/specializations/operator-skills--ux-and-accessibility-specialist.json +5 -0
  35. package/agent-packs/specializations/operator-skills--video-production-specialist.json +5 -0
  36. package/agent-packs/specializations/operator-skills--visual-design-specialist.json +5 -0
  37. package/agent-packs/specializations/operator-skills--workflow-integration-specialist.json +5 -0
  38. package/agent-packs/specializations/operators--access-and-privacy.json +5 -0
  39. package/agent-packs/specializations/operators--accounting-evidence-review.json +5 -0
  40. package/agent-packs/specializations/operators--agreement-operations.json +5 -0
  41. package/agent-packs/specializations/operators--books-operator.json +5 -0
  42. package/agent-packs/specializations/operators--brand-operator.json +5 -0
  43. package/agent-packs/specializations/operators--commerce-operations.json +5 -0
  44. package/agent-packs/specializations/operators--communications-operator.json +5 -0
  45. package/agent-packs/specializations/operators--content-and-assets-operator.json +5 -0
  46. package/agent-packs/specializations/operators--customer-operations.json +5 -0
  47. package/agent-packs/specializations/operators--design-operator.json +5 -0
  48. package/agent-packs/specializations/operators--design-production-02.json +5 -0
  49. package/agent-packs/specializations/operators--design-production-03.json +5 -0
  50. package/agent-packs/specializations/operators--design-production-04.json +5 -0
  51. package/agent-packs/specializations/operators--device-and-feed-operator.json +5 -0
  52. package/agent-packs/specializations/operators--director-operator.json +5 -0
  53. package/agent-packs/specializations/operators--experience-review.json +5 -0
  54. package/agent-packs/specializations/operators--growth-operations.json +5 -0
  55. package/agent-packs/specializations/operators--learn-operator.json +5 -0
  56. package/agent-packs/specializations/operators--localization-operator.json +5 -0
  57. package/agent-packs/specializations/operators--operators.json +5 -0
  58. package/agent-packs/specializations/operators--pay-operator.json +5 -0
  59. package/agent-packs/specializations/operators--research-and-intelligence.json +5 -0
  60. package/agent-packs/specializations/operators--roadmap-operator.json +5 -0
  61. package/agent-packs/specializations/operators--social-content-operator.json +5 -0
  62. package/agent-packs/specializations/operators--talent-operations.json +5 -0
  63. package/agent-packs/specializations/operators--web-operator.json +5 -0
  64. package/agent-packs/specializations/operators--workflow-operator.json +5 -0
  65. package/agent-packs/specializations/specialists--adversarial-qa.json +5 -0
  66. package/agent-packs/specializations/specialists--backlog-dispatch.json +5 -0
  67. package/agent-packs/specializations/specialists--build-01.json +5 -0
  68. package/agent-packs/specializations/specialists--build-02.json +5 -0
  69. package/agent-packs/specializations/specialists--build-03.json +5 -0
  70. package/agent-packs/specializations/specialists--developer-environment-steward.json +5 -0
  71. package/agent-packs/specializations/specialists--documentation-knowledge-steward.json +5 -0
  72. package/agent-packs/specializations/specialists--execution-readiness.json +5 -0
  73. package/agent-packs/specializations/specialists--incident-triage.json +5 -0
  74. package/agent-packs/specializations/specialists--platform-package-steward.json +5 -0
  75. package/agent-packs/specializations/specialists--product-direction-01.json +5 -0
  76. package/agent-packs/specializations/specialists--product-direction-02.json +5 -0
  77. package/agent-packs/specializations/specialists--release-recovery.json +5 -0
  78. package/agent-packs/specializations/specialists--release-watch.json +5 -0
  79. package/agent-packs/specializations/specialists--roadmap-planning-01.json +5 -0
  80. package/agent-packs/specializations/specialists--roadmap-planning-02.json +5 -0
  81. package/agent-packs/specializations/specialists--security-permissions-review.json +5 -0
  82. package/agent-packs/specializations/specialists--system-architecture-01.json +5 -0
  83. package/agent-packs/specializations/specialists--system-architecture-02.json +5 -0
  84. package/agent-packs/specializations/specialists--translation.json +5 -0
  85. package/agent-packs/specializations/specialists--ux-consistency-review.json +5 -0
  86. package/agent-packs/specializations/specialists--workflow-improvement.json +5 -0
  87. package/agent-packs/specializations/specialists--worktree-retirement.json +5 -0
  88. package/agent-packs/src/topolo_agent_pack/cicd.py +101 -0
  89. package/agent-packs/src/topolo_agent_pack/claude.py +22 -7
  90. package/agent-packs/src/topolo_agent_pack/claude_activation.py +157 -0
  91. package/agent-packs/src/topolo_agent_pack/claude_completion_recovery.py +216 -0
  92. package/agent-packs/src/topolo_agent_pack/claude_desktop.py +181 -0
  93. package/agent-packs/src/topolo_agent_pack/claude_dispatch.py +240 -0
  94. package/agent-packs/src/topolo_agent_pack/claude_prompt.py +74 -0
  95. package/agent-packs/src/topolo_agent_pack/claude_runner.py +207 -0
  96. package/agent-packs/src/topolo_agent_pack/claude_service.py +397 -0
  97. package/agent-packs/src/topolo_agent_pack/cli.py +181 -16
  98. package/agent-packs/src/topolo_agent_pack/closed_ledger_fence.py +118 -0
  99. package/agent-packs/src/topolo_agent_pack/cloud_attach.py +128 -0
  100. package/agent-packs/src/topolo_agent_pack/cloud_cutover.py +134 -0
  101. package/agent-packs/src/topolo_agent_pack/cloud_delta.py +162 -0
  102. package/agent-packs/src/topolo_agent_pack/cloud_migrate.py +142 -0
  103. package/agent-packs/src/topolo_agent_pack/cloud_recovery.py +42 -0
  104. package/agent-packs/src/topolo_agent_pack/cloud_work.py +126 -0
  105. package/agent-packs/src/topolo_agent_pack/coordinator_activity.py +27 -0
  106. package/agent-packs/src/topolo_agent_pack/coordinator_context.py +91 -0
  107. package/agent-packs/src/topolo_agent_pack/coordinator_refresh.py +116 -0
  108. package/agent-packs/src/topolo_agent_pack/desktop_acceptance.py +45 -0
  109. package/agent-packs/src/topolo_agent_pack/desktop_launcher.py +114 -0
  110. package/agent-packs/src/topolo_agent_pack/domain_context.py +38 -0
  111. package/agent-packs/src/topolo_agent_pack/execution.py +103 -6
  112. package/agent-packs/src/topolo_agent_pack/frontdoor.py +23 -0
  113. package/agent-packs/src/topolo_agent_pack/linked_task_context.py +80 -0
  114. package/agent-packs/src/topolo_agent_pack/live.py +285 -18
  115. package/agent-packs/src/topolo_agent_pack/native.py +160 -4
  116. package/agent-packs/src/topolo_agent_pack/review_context.py +33 -0
  117. package/agent-packs/src/topolo_agent_pack/role_catalog.py +137 -0
  118. package/agent-packs/src/topolo_agent_pack/role_context.py +63 -0
  119. package/agent-packs/src/topolo_agent_pack/role_refresh.py +50 -0
  120. package/agent-packs/src/topolo_agent_pack/topolo.py +7 -1
  121. package/agent-packs/src/topolo_agent_pack/work.py +322 -0
  122. package/agent-packs/src/topolo_agent_pack/work_adoption.py +122 -0
  123. package/agent-packs/src/topolo_agent_pack/work_artifacts.py +84 -0
  124. package/agent-packs/src/topolo_agent_pack/work_context.py +77 -0
  125. package/agent-packs/src/topolo_agent_pack/work_gui.py +51 -0
  126. package/agent-packs/src/topolo_agent_pack/work_native.py +414 -0
  127. package/agent-packs/src/topolo_agent_pack/work_wait.py +151 -0
  128. package/agent-packs/templates/topolo-coordinators/AGENTS.md +7 -0
  129. package/agent-packs/templates/topolo-coordinators/agents/intakes/cicd/AGENTS.md +6 -0
  130. package/agent-packs/templates/topolo-coordinators/agents/intakes/cicd/config.json +4 -0
  131. package/agent-packs/templates/topolo-coordinators/agents/intakes/engineering/AGENTS.md +3 -0
  132. package/agent-packs/templates/topolo-coordinators/agents/intakes/engineering/config.json +4 -0
  133. package/agent-packs/templates/topolo-coordinators/agents/intakes/operator-01/AGENTS.md +3 -0
  134. package/agent-packs/templates/topolo-coordinators/agents/intakes/operator-01/config.json +4 -0
  135. package/agent-packs/templates/topolo-coordinators/pack.json +56 -0
  136. package/dist/chunk-NYDIAYVY.js +144 -0
  137. package/dist/{claude-pack-backend-YEVPAOID.js → chunk-UY5LEU5C.js} +1659 -21
  138. package/dist/claude-desktop-profile-JCQKSY3K.js +13 -0
  139. package/dist/claude-pack-activation-Q25MCEPU.js +506 -0
  140. package/dist/claude-pack-backend-HUTKGIDS.js +90 -0
  141. package/dist/index.js +89 -10
  142. package/package.json +4 -4
@@ -0,0 +1,77 @@
1
+ """Pinned immutable Docs task context, fetched at assignment time."""
2
+ import hashlib
3
+ import pathlib
4
+ import json,tempfile
5
+ from .storage import read_json,atomic_json,PackError
6
+ from .work import require
7
+
8
+ def materialize(root,task,cwd,client_factory=None):
9
+ binding=pathlib.Path(root)/'context-refs'/(task['id']+'.json')
10
+ if not binding.exists() and not task.get('contextRef'):
11
+ require((cwd/'context/continuity.md').is_file(),'Task continuity context is missing; bind the existing cloud task packet before dispatch')
12
+ return
13
+ require(not binding.is_symlink(),'Task context binding cannot be a symlink')
14
+ ref=task.get('contextRef') or read_json(binding)
15
+ if client_factory is None:
16
+ from .topolo import Client
17
+ client_factory=lambda:Client('production','org:org_topolo_platform')
18
+ if set(ref)=={'taskId','artifact'}:
19
+ require(ref['taskId']==task['id'],'Cloud task context identity differs')
20
+ from .topolo import read_artifact
21
+ artifact=ref['artifact']
22
+ with tempfile.TemporaryDirectory(prefix='topolo-one-task-context-') as tmp:
23
+ path=pathlib.Path(tmp)/'task.json'
24
+ read_artifact(client_factory(),{'input':{'bytesScope':artifact['scope'],'bytesWorkspaceId':artifact['workspaceId']}},artifact['key'],artifact,path)
25
+ packet=json.loads(path.read_text())
26
+ require(set(packet)=={'format','taskId','markdown'} and packet['format']=='topolo-task-context-v1' and packet['taskId']==task['id'] and isinstance(packet['markdown'],str) and len(packet['markdown'])<=100000,'Unqualified individual task context')
27
+ target=cwd/'context';target.mkdir(exist_ok=True,mode=0o700)
28
+ (target/'continuity.md').write_text(packet['markdown'])
29
+ atomic_json(target/'continuity-source.json',ref)
30
+ return
31
+ require(set(ref)=={'taskId','workspaceId','documentId','versionId','bodySHA256'},'Invalid pinned task context')
32
+ require(ref['taskId']==task['id'],'Cloud task context identity differs')
33
+ result=client_factory().action('app_topolo_docs.documents.get',{'documentId':ref['documentId']},ref['workspaceId'])
34
+ doc=result['document'];versions=[v for v in result['versions'] if v['id']==ref['versionId']]
35
+ require(doc['id']==ref['documentId'] and doc['workspaceId']==ref['workspaceId'] and doc['organizationId']=='org_topolo_platform' and len(versions)==1,'Pinned cloud context is unavailable in the owning organization')
36
+ body=versions[0]['bodyMarkdown'];require(hashlib.sha256(body.encode()).hexdigest()==ref['bodySHA256'],'Pinned cloud task context changed')
37
+ start='<!-- topolo-task-context:'+task['id']+' -->';end='<!-- /topolo-task-context -->'
38
+ require(body.count(start)==1,'Cloud document has no unique context for this task')
39
+ section=body.split(start,1)[1];require(end in section,'Cloud task context section is incomplete');section=section.split(end,1)[0]
40
+ target=cwd/'context';target.mkdir(exist_ok=True,mode=0o700)
41
+ (target/'continuity.md').write_text('# Verified immutable Topolo Docs task context\n\nSource: '+ref['documentId']+' / '+ref['versionId']+'\n\n'+section)
42
+ atomic_json(target/'continuity-source.json',ref)
43
+
44
+ def ensure(root,task,client=None):
45
+ """Publish one new request packet before native work; never replay an uncertain upload."""
46
+ root=pathlib.Path(root);binding=root/'context-refs'/(task['id']+'.json')
47
+ if task.get('contextRef'):return task['contextRef']
48
+ if binding.exists():return read_json(binding)
49
+ authority=read_json(root/'context-authority.json',{})
50
+ require(set(authority)=={'format','organizationId','workspaceId','scope'} and authority['format']=='topolo-task-context-authority-v1' and authority['organizationId']=='org_topolo_platform' and authority['scope']=='org' and isinstance(authority['workspaceId'],str) and authority['workspaceId'],'Task cloud context authority is missing; no local-context fallback')
51
+ if client is None:
52
+ from .topolo import Client
53
+ client=Client('production','org:org_topolo_platform')
54
+ from .topolo import read_artifact
55
+ directory=root/'context-uploads';directory.mkdir(exist_ok=True,mode=0o700)
56
+ receipt=directory/(task['id']+'.receipt.json');file=directory/(task['id']+'.context.json')
57
+ packet={'format':'topolo-task-context-v1','taskId':task['id'],'markdown':'# Current human request\n\nTask: '+task['id']+'\nChannel: '+task['channel']+'\n\n'+task['objective']+'\n\n## Custody and evidence\nThis packet records the human request, not proof that existing work is unowned, resumed, released or complete. Discover only the relevant canonical Topolo Docs/Bytes references before execution. Preserve existing task/claim/intent identities, owners, pauses and UNKNOWN effects. Report missing context or authority without duplicating work. Current task phase and subsequent human responses are in TASK.json. No task history or portfolio export is required for this packet.\n'}
58
+ source=json.dumps(packet,sort_keys=True,separators=(',',':')).encode();digest=hashlib.sha256(source).hexdigest()
59
+ prior=read_json(receipt,{})
60
+ if prior:
61
+ require(prior.get('sourceSHA256')==digest,'Task context input changed; reconcile the existing upload')
62
+ require(prior.get('state') in {'uploaded','verified'},'Task context upload is uncertain; reconcile its receipt before retrying')
63
+ key=prior['key']
64
+ else:
65
+ require(not file.exists(),'Task context source exists without a receipt; reconcile before uploading')
66
+ file.write_bytes(source);file.chmod(0o600)
67
+ atomic_json(receipt,{'state':'prepared','taskId':task['id'],'sourceSHA256':digest})
68
+ result=client.action('app_topolo_bytes.files.upload_url.create',{'key':'task-context/'+file.name,'type':'application/json','size':len(source),'scope':authority['scope']},authority['workspaceId'],write=True,upload=file)
69
+ key=result.get('key') or result.get('session',{}).get('key')
70
+ require(isinstance(key,str) and key,'Task context upload returned no persisted key; reconcile before retrying')
71
+ atomic_json(receipt,{'state':'uploaded','taskId':task['id'],'sourceSHA256':digest,'key':key})
72
+ artifact=read_artifact(client,{'input':{'bytesScope':authority['scope'],'bytesWorkspaceId':authority['workspaceId']}},key)
73
+ require(artifact['sha256']==digest,'Persisted task context differs; no native work started')
74
+ ref={'taskId':task['id'],'artifact':artifact}
75
+ atomic_json(receipt,{'state':'verified','taskId':task['id'],'sourceSHA256':digest,'key':key,'artifact':artifact})
76
+ atomic_json(binding,ref)
77
+ return ref
@@ -0,0 +1,51 @@
1
+ """Closed loopback GUI adapter to the token-authenticated owning connection."""
2
+ import json
3
+ import socket
4
+ from .storage import Library,canonical,read_json,PackError
5
+ from . import live
6
+ from .work import Store
7
+
8
+ class Client:
9
+ def __init__(self,root):
10
+ self.lib=Library(root)
11
+ def snapshot(self):
12
+ try:
13
+ result=live.request(self.lib,'work-list',{})
14
+ if 'events' not in result:result['events']=[event for task in result['tasks'] for event in live.request(self.lib,'work-read',{'taskId':task['id']})['events']]
15
+ result['coordinatorObservations']=self.observe_coordinators()
16
+ if read_json(self.lib.root/'work/config.json',{}).get('frontdoorDispatch') is not True:
17
+ states=read_json(self.lib.root/'work/coordinator-observations.json',{}).get('states',{})
18
+ active=sorted({event['owner'] for event in result['coordinatorObservations'] if states.get(event['threadId'])=='active'})
19
+ if active:result['activityNotice']=', '.join(active)+' is executing outside the task ledger. Recorded milestones update here; continuous specialist progress awaits the pending backend activation.'
20
+ status=live.request(self.lib,'status',{})
21
+ if status.get('workStartupError'):result['activityNotice']='Automatic task resumption is blocked: '+status['workStartupError']
22
+ result['connection']='live';return result
23
+ except (PackError,OSError):
24
+ from .cloud_work import open_store
25
+ store=open_store(self.lib.root)
26
+ result=store.snapshot() if getattr(store,'cloud',False) else {'tasks':store.list(),'workers':store.workers(),'events':store.events(),'retainedCount':len(store.retained())}
27
+ return {**result,'connection':'offline','notice':'Native execution is disconnected. Recorded checkpoints are shown; no task is being retried.'}
28
+ def observe_coordinators(self):
29
+ from .coordinator_activity import record
30
+ status=live.request(self.lib,'status',{})
31
+ if status.get('activityError'):
32
+ raise PackError('Native activity recording failed: '+status['activityError'])
33
+ return record(self.lib.root,status)
34
+ def submit(self,request):
35
+ return live.request(self.lib,'work-submit',request)
36
+ def respond(self,request):
37
+ return live.request(self.lib,'work-respond',request)
38
+ def read(self,task_id):
39
+ return live.request(self.lib,'work-read',{'taskId':task_id})
40
+ def events(self):
41
+ record=read_json(self.lib.root/'live-bridge.json',{})
42
+ if not record:
43
+ raise PackError('Native event connection unavailable')
44
+ with socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) as connection:
45
+ connection.settimeout(3);connection.connect(record['socket']);connection.sendall(canonical({'token':record['token'],'operation':'work-watch','params':{}})+b'\n');connection.settimeout(None)
46
+ with connection.makefile('rb') as stream:
47
+ for line in stream:
48
+ value=json.loads(line)
49
+ if 'error' in value:
50
+ raise PackError(value['error'])
51
+ yield value
@@ -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())