@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.
Files changed (148) hide show
  1. package/README.md +18 -4
  2. package/agent-packs/README.md +469 -1
  3. package/agent-packs/coverage_changed.py +1 -1
  4. package/agent-packs/docs/selected-artifacts.md +47 -0
  5. package/agent-packs/documentation/cloud-task-state.md +56 -0
  6. package/agent-packs/domain-contexts/README.md +25 -0
  7. package/agent-packs/domain-contexts/cicd.json +8 -0
  8. package/agent-packs/gui/feed-adapter.js +4 -0
  9. package/agent-packs/gui/index.html +9 -0
  10. package/agent-packs/gui/studio.css +14 -0
  11. package/agent-packs/gui/studio.js +121 -0
  12. package/agent-packs/gui/studio_server.py +355 -0
  13. package/agent-packs/roles/cicd/CLAUDE.md +24 -0
  14. package/agent-packs/specializations/cicd--cicd.json +5 -0
  15. package/agent-packs/specializations/cicd--pipeline-engineering.json +5 -0
  16. package/agent-packs/specializations/cicd--promotion-deployment.json +5 -0
  17. package/agent-packs/specializations/cicd--queue-recovery.json +5 -0
  18. package/agent-packs/specializations/cicd--release-verification.json +5 -0
  19. package/agent-packs/specializations/cicd--runner-capacity.json +5 -0
  20. package/agent-packs/specializations/intakes--engineering-02.json +5 -0
  21. package/agent-packs/specializations/intakes--engineering-03.json +5 -0
  22. package/agent-packs/specializations/intakes--engineering.json +5 -0
  23. package/agent-packs/specializations/intakes--operator-01.json +5 -0
  24. package/agent-packs/specializations/intakes--operator-02.json +5 -0
  25. package/agent-packs/specializations/operator-skills--audio-and-narration-specialist.json +5 -0
  26. package/agent-packs/specializations/operator-skills--brand-and-copywriting-specialist.json +5 -0
  27. package/agent-packs/specializations/operator-skills--capability-research-specialist.json +5 -0
  28. package/agent-packs/specializations/operator-skills--data-analysis-and-visualization.json +5 -0
  29. package/agent-packs/specializations/operator-skills--financial-evidence-specialist.json +5 -0
  30. package/agent-packs/specializations/operator-skills--operating-template-specialist.json +5 -0
  31. package/agent-packs/specializations/operator-skills--operator-skill-library.json +5 -0
  32. package/agent-packs/specializations/operator-skills--operator-skills.json +5 -0
  33. package/agent-packs/specializations/operator-skills--operator-workflow-evaluation.json +5 -0
  34. package/agent-packs/specializations/operator-skills--privacy-and-agreement-review.json +5 -0
  35. package/agent-packs/specializations/operator-skills--storytelling-specialist.json +5 -0
  36. package/agent-packs/specializations/operator-skills--ux-and-accessibility-specialist.json +5 -0
  37. package/agent-packs/specializations/operator-skills--video-production-specialist.json +5 -0
  38. package/agent-packs/specializations/operator-skills--visual-design-specialist.json +5 -0
  39. package/agent-packs/specializations/operator-skills--workflow-integration-specialist.json +5 -0
  40. package/agent-packs/specializations/operators--access-and-privacy.json +5 -0
  41. package/agent-packs/specializations/operators--accounting-evidence-review.json +5 -0
  42. package/agent-packs/specializations/operators--agreement-operations.json +5 -0
  43. package/agent-packs/specializations/operators--books-operator.json +5 -0
  44. package/agent-packs/specializations/operators--brand-operator.json +5 -0
  45. package/agent-packs/specializations/operators--commerce-operations.json +5 -0
  46. package/agent-packs/specializations/operators--communications-operator.json +5 -0
  47. package/agent-packs/specializations/operators--content-and-assets-operator.json +5 -0
  48. package/agent-packs/specializations/operators--customer-operations.json +5 -0
  49. package/agent-packs/specializations/operators--design-operator.json +5 -0
  50. package/agent-packs/specializations/operators--design-production-02.json +5 -0
  51. package/agent-packs/specializations/operators--design-production-03.json +5 -0
  52. package/agent-packs/specializations/operators--design-production-04.json +5 -0
  53. package/agent-packs/specializations/operators--device-and-feed-operator.json +5 -0
  54. package/agent-packs/specializations/operators--director-operator.json +5 -0
  55. package/agent-packs/specializations/operators--experience-review.json +5 -0
  56. package/agent-packs/specializations/operators--growth-operations.json +5 -0
  57. package/agent-packs/specializations/operators--learn-operator.json +5 -0
  58. package/agent-packs/specializations/operators--localization-operator.json +5 -0
  59. package/agent-packs/specializations/operators--operators.json +5 -0
  60. package/agent-packs/specializations/operators--pay-operator.json +5 -0
  61. package/agent-packs/specializations/operators--research-and-intelligence.json +5 -0
  62. package/agent-packs/specializations/operators--roadmap-operator.json +5 -0
  63. package/agent-packs/specializations/operators--social-content-operator.json +5 -0
  64. package/agent-packs/specializations/operators--talent-operations.json +5 -0
  65. package/agent-packs/specializations/operators--web-operator.json +5 -0
  66. package/agent-packs/specializations/operators--workflow-operator.json +5 -0
  67. package/agent-packs/specializations/specialists--adversarial-qa.json +5 -0
  68. package/agent-packs/specializations/specialists--backlog-dispatch.json +5 -0
  69. package/agent-packs/specializations/specialists--build-01.json +5 -0
  70. package/agent-packs/specializations/specialists--build-02.json +5 -0
  71. package/agent-packs/specializations/specialists--build-03.json +5 -0
  72. package/agent-packs/specializations/specialists--developer-environment-steward.json +5 -0
  73. package/agent-packs/specializations/specialists--documentation-knowledge-steward.json +5 -0
  74. package/agent-packs/specializations/specialists--execution-readiness.json +5 -0
  75. package/agent-packs/specializations/specialists--incident-triage.json +5 -0
  76. package/agent-packs/specializations/specialists--platform-package-steward.json +5 -0
  77. package/agent-packs/specializations/specialists--product-direction-01.json +5 -0
  78. package/agent-packs/specializations/specialists--product-direction-02.json +5 -0
  79. package/agent-packs/specializations/specialists--release-recovery.json +5 -0
  80. package/agent-packs/specializations/specialists--release-watch.json +5 -0
  81. package/agent-packs/specializations/specialists--roadmap-planning-01.json +5 -0
  82. package/agent-packs/specializations/specialists--roadmap-planning-02.json +5 -0
  83. package/agent-packs/specializations/specialists--security-permissions-review.json +5 -0
  84. package/agent-packs/specializations/specialists--system-architecture-01.json +5 -0
  85. package/agent-packs/specializations/specialists--system-architecture-02.json +5 -0
  86. package/agent-packs/specializations/specialists--translation.json +5 -0
  87. package/agent-packs/specializations/specialists--ux-consistency-review.json +5 -0
  88. package/agent-packs/specializations/specialists--workflow-improvement.json +5 -0
  89. package/agent-packs/specializations/specialists--worktree-retirement.json +5 -0
  90. package/agent-packs/src/topolo_agent_pack/cicd.py +101 -0
  91. package/agent-packs/src/topolo_agent_pack/claude.py +22 -7
  92. package/agent-packs/src/topolo_agent_pack/claude_activation.py +157 -0
  93. package/agent-packs/src/topolo_agent_pack/claude_completion_recovery.py +216 -0
  94. package/agent-packs/src/topolo_agent_pack/claude_desktop.py +181 -0
  95. package/agent-packs/src/topolo_agent_pack/claude_dispatch.py +240 -0
  96. package/agent-packs/src/topolo_agent_pack/claude_prompt.py +74 -0
  97. package/agent-packs/src/topolo_agent_pack/claude_runner.py +207 -0
  98. package/agent-packs/src/topolo_agent_pack/claude_service.py +397 -0
  99. package/agent-packs/src/topolo_agent_pack/cli.py +188 -18
  100. package/agent-packs/src/topolo_agent_pack/closed_ledger_fence.py +118 -0
  101. package/agent-packs/src/topolo_agent_pack/cloud_attach.py +128 -0
  102. package/agent-packs/src/topolo_agent_pack/cloud_cutover.py +134 -0
  103. package/agent-packs/src/topolo_agent_pack/cloud_delta.py +162 -0
  104. package/agent-packs/src/topolo_agent_pack/cloud_migrate.py +142 -0
  105. package/agent-packs/src/topolo_agent_pack/cloud_recovery.py +42 -0
  106. package/agent-packs/src/topolo_agent_pack/cloud_work.py +126 -0
  107. package/agent-packs/src/topolo_agent_pack/coordinator_activity.py +27 -0
  108. package/agent-packs/src/topolo_agent_pack/coordinator_context.py +91 -0
  109. package/agent-packs/src/topolo_agent_pack/coordinator_refresh.py +116 -0
  110. package/agent-packs/src/topolo_agent_pack/desktop_acceptance.py +45 -0
  111. package/agent-packs/src/topolo_agent_pack/desktop_launcher.py +114 -0
  112. package/agent-packs/src/topolo_agent_pack/domain_context.py +38 -0
  113. package/agent-packs/src/topolo_agent_pack/execution.py +104 -7
  114. package/agent-packs/src/topolo_agent_pack/fresh.py +1 -1
  115. package/agent-packs/src/topolo_agent_pack/frontdoor.py +23 -0
  116. package/agent-packs/src/topolo_agent_pack/linked_task_context.py +80 -0
  117. package/agent-packs/src/topolo_agent_pack/live.py +285 -18
  118. package/agent-packs/src/topolo_agent_pack/native.py +160 -4
  119. package/agent-packs/src/topolo_agent_pack/policy.py +8 -0
  120. package/agent-packs/src/topolo_agent_pack/review_context.py +33 -0
  121. package/agent-packs/src/topolo_agent_pack/role_catalog.py +137 -0
  122. package/agent-packs/src/topolo_agent_pack/role_context.py +63 -0
  123. package/agent-packs/src/topolo_agent_pack/role_refresh.py +50 -0
  124. package/agent-packs/src/topolo_agent_pack/task_models.py +11 -7
  125. package/agent-packs/src/topolo_agent_pack/throughput.py +149 -0
  126. package/agent-packs/src/topolo_agent_pack/topolo.py +7 -1
  127. package/agent-packs/src/topolo_agent_pack/work.py +322 -0
  128. package/agent-packs/src/topolo_agent_pack/work_adoption.py +122 -0
  129. package/agent-packs/src/topolo_agent_pack/work_artifacts.py +84 -0
  130. package/agent-packs/src/topolo_agent_pack/work_context.py +77 -0
  131. package/agent-packs/src/topolo_agent_pack/work_gui.py +51 -0
  132. package/agent-packs/src/topolo_agent_pack/work_native.py +414 -0
  133. package/agent-packs/src/topolo_agent_pack/work_wait.py +151 -0
  134. package/agent-packs/templates/topolo-coordinators/AGENTS.md +7 -0
  135. package/agent-packs/templates/topolo-coordinators/agents/intakes/cicd/AGENTS.md +6 -0
  136. package/agent-packs/templates/topolo-coordinators/agents/intakes/cicd/config.json +4 -0
  137. package/agent-packs/templates/topolo-coordinators/agents/intakes/engineering/AGENTS.md +3 -0
  138. package/agent-packs/templates/topolo-coordinators/agents/intakes/engineering/config.json +4 -0
  139. package/agent-packs/templates/topolo-coordinators/agents/intakes/operator-01/AGENTS.md +3 -0
  140. package/agent-packs/templates/topolo-coordinators/agents/intakes/operator-01/config.json +4 -0
  141. package/agent-packs/templates/topolo-coordinators/pack.json +56 -0
  142. package/dist/chunk-NYDIAYVY.js +144 -0
  143. package/dist/{claude-pack-backend-YEVPAOID.js → chunk-UY5LEU5C.js} +1659 -21
  144. package/dist/claude-desktop-profile-JCQKSY3K.js +13 -0
  145. package/dist/claude-pack-activation-Q25MCEPU.js +506 -0
  146. package/dist/claude-pack-backend-HUTKGIDS.js +90 -0
  147. package/dist/index.js +408 -164
  148. 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.
@@ -0,0 +1,4 @@
1
+ {
2
+ "model": "gpt-6.1-sol",
3
+ "reasoningEffort": "low"
4
+ }
@@ -0,0 +1,3 @@
1
+ # Topolo Engineering
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.
@@ -0,0 +1,4 @@
1
+ {
2
+ "model": "gpt-6.1-sol",
3
+ "reasoningEffort": "low"
4
+ }