@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,181 @@
1
+ """Task workers in the already authenticated Claude Desktop process.
2
+
3
+ The closed native endpoint survives closing the temporary setup inspector.
4
+ It exposes task lifecycle operations only, never vendor credentials or eval.
5
+ """
6
+ import asyncio
7
+ import hashlib
8
+ import json
9
+ import os
10
+ import pathlib
11
+ import shlex
12
+ import socket
13
+ import stat
14
+ import sys
15
+
16
+ from .claude_runner import public_progress
17
+ from .storage import read_json, canonical, atomic_json
18
+ from .work import require
19
+
20
+
21
+ def call(connection, operation):
22
+ info = pathlib.Path(connection['secretPath'])
23
+ metadata = info.lstat()
24
+ require(stat.S_ISREG(metadata.st_mode) and metadata.st_uid == os.getuid()
25
+ and metadata.st_mode & 0o077 == 0, 'Unowned native task connection secret')
26
+ target = pathlib.Path(connection['socket']).lstat()
27
+ require(stat.S_ISSOCK(target.st_mode) and target.st_uid == os.getuid(), 'Unowned native task socket')
28
+ secret = read_json(info)
29
+ payload = {'pid': connection['pid'], 'identity': connection['identity'],
30
+ 'token': secret['token'], 'operation': operation}
31
+ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as channel:
32
+ channel.settimeout(65)
33
+ channel.connect(connection['socket'])
34
+ channel.sendall(canonical(payload) + b'\n')
35
+ with channel.makefile('rb') as stream:
36
+ raw = stream.readline(1048577)
37
+ require(raw.endswith(b'\n') and len(raw) <= 1048576, 'Native task response is missing or exceeds its boundary')
38
+ result = json.loads(raw)
39
+ require(set(result) == {'result'}, 'Native task operation refused: ' + str(result.get('error', 'invalid response')))
40
+ return result['result']
41
+
42
+
43
+ def authorize(root, operation):
44
+ """Independent cloud read before a native creation, send or archive."""
45
+ from .cloud_work import CloudStore
46
+ from .work_artifacts import read_checkpoint
47
+ plan = operation['plan']
48
+ store = CloudStore(root)
49
+ worker = store.worker(plan['workerId'])
50
+ task = store.get(worker['taskId'])
51
+ require(task.get('ownerInstallationId') == store.binding['installationId']
52
+ and worker.get('nativePlan') == plan and worker['taskId'] == plan['taskId']
53
+ and worker['nativeId'] == plan['nativeId'], 'Native worker cloud ownership differs')
54
+ if operation['kind'] == 'create':
55
+ require(set(operation) == {'kind', 'plan', 'files'} and worker['state'] == 'creating'
56
+ and hashlib.sha256(canonical(operation['files'])).hexdigest() == plan['contextDigest'],
57
+ 'Native creation has no exact cloud reservation')
58
+ elif operation['kind'] == 'send':
59
+ require(set(operation) == {'kind', 'plan', 'workerId', 'assignment'}
60
+ and worker['state'] == 'running' and worker['turnId'] == operation['assignment']['messageId']
61
+ and worker['resolution']['nativeACK'] == {'model': plan['model'], 'effort': plan['effort']},
62
+ 'Native assignment has no acknowledged cloud delivery reservation')
63
+ else:
64
+ require(operation['kind'] == 'archive' and
65
+ set(operation) == {'kind', 'plan', 'workerId', 'messageId', 'terminal'}
66
+ and worker['state'] == 'archiving', 'Native archival has no checkpointed worker')
67
+ payload = read_checkpoint(store.root, worker['checkpoint'], task['id'], worker['id'])
68
+ receipt = payload['nativeReceipt']
69
+ terminal = operation['terminal']
70
+ require(receipt['status'] == 'completed' and receipt['threadId'] == plan['nativeId']
71
+ and receipt['turnId'] == operation['messageId'] == terminal['messageId']
72
+ and receipt['producer']['resultId'] == terminal['resultUuid'],
73
+ 'Native archival differs from the verified cloud checkpoint')
74
+ return {'authorized': True}
75
+
76
+
77
+ def main():
78
+ try:
79
+ raw = sys.stdin.read(262145)
80
+ require(len(raw) <= 262144 and len(sys.argv) == 2, 'Bounded native authority request required')
81
+ print(json.dumps(authorize(sys.argv[1], json.loads(raw))))
82
+ return 0
83
+ except Exception as exc:
84
+ print('Native cloud authority refused: ' + str(exc), file=sys.stderr)
85
+ return 2
86
+
87
+
88
+ class Session:
89
+ def __init__(self, cwd, worker_id, model, effort, progress=None):
90
+ self.cwd = pathlib.Path(cwd)
91
+ self.id = worker_id
92
+ self.model = model
93
+ self.effort = effort
94
+ self.progress = progress
95
+ self.plan = None
96
+ self.connection = None
97
+ self.results = {}
98
+ self.pending = None
99
+ self.assigned = False
100
+ self.closed = False
101
+ self.archived = False
102
+ self.allow_tools = True
103
+ self.directory = self.cwd / '.topolo-worker'
104
+ self.directory.mkdir(mode=0o700)
105
+ atomic_json(self.directory / 'gate.json', {'sessionId': None, 'model': model,
106
+ 'effort': effort, 'phase': 'readiness'})
107
+ adapter = pathlib.Path(__file__).resolve().parents[2] / 'topolo-agent-pack'
108
+ command = shlex.join([sys.executable, str(adapter), 'work', 'claude-worker-hook',
109
+ '--harness', 'claude', '--input', str(self.directory)]) + ' || exit 2'
110
+ atomic_json(self.cwd / '.claude/settings.json', {'autoMemoryEnabled': False,
111
+ 'hooks': {'PreToolUse': [{'hooks': [{'type': 'command', 'command': command, 'timeout': 15}]}]}})
112
+
113
+ async def operation(self, value):
114
+ require(self.connection is not None, 'The signed-in Desktop task connection is unavailable')
115
+ return await asyncio.to_thread(call, self.connection, value)
116
+
117
+ async def start(self):
118
+ require(self.plan is not None, 'Exact cloud-reserved native plan required')
119
+ files = {str(p): hashlib.sha256(p.read_bytes()).hexdigest()
120
+ for p in sorted(self.cwd.rglob('*')) if p.is_file() and not p.is_relative_to(self.directory)}
121
+ require(hashlib.sha256(canonical(files)).hexdigest() == self.plan['contextDigest'],
122
+ 'Selected native context changed before creation')
123
+ await self.operation({'kind': 'create', 'plan': self.plan, 'files': files})
124
+ result = await self.wait(self.id)
125
+ require(result['output'].strip() == 'TOPOLO_WORKER_READY:' + self.id and result['toolItems'] == 0,
126
+ 'Exact tool-free native readiness did not complete')
127
+ self.sdk_session = result['nativeSessionId']
128
+ return {'sessionId': self.plan['nativeId'], 'processId': self.connection['pid'],
129
+ 'model': self.model, 'effort': self.effort, 'boundary': 'signed-in Desktop native terminal producer'}
130
+
131
+ async def assign(self, message_id, prompt):
132
+ require(not self.assigned and not self.closed and self.pending is None,
133
+ 'This native worker already has an assignment; reconcile without resend')
134
+ self.assigned = True
135
+ self.pending = message_id
136
+ atomic_json(self.directory / 'gate.json', {'sessionId': self.sdk_session, 'model': self.model,
137
+ 'effort': self.effort, 'phase': 'assigned', 'allowTools': self.allow_tools})
138
+ await self.operation({'kind': 'send', 'workerId': self.id,
139
+ 'assignment': {'messageId': message_id, 'prompt': prompt}})
140
+ return await self.wait(message_id)
141
+
142
+ async def wait(self, message_id):
143
+ seen = set()
144
+ async with asyncio.timeout(1800):
145
+ while True:
146
+ result = await self.operation({'kind': 'read', 'workerId': self.id, 'messageId': message_id})
147
+ if self.assigned and self.progress:
148
+ for row in result['messages']:
149
+ if row['id'] not in seen and public_progress(row['text']):
150
+ await self.progress(row['id'], row['text'][:1600])
151
+ seen.add(row['id'])
152
+ if result['state'] == 'native-task-completed':
153
+ require(result['nativeId'] == self.plan['nativeId'] and result['messageId'] == message_id
154
+ and result['model'] == self.model and result['effort'] == self.effort,
155
+ 'Native terminal identity or settings differ')
156
+ receipt = {'id': message_id, 'status': 'completed', 'output': result['result'],
157
+ 'toolItems': result['toolItems'], 'nativeSessionId': result['cliSessionId'],
158
+ 'nativeResultId': result['resultUuid']}
159
+ self.results[message_id] = receipt
160
+ self.pending = None
161
+ return receipt
162
+ require(result['state'] == 'native-task-pending', 'Native task state is unqualified')
163
+ await asyncio.sleep(1)
164
+
165
+ async def archive(self, message_id):
166
+ result = await self.operation({'kind': 'archive', 'workerId': self.id, 'messageId': message_id})
167
+ require(result['state'] == 'native-worker-archived' and result['nativeId'] == self.plan['nativeId'],
168
+ 'Native Desktop worker archival was not acknowledged')
169
+ self.closed = True
170
+ self.archived = True
171
+ return {'kind': 'native-desktop-session', 'desktopChatArchived': True, **result}
172
+
173
+ async def close(self):
174
+ # An uncertain task stays visible and owned. Closing an adapter is never
175
+ # represented as archival, and never cancels or deletes native work.
176
+ self.closed = True
177
+ return {'state': 'native-session-preserved', 'desktopChatArchived': self.archived}
178
+
179
+
180
+ if __name__ == '__main__':
181
+ sys.exit(main())
@@ -0,0 +1,240 @@
1
+ """Claude subscription workers behind Desktop's three permanent front doors.
2
+
3
+ Uses the same cloud state, checkpoints, review phases and Activity events as the
4
+ Codex broker. It does not launch a competing local task ledger or import history.
5
+ """
6
+ import asyncio
7
+ import hashlib
8
+ import copy
9
+ import pathlib
10
+ import sys
11
+ import types
12
+ import uuid
13
+
14
+ from . import task_models
15
+ from .claude_desktop import Session
16
+ from .cicd import RELEASE_RISK_TYPES
17
+ from .role_catalog import digest
18
+ from .storage import atomic_json, read_json, component_key, canonical
19
+ from .work import require
20
+ from .work_native import Broker
21
+
22
+
23
+ class ClaudeBroker(Broker):
24
+ def __init__(self, lib, session_factory=Session):
25
+ owner = types.SimpleNamespace(lib=lib, runtime=str(lib.root / 'claude/task-runtime'),
26
+ execution=types.SimpleNamespace(roles={}, verified=set()))
27
+ super().__init__(owner)
28
+ require(getattr(self.store, 'cloud', False), 'Claude dispatch requires cloud authority; no local fallback')
29
+ self.factory = session_factory
30
+ self.sessions = {}
31
+
32
+ async def initialize(self):
33
+ await self.state_call('snapshot')
34
+ self.config()
35
+ for channel in ['engineering', 'operations', 'cicd']:
36
+ config, _, _ = self.config(channel)
37
+ await asyncio.to_thread(self.qualify, config)
38
+ workers = await self.state_call('workers')
39
+ for task in await self.state_call('list'):
40
+ if task.get('ownerInstallationId') == self.store.binding['installationId'] or task.get('resumeAfterCheckpoint') is not True:
41
+ continue
42
+ if task['state'] not in {'queued', 'executing', 'reviewing'}:
43
+ continue
44
+ owned = [w for w in workers if w['taskId'] == task['id']]
45
+ phase = {'queued': 'route', 'executing': 'execute', 'reviewing': 'review'}[task['state']]
46
+ if any(w['phase'] == phase and w.get('attempt', 1) == task.get('attempt', 1)
47
+ and (phase != 'review' or w.get('reviewGeneration', 0) == task.get('reviewGeneration', 0)) for w in owned):
48
+ continue
49
+ if not all(w['state'] == 'archived' and w.get('nativeArchived') is True or
50
+ w['state'] == 'failed' and not w.get('turnId') and
51
+ (not w.get('nativeId') or w.get('nativeArchived') is True) for w in owned):
52
+ continue
53
+ await self.state_call('adopt_ready', task['id'], task['revision'])
54
+
55
+ def config(self, channel='engineering'):
56
+ root = self.store.root
57
+ saved = read_json(root / 'config.json')
58
+ catalog = read_json(root / 'role-catalog.json')
59
+ require(saved.get('format') == 'topolo-task-dispatch-v2'
60
+ and saved.get('requireCloudAuthority') is True and saved.get('frontdoorDispatch') is True,
61
+ 'Claude requires the source-independent cloud dispatcher')
62
+ require(catalog.get('format') == 'topolo-task-role-catalog-v1'
63
+ and catalog.get('pack') == saved['pack'] and digest(catalog) == saved['catalogDigest'],
64
+ 'Reviewed specialization catalog changed')
65
+ native, revision = self.owner.lib.load(saved['pack'])
66
+ require(revision == catalog['nativePackRevision'] and len(native['agents']) == 3,
67
+ 'The three-coordinator installation changed')
68
+ require(channel in catalog['channels'] and type(saved['maxWorkers']) is int
69
+ and 1 <= saved['maxWorkers'] <= 8, 'Unknown channel or invalid worker capacity')
70
+ registry = read_json(root / 'role-bindings.json')
71
+ require(registry.get('organizationId') == 'org_topolo_platform'
72
+ and registry.get('roles') == catalog['bindings'], 'Agent definition references differ')
73
+ selected = catalog['channels'][channel]
74
+ manifest = {'packId': saved['pack'], 'sourceIndependent': True,
75
+ 'agents': [r for r in catalog['roles'] if component_key(r) in selected['roleKeys']]}
76
+ return {**selected, 'pack': saved['pack'], 'catalogOnly': True,
77
+ 'maxWorkers': saved['maxWorkers']}, manifest, {
78
+ 'configuration': catalog['configuration'], 'constraints': catalog['constraints']}
79
+
80
+ def qualify(self, config):
81
+ from .permissions import load
82
+ from .topolo import Client, organization_execution_policy
83
+ native, _ = self.owner.lib.load(config['pack'])
84
+ consent = load(self.owner.lib, config['pack'], native['agents'])
85
+ require(consent is not None and consent['profile'] == 'full-access',
86
+ 'Explicit organization-bound full-access installation consent is required')
87
+ key = config['pack'] + '/intakes/' + config['intakeRole']
88
+ require(consent.get('overrides', {}).get(key, consent['profile']) == 'full-access',
89
+ 'Selected channel does not have full-access installation consent')
90
+ client = Client('production', 'org:org_topolo_platform')
91
+ policy, proof = organization_execution_policy(client, consent, native['agents'],
92
+ consent.get('agentWorkspaceId'))
93
+ require(policy.get('scopePolicyMode') == 'unrestricted' and policy.get('fullAccessEnabled') is True
94
+ and proof['profileCeilingVerified'],
95
+ 'This worker cannot enforce an organization tool/action allowlist; execution remains blocked')
96
+ self.tool_path = client.env['PATH']
97
+
98
+ async def start(self, task, phase, config, manifest, models):
99
+ await asyncio.to_thread(self.qualify, config)
100
+ if phase == 'route':
101
+ role_id = config['intakeRole']
102
+ model_task = {'type': 'route-request', 'complexity': 'routine', 'risk': 'normal'}
103
+ elif phase == 'execute':
104
+ role_id, model_task = task['route']['role'], task['route']['task']
105
+ else:
106
+ role_id = config['reviewRole']
107
+ model_task = {**task['route']['task'], 'type': 'verify-task',
108
+ 'complexity': 'complex' if task['route']['task']['risk'] == 'security-critical'
109
+ or task['channel'] == 'cicd' and task['route']['task']['type'] in RELEASE_RISK_TYPES else 'standard'}
110
+ roles = [r for r in manifest['agents'] if r['id'] == role_id]
111
+ require(len(roles) == 1, 'Selected specialization is ambiguous')
112
+ key = component_key(roles[0])
113
+ constraints = task_models.role_constraints(models['constraints'], key)
114
+ selected = task_models._selection(models['configuration'], model_task, 'claude', constraints)
115
+ require(task['channel'] != 'cicd' or model_task['type'] not in RELEASE_RISK_TYPES
116
+ or selected['rule']['profile'] == 'deep', 'Release-risk tasks require a deep profile')
117
+ effort = next(e for e in selected['preferences'] if e in selected['allowedEfforts'])
118
+ pending = {'harness': 'claude', 'resolved': {'model': selected['model'], 'effort': effort},
119
+ 'nativeACK': None, 'state': 'intent-only-awaiting-native-readiness'}
120
+ worker = await self.state_call('reserve', task['id'], phase, key, pending, config['maxWorkers'])
121
+ wid = worker['id']
122
+ cwd = pathlib.Path(self.owner.runtime) / 'task-workspaces' / wid
123
+ cwd.mkdir(parents=True, mode=0o700)
124
+ from .work_context import ensure, materialize
125
+ context = await asyncio.to_thread(ensure, self.store.root, task)
126
+ if task.get('contextRef') != context:
127
+ current = await self.state_call('get', task['id'])
128
+ task = await self.state_call('update', task['id'], current['revision'],
129
+ {'contextRef': context, 'summary': 'Selected cloud task context is verified.'}, 'task-context-bound')
130
+ from .role_context import materialize as materialize_role
131
+ role = await asyncio.to_thread(materialize_role, self.store.root, key, cwd, harness='claude')
132
+ atomic_json(cwd / 'TASK.json', task)
133
+ await asyncio.to_thread(materialize, self.store.root, task, cwd)
134
+ if phase in {'execute', 'review'}:
135
+ from .linked_task_context import materialize as materialize_linked
136
+ await materialize_linked(self.store, task, cwd, lambda tid: self.state_call('get', tid))
137
+ atomic_json(cwd / 'context/retained-read-input.json', {'cursor': 0, 'limit': 50})
138
+ atomic_json(cwd / 'context/dispatch-connection.json', {
139
+ 'format': 'topolo-dispatch-transport-v1',
140
+ 'cloudReference': {k: self.store.binding[k] for k in ['organizationId', 'workspaceId', 'documentId', 'datasetId']},
141
+ 'retainedReadCommand': [sys.executable, str(pathlib.Path(__file__).resolve().parents[2] / 'topolo-agent-pack'), '--store', str(self.owner.lib.root),
142
+ 'work', 'retained', '--harness', 'claude', '--input', str(cwd / 'context/retained-read-input.json')],
143
+ 'pageInput': {'cursor': 0, 'limit': 50},
144
+ 'boundary': 'Read relevant retained context in pages; preserve original owners. Do not load the whole state document, portfolio export or old chats.'})
145
+ if phase == 'review':
146
+ from .work_artifacts import materialize_result
147
+ await asyncio.to_thread(materialize_result, self.store.root, task, cwd)
148
+ from .review_context import materialize as materialize_review
149
+ await asyncio.to_thread(materialize_review, self.store, task, cwd)
150
+ async def progress(identity, text):
151
+ await self.state_call('progress', wid, identity, text)
152
+ await self.notify()
153
+ session = self.factory(cwd, wid, selected['model'], effort, progress if phase == 'execute' else None)
154
+ session.allow_tools = phase != 'route'
155
+ settings_path = cwd / '.claude/settings.json'
156
+ settings = read_json(settings_path, {})
157
+ settings['env'] = {'PATH': self.tool_path}
158
+ atomic_json(settings_path, settings)
159
+ files = {str(p): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(cwd.rglob('*'))
160
+ if p.is_file() and not p.is_relative_to(cwd / '.topolo-worker')}
161
+ native_plan = {'taskId': task['id'], 'workerId': wid, 'nativeId': 'local_' + wid, 'cwd': str(cwd),
162
+ 'title': (phase.title() + ' · ' + task['objective'])[:256],
163
+ 'model': selected['model'], 'effort': effort, 'permissionMode': 'bypassPermissions',
164
+ 'contextDigest': hashlib.sha256(canonical(files)).hexdigest()}
165
+ await self.state_call('worker_update', wid, {'state': 'creating', 'cwd': str(cwd),
166
+ 'nativeId': native_plan['nativeId'], 'roleSource': role, 'nativePlan': native_plan})
167
+ session.plan = native_plan
168
+ session.connection = read_json(self.store.root / 'claude-frontdoors.json')['nativeConnection']
169
+ self.sessions[wid] = session
170
+ try:
171
+ ack = await session.start()
172
+ catalog = {'harness': 'claude', 'source': 'native-account-model-ids',
173
+ 'connectionGeneration': str(ack['processId']) + ':' + ack['sessionId'],
174
+ 'models': [ack['model']]}
175
+ resolution = task_models.resolve_intent(models['configuration'], model_task, 'claude', catalog, constraints)
176
+ resolution = task_models.acknowledge(resolution, {'model': ack['model'], 'effort': ack['effort']},
177
+ configuration=models['configuration'], catalog=catalog, constraints=constraints)
178
+ message_id = str(uuid.uuid4())
179
+ # Cloud commit and readback precede stdin. An uncertain commit or
180
+ # delivery retains this exact worker/input; no replacement is sent.
181
+ await self.state_call('worker_update', wid, {'state': 'running', 'resolution': resolution, 'turnId': message_id})
182
+ current = await self.state_call('get', task['id'])
183
+ await self.state_call('update', task['id'], current['revision'],
184
+ {'state': {'route': 'routing', 'execute': 'executing', 'review': 'reviewing'}[phase],
185
+ 'phase': phase, 'workerIds': current['workerIds'] + [wid],
186
+ 'summary': 'A fresh Claude ' + phase + ' specialist is running.'}, 'native-resume-verified')
187
+ check = await self.state_call('worker', wid)
188
+ require(check['turnId'] == message_id and check['resolution'] == resolution,
189
+ 'Canonical delivery reservation was not acknowledged')
190
+ await self.notify()
191
+ self.launch(self.execute(worker, session, message_id, self.prompt(task, phase, manifest, cwd)))
192
+ except Exception:
193
+ # Task input is sent only by execute(), after this startup block.
194
+ await session.close()
195
+ await self.state_call('worker_update', wid, {'state': 'failed', 'nativeArchived': False,
196
+ 'nativeRetirement': {'kind': 'native-desktop-session', 'desktopChatArchived': False}})
197
+ raise
198
+
199
+ def prompt(self, task, phase, manifest, cwd):
200
+ prompt = super().prompt(task, phase, manifest, cwd).replace('Read AGENTS.md', 'Read CLAUDE.md')
201
+ if phase in {'execute', 'review'}:
202
+ prompt += '\nFor relevant existing-work inventory only, context/dispatch-connection.json supplies the current installation transport and paged read command. It grants no takeover or replay authority.'
203
+ if phase == 'review':
204
+ prompt += '\nRead context/dispatch-connection.json and context/workflow-evidence.json, including its evidenceContract, before declaring authority or lifecycle evidence missing. Save any read-only command output you cite in this reviewer workspace; never cite /tmp, /private/tmp or another worker directory. The transport supplies the pinned adapter command and bounded pageInput; run that read-only command to independently verify relevant retained evidence. A role such as queue-recovery is the selected specialization; status-read is its classified task type, not a separate specialist name. Workflow evidence records this attempt only. Inspect prior worker archival and native model acknowledgements there. The coordinator separately verifies this reviewer archives after its checkpoint and that Activity reaches the live GUI; never fabricate those post-review observations or perform work again.'
205
+ return prompt
206
+
207
+ async def execute(self, worker, session, message_id, prompt):
208
+ try:
209
+ result = await session.assign(message_id, prompt)
210
+ await self.finish(worker['id'], result)
211
+ saved = await self.state_call('worker', worker['id'])
212
+ if saved['state'] != 'archived':
213
+ await session.close()
214
+ except Exception as exc:
215
+ await self.state_call('worker_update', worker['id'], {'state': 'unknown', 'error': str(exc)})
216
+ task = await self.state_call('get', worker['taskId'])
217
+ await self.state_call('update', task['id'], task['revision'],
218
+ {'state': 'unknown', 'summary': 'Claude task outcome is unresolved; no retry was sent.'}, 'delivery-unknown')
219
+ await session.close()
220
+ await self.notify()
221
+
222
+ async def read_turn(self, worker, turn):
223
+ receipt = self.sessions[worker['id']].results.get(turn['id'])
224
+ require(receipt is not None and receipt == turn, 'Exact native Claude completion is unavailable')
225
+ return {'id': turn['id'], 'status': 'completed',
226
+ 'items': [{'type': 'agentMessage', 'text': turn['output']}] +
227
+ [{'type': 'toolUse'} for _ in range(turn['toolItems'])],
228
+ 'nativeProof': {'harness': 'claude-desktop', 'sessionId': receipt['nativeSessionId'],
229
+ 'resultId': receipt['nativeResultId']}}
230
+
231
+ async def archive_worker(self, worker):
232
+ session = self.sessions[worker['id']]
233
+ result = await session.archive(worker['turnId'])
234
+ require(session.closed and session.archived, 'Native Desktop worker remains unarchived')
235
+ return {'nativeRetirement': result}
236
+
237
+ def persist(self):
238
+ # The cloud store owns task/worker state. Never write a Codex execution
239
+ # policy for Claude or use a second local ledger as an execution fallback.
240
+ pass
@@ -0,0 +1,74 @@
1
+ """Supported native coordinator conversation and typed broker-tool boundary.
2
+
3
+ The native prompt_id is the deduplication identity. No transcript, chat export,
4
+ or locally generated substitute is consulted when it is missing.
5
+ """
6
+ import json
7
+ import pathlib
8
+ import sys
9
+ import uuid
10
+
11
+ from .storage import Library, PackError, read_json
12
+ from .work import require
13
+
14
+
15
+ def submit(root, event, send=None):
16
+ require(isinstance(event, dict), 'Native Claude hook input must be an object')
17
+ lib = Library(root)
18
+ binding = read_json(lib.root / 'work/claude-frontdoors.json')
19
+ require(binding.get('format') == 'topolo-claude-frontdoors-v1',
20
+ 'Claude front-door binding is not installed')
21
+ require(event.get('hook_event_name') in {'UserPromptSubmit', 'PreToolUse'},
22
+ 'Unsupported Claude front-door hook')
23
+ rows = [r for r in binding['roles'] if r['sessionId'] == event.get('session_id')
24
+ and r['cwd'] == event.get('cwd')]
25
+ require(len(rows) == 1, 'Native Claude session is not the bound front door')
26
+ role = rows[0]
27
+ if event['hook_event_name'] == 'PreToolUse':
28
+ from .coordinator_context import tool_allowed
29
+ try:allowed=tool_allowed(lib.root,role,event)
30
+ except (PackError,ValueError,TypeError,OSError):allowed=False
31
+ return {'hookSpecificOutput': {'hookEventName': 'PreToolUse',
32
+ 'permissionDecision': 'allow' if allowed else 'deny', 'permissionDecisionReason':
33
+ 'Use only the exact broker schemas in the current Topolo coordinator context. For continuation: read the existing task, then Write {requestId: dispatchRequestId, taskId, revision: latest task.revision, intent: continue/request_changes/hold, note} to context/coordinator-request.json and run the installed respond --input command. Do not use {taskId,message}, create a replacement task, or perform engineering effects.'}}
34
+ identity = event.get('prompt_id')
35
+ try:
36
+ valid = isinstance(identity, str) and str(uuid.UUID(identity)) == identity
37
+ except ValueError:
38
+ valid = False
39
+ require(valid, 'Native Claude prompt identity is unavailable; no task was submitted')
40
+ require(event.get('permission_mode') == 'bypassPermissions',
41
+ 'Claude front-door startup permissions differ from the installation')
42
+ require(isinstance(event.get('prompt'),str) and 1<=len(event['prompt'])<=16000,'Bounded native human input required')
43
+ if send is None:
44
+ from .live import request as live_request
45
+ send = lambda operation, params: live_request(lib, operation, params)
46
+ status = send('status', {})
47
+ require(status.get('harness') == 'claude' and status.get('cloudDispatchReady') is True
48
+ and status.get('desktopPid') == binding.get('desktopPid'),
49
+ 'Claude cloud dispatch is not active; the coordinator will not execute the request itself')
50
+ from .coordinator_context import prepare
51
+ selected=prepare(lib.root,role,event,send('work-list',{}))
52
+ return {'hookSpecificOutput': {'hookEventName': 'UserPromptSubmit',
53
+ 'additionalContext':'Topolo coordinator context (the original human prompt is unchanged; no task was submitted):\n'+json.dumps(selected,sort_keys=True,separators=(',',':'))}}
54
+
55
+
56
+
57
+ def main(root, stream=None):
58
+ try:
59
+ source = (stream or sys.stdin).read(100001)
60
+ require(len(source) <= 100000, 'Claude hook input exceeds its boundary')
61
+ event = json.loads(source)
62
+ if isinstance(event, dict) and event.get('hook_event_name') == 'UserPromptSubmit':
63
+ from .claude_service import ensure_running, refresh_desktop_owner
64
+ lib = Library(root)
65
+ refresh_desktop_owner(lib)
66
+ ensure_running(lib)
67
+ result = submit(pathlib.Path(root), event)
68
+ print(json.dumps(result))
69
+ return 0
70
+ except Exception as exc:
71
+ # Nonzero hook exit blocks direct execution on a failed handoff. Never
72
+ # print the incoming human prompt or a transcript in a diagnostic.
73
+ print('Topolo handoff unavailable: ' + str(exc), file=sys.stderr)
74
+ return 2