@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,101 @@
1
+ """Native Claude task-profile definitions, not persistent worker sessions."""
2
+ import argparse
3
+ import hashlib
4
+ import json
5
+ import pathlib
6
+ from .storage import PackError, atomic_json
7
+ from .task_models import default_configuration, _selection
8
+
9
+ DISCIPLINES = {
10
+ 'queue-recovery': 'Reconcile existing intents, dependencies, queue age, leases and uncertain effects. Preserve ownership. Never clear holds or replay operations without verified reconciliation.',
11
+ 'runner-capacity': 'Diagnose runner eligibility, architecture, admission, locking, utilisation and accepted job evidence. Do not change capacities or restart services without the task authority.',
12
+ 'pipeline-engineering': 'Diagnose and repair assigned validation, caching, artifact, provenance or scheduler defects in an isolated dev worktree. Follow the normal review and release process.',
13
+ 'promotion-deployment': 'Advance explicitly eligible existing intents through DEV, STG and PROD using the owning release controller. Verify exact SHA and artifact custody. Never create duplicate handoffs or bypass a train.',
14
+ 'release-verification': 'Independently inspect exact release intent, deployment revision and actual environment proof. Do not implement source changes or perform deployment effects during verification.'}
15
+ PROFILES = {'trivial': ('claude-haiku-5-5','low'), 'routine': ('claude-sonnet-5-5','low'), 'standard': ('claude-sonnet-5-5','medium'), 'deep': ('claude-opus-5-5','high')}
16
+ RELEASE_RISK_TYPES = frozenset({'production-deployment','effect-reconciliation','signing-custody','concurrency-repair'})
17
+ COMMON = '''Execute only the bounded assignment, then return its checkpoint and stop. Do not spawn other agents, resume old workers, create schedules or send peer messages. Topolo Agent contains reusable specialization only. Load current task context from the referenced Topolo Docs/Bytes packet, preserving canonical task, intent, owner and effect identities. An empty local workspace grants no permission to repeat work. Local task packets are disposable caches. Refuse missing context or unresolved effects; never silently invent completion. Record outcome, next action and evidence in the existing task/GUI through supported mechanisms. A missing cloud or feed capability is a blocker, not permission to substitute chat history. No production mutation is authorized by this definition alone.'''
18
+ from .coordinator_context import CONTRACT as COORDINATOR
19
+
20
+ def definitions():
21
+ result={}
22
+ for discipline,prompt in DISCIPLINES.items():
23
+ for profile,(model,effort) in PROFILES.items():
24
+ name='cicd-'+discipline+'-'+profile
25
+ result[name]={'description':discipline+'; '+profile+' task profile. Select using context/CICD-DISPATCH.md.', 'model':model,'effort':effort,'maxTurns':24,'permissionMode':'bypassPermissions','disallowedTools':['Agent'],'prompt':prompt+'\n\n'+COMMON}
26
+ if profile=='trivial':result[name]['tools']=['Read','Glob','Grep']
27
+ return result
28
+
29
+ def select(discipline,task):
30
+ if discipline not in DISCIPLINES:
31
+ raise PackError('Unknown CI/CD discipline')
32
+ config=default_configuration();constraints={'allowedModels':sorted({v[0] for v in PROFILES.values()}),'allowedEfforts':['low','medium','high']}
33
+ selection=_selection(config,task,'claude',constraints)
34
+ if task['type'] in RELEASE_RISK_TYPES and selection['rule']['profile']!='deep':
35
+ raise PackError('Release-risk tasks require a deep profile')
36
+ return 'cicd-'+discipline+'-'+selection['rule']['profile']
37
+
38
+ def install(workspace):
39
+ workspace=pathlib.Path(workspace).expanduser().resolve()
40
+ if not (workspace/'CLAUDE.md').is_file():
41
+ raise PackError('Existing coordinator CLAUDE.md required')
42
+ rows=definitions(); files={}
43
+ for name,row in rows.items():
44
+ header={k:v for k,v in row.items() if k!='prompt'};header={'name':name,**header}
45
+ # JSON values are valid YAML scalars and avoid implicit YAML conversions.
46
+ body='---\n'+'\n'.join(k+': '+json.dumps(v) for k,v in header.items())+'\n---\n\n'+row['prompt']+'\n'
47
+ files[workspace/'.claude/agents'/(name+'.md')]=body
48
+ guide='''# CI/CD dispatch contract
49
+
50
+ Use one fresh native Agent invocation per bounded task. Never resume a completed worker. These files are reusable templates, not sidebar sessions. Select the discipline and task profile before dispatch; pass task identity, canonical Docs/Bytes references, current owner, exact release intent, scope, acceptance and effect authority in the invocation. If an existing owner holds the task, reconcile rather than replace them.
51
+
52
+ Task profiles: trivial = Haiku 5.5 low, only low-risk format/extract-fields/reference-lookup/status-read; routine = Sonnet 5.5 low; standard = Sonnet 5.5 medium; complex or security-critical = Opus 5.5 high. Ordinary implementation is never trivial. Production effects, uncertain recovery, signing/custody and concurrency require deep. Choose the matching cicd-<discipline>-<profile> subagent_type; do not override its model or inherit the coordinator effort. Native model rejection is a visible blocker, never a silent fallback.
53
+
54
+ Disciplines: queue-recovery, runner-capacity, pipeline-engineering, promotion-deployment, release-verification. For simple read-only status extraction use queue-recovery-trivial. Before mutations read the current owning controller records, current signed authority and unresolved operations. Do not duplicate existing intents. Independent release verification uses a fresh release-verification worker. Completed model turns do not prove delivery.
55
+
56
+ Dispatch may run independent bounded tasks concurrently subject to current admitted capacity and disjoint ownership. Coordinator remains available for human input. Publish each meaningful checkpoint once to the existing canonical task and GUI; missing platform persistence or dispatch must be reported explicitly. Do not claim cross-machine duplicate protection from a local lock.
57
+ '''
58
+ files[workspace/'context/CICD-DISPATCH.md']=guide
59
+ # Reject unowned conflicting files before any write.
60
+ receipt_path=workspace/'.claude/cicd-installation.json'
61
+ prior=json.loads(receipt_path.read_text()) if receipt_path.exists() else {'files':{}}
62
+ for path,body in files.items():
63
+ if path.is_symlink():
64
+ raise PackError('Symlinked CI/CD definition')
65
+ if path.exists() and path.read_text()!=body and hashlib.sha256(path.read_bytes()).hexdigest()!=prior['files'].get(str(path.relative_to(workspace))):
66
+ raise PackError('Changed definition requires reconciliation: '+str(path))
67
+ for path,body in files.items():path.parent.mkdir(parents=True,exist_ok=True);path.write_text(body);path.chmod(0o600)
68
+ marker='\n\n## Native CI/CD specialist dispatch\nRead context/CICD-DISPATCH.md before assigning release work. Use the project cicd-* native subagents with explicit task profiles.\n'
69
+ charter=workspace/'CLAUDE.md'
70
+ # Older generated coordinators accidentally carried the worker-only contract.
71
+ # Replace only that exact known prefix; preserve the remaining role charter.
72
+ body=charter.read_text()
73
+ if body.startswith(COMMON):
74
+ charter.write_text(COORDINATOR+body[len(COMMON):])
75
+ if marker not in charter.read_text():
76
+ with charter.open('a') as stream:stream.write(marker)
77
+ receipt={'format':'topolo-cicd-specialists','schemaVersion':1,'profiles':PROFILES,'files':{str(p.relative_to(workspace)):hashlib.sha256(p.read_bytes()).hexdigest() for p in files},'workersCreated':0,'activationVerified':False}
78
+ atomic_json(receipt_path,receipt);return receipt
79
+
80
+ def main():
81
+ parser=argparse.ArgumentParser();sub=parser.add_subparsers(dest='action',required=True)
82
+ p=sub.add_parser('install');p.add_argument('workspace')
83
+ p=sub.add_parser('select');p.add_argument('discipline',choices=DISCIPLINES);p.add_argument('--task',required=True)
84
+ args=parser.parse_args();print(json.dumps(install(args.workspace) if args.action=='install' else {'subagent':select(args.discipline,json.loads(args.task))},indent=2))
85
+
86
+
87
+ def configure_profile(config):
88
+ """Set the pack-owned user scope; project scope cannot enable bypass."""
89
+ config=pathlib.Path(config).expanduser().resolve()
90
+ config.mkdir(parents=True,exist_ok=True,mode=0o700)
91
+ path=config/'settings.json'
92
+ if path.is_symlink():
93
+ raise PackError('Symlinked Claude user settings')
94
+ settings=json.loads(path.read_text()) if path.exists() else {}
95
+ settings.setdefault('permissions',{})['defaultMode']='bypassPermissions'
96
+ settings['model']='claude-sonnet-5-5'
97
+ settings.setdefault('modelSettings',{}).setdefault('claude-sonnet-5-5',{})['effortLevel']='low'
98
+ atomic_json(path,settings)
99
+ return {'path':str(path),'settingsReadback':json.loads(path.read_text()),'existingDesktopSessionsVerified':False}
100
+
101
+ if __name__=='__main__':main()
@@ -10,6 +10,7 @@ import json
10
10
  import os
11
11
  import pathlib
12
12
  import pty
13
+ import re
13
14
  import select as io_select
14
15
  import shutil
15
16
  import subprocess
@@ -28,19 +29,21 @@ def find_binary():
28
29
  return shutil.which('claude')
29
30
 
30
31
 
31
- def handoff(binary, directory, session_id):
32
+ def handoff(binary, directory, session_id, env=None):
32
33
  """Native --desktop requires a TTY. No prompt, model call or shell is used."""
33
34
  if not binary:
34
35
  raise PackError('Claude Code 2.1.285+ is required; install Claude Desktop or Claude Code.')
35
36
  version = subprocess.run([binary, '--version'], capture_output=True, text=True, timeout=10)
36
- if version.returncode or not version.stdout.startswith('2.1.289 '):
37
- raise PackError('This native import adapter is qualified for Claude Code 2.1.289; selected binary: ' + version.stdout.strip())
37
+ release = re.match(r'^(\d+)\.(\d+)\.(\d+)\s+\(Claude Code\)', version.stdout)
38
+ if version.returncode or release is None or tuple(map(int, release.groups())) < (2, 1, 285):
39
+ raise PackError('Native import requires the Desktop handoff capability introduced in Claude Code 2.1.285; selected binary: ' + version.stdout.strip())
38
40
  master, slave = pty.openpty()
39
41
  process = None
40
42
  output = bytearray()
41
43
  try:
42
44
  process = subprocess.Popen([binary, '--desktop', '--resume', session_id],
43
- cwd=directory, stdin=slave, stdout=slave, stderr=slave)
45
+ cwd=directory, stdin=slave, stdout=slave, stderr=slave,
46
+ **({'env':env} if env is not None else {}))
44
47
  os.close(slave)
45
48
  slave = None
46
49
  deadline = time.monotonic() + 20
@@ -105,6 +108,10 @@ def materialize(lib, name, selected, config):
105
108
  if record['sourceRevision'] != revision:
106
109
  raise PackError('Existing Claude agent uses a different revision; build a new named pack to preserve its context.')
107
110
  if record['importState'] != 'preparing':
111
+ if agent['id'] == 'cicd':
112
+ from .cicd import install, configure_profile
113
+ install(record['workspace'])
114
+ configure_profile(config)
108
115
  records.append(record)
109
116
  continue
110
117
  else:
@@ -139,12 +146,19 @@ def materialize(lib, name, selected, config):
139
146
  if relative.as_posix() == 'AGENTS.md':
140
147
  relative = pathlib.Path('CLAUDE.md')
141
148
  write_new(workspace / relative, lib.object(resource['content']).read_bytes())
149
+ if agent['id'] == 'cicd':
150
+ from .cicd import install, configure_profile
151
+ install(workspace)
152
+ configure_profile(config)
142
153
  session_id = record['cliSessionId']
143
154
  transcript = pathlib.Path(record['transcript'])
144
155
  note = (agent['title'] + '. You are this Topolo Agent Pack role in Claude Desktop. '
145
156
  'Read CLAUDE.md and relevant context/ files before work. '
146
157
  'This is starting context only; no assignment, claim or prior conversation is inherited. '
147
- 'Await a new human request. Coordination requires human authorization.')
158
+ 'Await a new human request. A human request authorizes the necessary bounded specialist dispatch '
159
+ 'within its scope; do not ask for that authorization again. Preserve existing owners, holds '
160
+ 'and action-specific approval requirements. Use the configured dispatch adapter and canonical '
161
+ 'task records; do not substitute coordinator execution when dispatch is unavailable.')
148
162
  rows = [{'parentUuid': None, 'isSidechain': False, 'userType': 'external',
149
163
  'cwd': str(workspace), 'sessionId': session_id, 'version': '2.1.289',
150
164
  'type': 'user', 'message': {'role': 'user', 'content': note},
@@ -214,7 +228,8 @@ def main(argv):
214
228
  if args.command in {'status', 'doctor', 'recover', 'list'}:
215
229
  proof = readback(state['agents'], app_data)
216
230
  active_path = lib.root / 'claude/active.json'
217
- pending_path = lib.root / 'claude/native-operation.json'
231
+ pending_path = lib.root / ('claude/startup-operation.json' if
232
+ (lib.root / 'claude/startup-operation.json').exists() else 'claude/native-operation.json')
218
233
  result = {**state, 'adoptionProof': proof, 'verifiedAgents': sum(p['verified'] for p in proof),
219
234
  'fullPackSwitchSupported': False, 'sidebarGroupsAndPinsVerified': False,
220
235
  'nativeRecordsRewritten': False, 'modelIdentityResponseVerified': False,
@@ -224,7 +239,7 @@ def main(argv):
224
239
  'nativeRecoveryReplayed': False,
225
240
  'verificationBoundary': 'read-only adoption records; use the explicit native connection for current settings or recovery'}
226
241
  else:
227
- if (lib.root / 'claude/native-operation.json').exists():
242
+ if any((lib.root / ('claude/'+name)).exists() for name in ['native-operation.json','startup-operation.json','activation-operation.json','launch-operation.json']):
228
243
  raise PackError('An unresolved native Claude operation blocks imports and preparation. Use topolo agent-packs recover with the explicit native connection; no effects are replayed.')
229
244
  manifest, revision = lib.load(args.source)
230
245
  chosen = select(manifest, args.sets, args.agents)
@@ -0,0 +1,157 @@
1
+ """Read-only cloud and service evidence for the exact installed Desktop roster."""
2
+ import hashlib,json,os,time,fcntl
3
+ from .storage import Library,read_json,PackError,canonical
4
+ from .work import require
5
+ from .cloud_work import CloudStore
6
+ from .work_artifacts import read_checkpoint
7
+ from .claude_service import ensure_running,verify_retired_binding
8
+ from .live import request
9
+
10
+
11
+ def observe(root,qualification_task=None,previous=None,connection=None,start_service=False,native_proof=False,wait_retired_service=False,require_native_idle=False,successor_connection=None,warm_connection=None):
12
+ lib=Library(root);store=CloudStore(root);version,state=store.load()
13
+ if warm_connection is not None:
14
+ require(previous is None and connection is None and not start_service and not wait_retired_service and not require_native_idle and not native_proof and qualification_task is None and successor_connection is None,'Warm restore cannot change lifecycle custody')
15
+ binding=read_json(lib.root/'work/claude-frontdoors.json')
16
+ require(isinstance(warm_connection,dict) and set(warm_connection)=={'pid','runtime','identity','socket','secretPath'} and binding.get('nativeConnection')==warm_connection and binding.get('desktopPid')==warm_connection['pid'] and warm_connection['runtime']==str(lib.root/'claude/task-runtime'),'Current warm binding differs')
17
+ ensure_running(lib)
18
+ service=request(lib,'status',{})
19
+ require(service.get('harness')=='claude' and service.get('cloudDispatchReady') is True and service.get('desktopPid')==warm_connection['pid'] and service.get('runtime')==warm_connection['runtime'] and isinstance(service.get('activeTurns'),list) and all(isinstance(w,str) for w in service['activeTurns']) and len(set(service['activeTurns']))==len(service['activeTurns']),'Authenticated warm service differs')
20
+ from .claude_desktop import call
21
+ status=call(warm_connection,{'kind':'status'})
22
+ require(status.get('harness')=='claude-desktop' and status.get('pid')==warm_connection['pid'] and status.get('identity')==warm_connection['identity'] and status.get('qualified') is True and type(status.get('workers')) is int and status['workers']>=0,'Qualified warm native control differs')
23
+ return {'warmOwner':True,'nativeInputsSent':False,'workerCustodyPreserved':True,'service':service,'binding':store.binding}
24
+ if successor_connection is not None:
25
+ require(previous is None and connection is None and not start_service and not wait_retired_service and not require_native_idle and not native_proof and qualification_task is None,'Exact successor observation cannot change lifecycle custody')
26
+ binding=read_json(lib.root/'work/claude-frontdoors.json')
27
+ require(isinstance(successor_connection,dict) and set(successor_connection)=={'pid','runtime','identity','socket','secretPath'} and binding.get('nativeConnection')==successor_connection and binding.get('desktopPid')==successor_connection['pid'] and successor_connection['runtime']==str(lib.root/'claude/task-runtime'),'Current successor binding differs')
28
+ try:service=request(lib,'status',{})
29
+ except (OSError,PackError):return {'existingSuccessor':False,'serviceStarted':False,'nativeInputsSent':False}
30
+ require(service.get('harness')=='claude' and service.get('cloudDispatchReady') is True and service.get('desktopPid')==successor_connection['pid'] and service.get('runtime')==successor_connection['runtime'] and isinstance(service.get('activeTurns'),list) and all(isinstance(w,str) for w in service['activeTurns']) and len(set(service['activeTurns']))==len(service['activeTurns']),'Authenticated successor service differs; preserve its ownership')
31
+ from .claude_desktop import call
32
+ status=call(successor_connection,{'kind':'status'})
33
+ require(status.get('harness')=='claude-desktop' and status.get('pid')==successor_connection['pid'] and status.get('identity')==successor_connection['identity'] and status.get('qualified') is True and type(status.get('workers')) is int and status['workers']>=0,'Qualified successor native control differs')
34
+ native=call(successor_connection,{'kind':'frontdoors'})
35
+ require(native.get('state')=='installed-frontdoors-verified' and native.get('nativeInputsSent') is False,'Native successor front doors differ')
36
+ return {'existingSuccessor':True,'serviceStarted':False,'workerCustodyPreserved':True,'nativeInputsSent':False,'native':native,'service':service,'binding':store.binding,'versionId':version,'generation':state['generation']}
37
+ if previous is not None:
38
+ if connection is not None:
39
+ verify_retired_binding(lib,previous,connection)
40
+ else:
41
+ old=previous['nativeConnection']
42
+ try:os.kill(old['pid'],0)
43
+ except ProcessLookupError:pass
44
+ else:raise ValueError('Previous native owner remains alive')
45
+ tasks={row[0]:json.loads(row[2]) for row in state['tables']['tasks']}
46
+ for row in state['tables']['workers']:
47
+ worker=json.loads(row[2]);cwd=worker.get('nativePlan',{}).get('cwd','')
48
+ if cwd==old['runtime'] or cwd.startswith(old['runtime']+os.sep):
49
+ require(cwd!=old['runtime'] and os.path.normpath(cwd)==cwd,'Ambiguous previous native workspace path')
50
+ if not cwd.startswith(old['runtime']+os.sep):
51
+ if tasks.get(worker['taskId'],{}).get('ownerInstallationId')==store.binding['installationId']:
52
+ require(worker['state']=='archived' and worker.get('nativeArchived') is True or worker['state']=='failed' and not worker.get('turnId') and (not worker.get('nativeId') or worker.get('nativeArchived') is True),'Historical worker remains active or UNKNOWN')
53
+ continue
54
+ require(worker.get('resolution',{}).get('harness')=='claude','Previous runtime worker harness custody differs')
55
+ if worker['state']=='failed' and not worker.get('turnId') and (not worker.get('nativeId') or worker.get('nativeArchived') is True):continue
56
+ require(worker['state']=='archived' and worker.get('nativeArchived') is True and worker.get('checkpoint') and worker.get('turnId'),'Previous native worker remains active or UNKNOWN')
57
+ payload=read_checkpoint(store.root,worker['checkpoint'],worker['taskId'],worker['id'],client=store.client);receipt=payload['nativeReceipt']
58
+ require(payload['phase']==worker['phase'] and receipt['threadId']==worker['nativeId'] and receipt['turnId']==worker['turnId'] and receipt['status'] in {'completed','failed','cancelled'},'Previous terminal checkpoint differs')
59
+ if wait_retired_service:
60
+ require(previous is not None,'Exact retired service owner required')
61
+ for _ in range(50):
62
+ try:status=request(lib,'status',{})
63
+ except (OSError,PackError):
64
+ bridge=read_json(lib.root/'live-bridge.json',{})
65
+ if bridge:
66
+ require(bridge.get('harness')=='claude' and type(bridge.get('pid')) is int and bridge['pid']>1,'Prior service process custody is unverified')
67
+ try:os.kill(bridge['pid'],0)
68
+ except ProcessLookupError:pass
69
+ else:
70
+ time.sleep(.1)
71
+ continue
72
+ with (lib.root/'work/claude-service-owner.lock').open('a') as owner:
73
+ try:fcntl.flock(owner,fcntl.LOCK_EX|fcntl.LOCK_NB)
74
+ except BlockingIOError:pass
75
+ else:break
76
+ time.sleep(.1)
77
+ continue
78
+ require(status.get('harness')=='claude' and status.get('desktopPid')==previous['nativeConnection']['pid'],'Another service owns this installation')
79
+ time.sleep(.1)
80
+ else:raise ValueError('Previous native service has not finished its owned shutdown')
81
+ task=None
82
+ if qualification_task is not None:
83
+ tasks=[json.loads(row[2]) for row in state['tables']['tasks'] if row[0]==qualification_task]
84
+ require(len(tasks)==1,'Qualification task is not in the current cloud authority')
85
+ task=tasks[0]
86
+ require(task['state']=='completed' and task.get('ownerInstallationId')==store.binding['installationId']
87
+ and task.get('verification',{}).get('verdict')=='completed','Independent cloud qualification is incomplete or belongs to another installation')
88
+ workers=[json.loads(row[2]) for row in state['tables']['workers'] if row[1]==qualification_task]
89
+ attempt=[w for w in workers if w.get('attempt',1)==task.get('attempt',1)]
90
+ generation=task.get('reviewGeneration',0);history=task.get('reviewHistory',[])
91
+ require(type(generation) is int and generation>=0 and isinstance(history,list) and len(history)==generation,'Authorized review generation history differs')
92
+ execution=[w for w in attempt if w['phase']!='review'];reviews=[w for w in attempt if w['phase']=='review']
93
+ require(len(execution)==2 and {w['phase'] for w in execution}=={'route','execute'} and len(reviews)==generation+1
94
+ and all(type(w.get('reviewGeneration',0)) is int for w in reviews)
95
+ and sorted(w.get('reviewGeneration',0) for w in reviews)==list(range(generation+1)),
96
+ 'Original execution and unique authorized review generations are required')
97
+ latest=next(w for w in reviews if w.get('reviewGeneration',0)==generation)
98
+ current=execution+[latest];verification_by_worker={latest['id']:task['verification']}
99
+ require(all(w['state']=='archived' and w.get('nativeArchived') is True for w in attempt)
100
+ and task['verification'].get('nativeCheckpoint')==latest.get('checkpoint'),
101
+ 'Current Claude qualification phase receipts are incomplete')
102
+ result=task.get('result',{});executed=next(w for w in execution if w['phase']=='execute')
103
+ require(result.get('workerId')==executed['id'] and result.get('checkpoint')==executed.get('checkpoint'),'Qualification must preserve the exact original execution result')
104
+ for index,entry in enumerate(history):
105
+ rejected=next(w for w in reviews if w.get('reviewGeneration',0)==index)
106
+ require(isinstance(entry,dict) and set(entry)=={'reviewWorkerId','generation','verification','continuation'}
107
+ and type(entry['generation']) is int and entry['generation']==index and entry['reviewWorkerId']==rejected['id']
108
+ and isinstance(entry['verification'],dict)
109
+ and entry['verification'].get('verdict') in {'blocked','needs_input'}
110
+ and entry['verification'].get('nativeCheckpoint')==rejected.get('checkpoint'),
111
+ 'Prior rejected review identity and checkpoint must remain preserved')
112
+ continuation=entry['continuation']
113
+ require(isinstance(continuation,dict) and set(continuation)=={'requestId','taskId','revision','reviewWorkerId','reviewCheckpoint','note'}
114
+ and continuation['taskId']==task['id'] and continuation['reviewWorkerId']==rejected['id']
115
+ and continuation['reviewCheckpoint']==rejected.get('checkpoint')
116
+ and type(continuation['revision']) is int and continuation['revision']<task['revision']
117
+ and isinstance(continuation['note'],str) and bool(continuation['note'])
118
+ and [continuation['requestId'],hashlib.sha256(canonical(continuation)).hexdigest(),task['id']] in state['tables']['responses'],
119
+ 'Review continuation must have its exact canonical authorization receipt')
120
+ verification_by_worker[rejected['id']]=entry['verification']
121
+ for worker in workers:
122
+ if worker['state']=='failed' and not worker.get('turnId') and (not worker.get('nativeId') or worker.get('nativeArchived') is True):continue
123
+ require(worker['state']=='archived' and worker.get('nativeArchived') is True and worker.get('checkpoint') and worker.get('turnId'),'Qualification worker closure is unresolved')
124
+ if worker not in current and worker.get('resolution',{}).get('harness')!='claude':continue
125
+ payload=read_checkpoint(store.root,worker['checkpoint'],qualification_task,worker['id'],client=store.client)
126
+ receipt=payload['nativeReceipt']
127
+ require(payload['phase']==worker['phase'] and receipt['threadId']==worker['nativeId'] and receipt['turnId']==worker['turnId'] and receipt['status'] in {'completed','failed','cancelled'},'Qualification checkpoint identity differs')
128
+ if worker['id'] in verification_by_worker:
129
+ expected=verification_by_worker[worker['id']]
130
+ require(payload.get('outputError') is None and json.loads(payload['output'])=={k:expected[k] for k in ['verdict','summary','evidence']},'Review verification differs from its immutable checkpoint')
131
+ if worker in current:
132
+ resolution=worker.get('resolution',{});role=worker.get('roleSource',{})
133
+ require(receipt['status']=='completed' and resolution.get('harness')=='claude'
134
+ and isinstance(resolution.get('nativeACK'),dict) and resolution['nativeACK']==resolution.get('resolved')
135
+ and bool(resolution['nativeACK'].get('model')) and bool(resolution['nativeACK'].get('effort'))
136
+ and role.get('source')=='topolo-agent-definition' and role.get('rolesLoaded')==1
137
+ and role.get('wholePackExportLoaded') is False and role.get('memoryLoaded') is False
138
+ and bool(role.get('agentId')) and bool(role.get('instructionsSHA256'))
139
+ and payload.get('resolution')==resolution and payload.get('roleSource')==role,
140
+ 'Current qualification must prove selected Claude Agent context and native settings ACK')
141
+ native=None
142
+ if native_proof:
143
+ from .claude_desktop import call
144
+ binding=read_json(lib.root/'work/claude-frontdoors.json')
145
+ if require_native_idle:
146
+ status=call(binding['nativeConnection'],{'kind':'status'})
147
+ require(status.get('harness')=='claude-desktop' and status.get('pid')==binding['desktopPid'] and status.get('identity')==binding['nativeConnection']['identity'] and status.get('workers')==0 and status.get('qualified') is True,'Exact idle native control is required')
148
+ native=call(binding['nativeConnection'],{'kind':'frontdoors'})
149
+ require(native.get('state')=='installed-frontdoors-verified' and native.get('nativeInputsSent') is False,'Native front doors are unverified')
150
+ if start_service:ensure_running(lib)
151
+ service=request(lib,'status',{}) if start_service or qualification_task is not None else None
152
+ if service is not None:
153
+ binding=read_json(lib.root/'work/claude-frontdoors.json')
154
+ require(service.get('harness')=='claude' and service.get('cloudDispatchReady') is True and service.get('desktopPid')==binding['desktopPid'],'Current Claude dispatcher is not ready')
155
+ return {'native':native,'format':'topolo-claude-cloud-activation-proof-v1','binding':store.binding,'versionId':version,
156
+ 'generation':state['generation'],'qualificationTaskId':qualification_task,
157
+ 'qualificationRevision':task['revision'] if task else None,'service':service,'activated':True}
@@ -0,0 +1,216 @@
1
+ """Recover one already delivered Desktop completion, without resending any input.
2
+
3
+ The legacy native guard requires the original inventory during read/archive.
4
+ Only explicitly reviewed added outputs may be moved, reversibly, beneath its
5
+ excluded recovery directory. Original context is always checked by its digest.
6
+ """
7
+ import hashlib
8
+ import os
9
+ from pathlib import Path
10
+ import stat
11
+ import uuid
12
+
13
+ from .storage import atomic_json, canonical, read_json
14
+ from .work import require
15
+
16
+
17
+ def _file(path):
18
+ meta = path.lstat()
19
+ require(stat.S_ISREG(meta.st_mode) and meta.st_uid == os.getuid()
20
+ and not meta.st_mode & 0o022 and path.resolve() == path,
21
+ 'Recovery requires an owned regular non-writable-by-others file')
22
+ require(0 < meta.st_size <= 100 * 1024 * 1024, 'Recovery file exceeds boundary')
23
+ return hashlib.sha256(path.read_bytes()).hexdigest()
24
+
25
+
26
+ class OutputCustody:
27
+ def __init__(self, worker, outputs):
28
+ self.cwd = Path(worker['cwd'])
29
+ require(self.cwd.is_absolute() and self.cwd.resolve() == self.cwd,
30
+ 'Recovery workspace identity differs')
31
+ require(isinstance(outputs, list) and 0 < len(outputs) <= 16,
32
+ 'Explicit selected output manifest required')
33
+ self.outputs = []
34
+ for row in outputs:
35
+ require(isinstance(row, dict) and set(row) == {'path', 'sha256'},
36
+ 'Closed recovery output manifest required')
37
+ path = Path(row['path'])
38
+ require(path.is_relative_to(self.cwd) and path != self.cwd
39
+ and not path.is_relative_to(self.cwd / '.topolo-worker')
40
+ and path not in [p for p, _ in self.outputs], 'Recovery output path differs')
41
+ require(_file(path) == row['sha256'], 'Selected output digest differs')
42
+ self.outputs.append((path, row['sha256']))
43
+ self.initial = {}
44
+ for path in sorted(self.cwd.rglob('*')):
45
+ if path.is_relative_to(self.cwd / '.topolo-worker'):
46
+ continue
47
+ require(not path.is_symlink(), 'Recovery workspace contains a link')
48
+ if path.is_file() and path not in [p for p, _ in self.outputs]:
49
+ self.initial[str(path)] = _file(path)
50
+ require(hashlib.sha256(canonical(self.initial)).hexdigest()
51
+ == worker['nativePlan']['contextDigest'], 'Original selected context differs')
52
+ for directory in [self.cwd, self.cwd / '.topolo-worker', self.cwd / '.topolo-worker' / 'recovery']:
53
+ if directory.exists():
54
+ meta = directory.lstat()
55
+ require(stat.S_ISDIR(meta.st_mode) and meta.st_uid == os.getuid()
56
+ and not meta.st_mode & 0o022 and directory.resolve() == directory,
57
+ 'Recovery directory ownership differs')
58
+ self.directory = self.cwd / '.topolo-worker' / 'recovery' / worker['id']
59
+ self.journal = self.directory / 'journal.json'
60
+ require(not self.directory.exists(), 'Recovery already exists; reconcile its exact journal')
61
+ self.moved = []
62
+
63
+ def begin(self):
64
+ self.directory.mkdir(parents=True, mode=0o700)
65
+ self.record('prepared')
66
+
67
+ def record(self, state):
68
+ atomic_json(self.journal, {'format': 'topolo-native-completion-recovery-v1',
69
+ 'state': state, 'inputs': self.initial,
70
+ 'outputs': [{'path': str(p), 'sha256': h} for p, h in self.outputs]})
71
+
72
+ def quarantine(self):
73
+ require(not self.moved, 'Output quarantine already active')
74
+ for path, digest in self.outputs:
75
+ require(_file(path) == digest, 'Output changed before quarantine')
76
+ target = self.directory / ('output-' + str(len(self.moved)))
77
+ require(not target.exists(), 'Quarantine target exists')
78
+ os.rename(path, target)
79
+ self.moved.append((path, target, digest))
80
+ actual = {str(p): _file(p) for p in sorted(self.cwd.rglob('*'))
81
+ if p.is_file() and not p.is_relative_to(self.cwd / '.topolo-worker')}
82
+ require(actual == self.initial, 'Original context changed during quarantine')
83
+ self.record('quarantined')
84
+
85
+ def restore(self):
86
+ for path, target, digest in reversed(self.moved):
87
+ require(not path.exists() and _file(target) == digest,
88
+ 'Output restoration collision or digest differs; custody retained')
89
+ os.rename(target, path)
90
+ self.moved = []
91
+ self.record('outputs-restored')
92
+
93
+
94
+ def _terminal(result, worker):
95
+ plan = worker['nativePlan']
96
+ require(result.get('state') == 'native-task-completed'
97
+ and result.get('nativeId') == worker['nativeId']
98
+ and result.get('messageId') == worker['turnId']
99
+ and result.get('model') == plan['model'] and result.get('effort') == plan['effort']
100
+ and isinstance(result.get('result'), str)
101
+ and type(result.get('toolItems')) is int and result['toolItems'] >= 0,
102
+ 'Exact existing native terminal result required')
103
+ for key in ['resultUuid', 'cliSessionId']:
104
+ require(str(uuid.UUID(result[key])) == result[key], 'Native producer identity is unqualified')
105
+ return {'id': worker['turnId'], 'status': 'completed', 'output': result['result'],
106
+ 'toolItems': result['toolItems'], 'nativeSessionId': result['cliSessionId'],
107
+ 'nativeResultId': result['resultUuid']}
108
+
109
+
110
+ def _alive(pid):
111
+ try:
112
+ os.kill(pid, 0)
113
+ except ProcessLookupError:
114
+ return False
115
+ return True
116
+
117
+
118
+ def recover_completed(lib, request, store=None, native_call=None, alive=None, checkpoint=None):
119
+ """Recover after the exact old Python service stops; Desktop remains alive.
120
+
121
+ No dispatcher is started or pumped. The caller must separately qualify the
122
+ corrected native guard before starting the independent review continuation.
123
+ """
124
+ import fcntl
125
+ from .cloud_work import CloudStore
126
+ from .claude_desktop import call
127
+ from .work_artifacts import checkpoint as publish_checkpoint
128
+ require(set(request) == {'taskId', 'workerId', 'revision', 'attempt', 'turnId',
129
+ 'outputs', 'expectedServicePid', 'expectedNative'},
130
+ 'Closed native completion recovery request required')
131
+ checkpoint = checkpoint or publish_checkpoint
132
+ native_call = native_call or call
133
+ alive = alive or _alive
134
+ with lib.lock(), (lib.root / 'work/claude-service-start.lock').open('a') as start_lock:
135
+ try:
136
+ fcntl.flock(start_lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
137
+ except BlockingIOError:
138
+ require(False, 'Another Claude startup owns this installation')
139
+ previous = read_json(lib.root / 'live-bridge.json')
140
+ require(type(request['expectedServicePid']) is int and request['expectedServicePid'] > 1
141
+ and previous.get('harness') == 'claude'
142
+ and previous.get('pid') == request['expectedServicePid']
143
+ and not alive(request['expectedServicePid']),
144
+ 'Exact previous Python owner must be stopped before recovery')
145
+ connection = read_json(lib.root / 'work/claude-frontdoors.json')['nativeConnection']
146
+ require(alive(connection['pid']) and connection['pid'] == request['expectedNative']['pid']
147
+ and connection['identity'] == request['expectedNative']['identity'],
148
+ 'Original Desktop native owner differs')
149
+ require(native_call(connection, {'kind': 'status'}) == request['expectedNative']
150
+ and request['expectedNative'].get('qualified') is True
151
+ and request['expectedNative'].get('workers') == 1,
152
+ 'Exact sole native worker connection required')
153
+ store = store or CloudStore(lib.root)
154
+ task = store.get(request['taskId'])
155
+ worker = store.worker(request['workerId'])
156
+ require(task['state'] == 'unknown' and task['revision'] == request['revision']
157
+ and task['attempt'] == request['attempt'] and worker['state'] == 'unknown'
158
+ and worker['taskId'] == task['id'] and worker['phase'] == 'execute'
159
+ and worker['attempt'] == task['attempt'] and worker['turnId'] == request['turnId']
160
+ and worker['id'] in task['workerIds'] and not worker.get('checkpoint')
161
+ and task.get('ownerInstallationId') == store.binding['installationId'],
162
+ 'Exact owned UNKNOWN execution checkpoint required')
163
+ plan = worker['nativePlan']
164
+ require(all(plan.get(key) == worker.get(key) for key in ['taskId', 'nativeId', 'cwd'])
165
+ and plan.get('workerId') == worker['id'], 'Cloud native plan identity differs')
166
+ for other in store.workers():
167
+ require(other['id'] == worker['id'] or
168
+ other['state'] == 'archived' and other.get('nativeArchived') is True or
169
+ other['state'] == 'failed' and not other.get('turnId') and
170
+ (not other.get('nativeId') or other.get('nativeArchived') is True),
171
+ 'Another unresolved cloud worker prevents standalone recovery')
172
+ custody = OutputCustody(worker, request['outputs'])
173
+ custody.begin()
174
+ try:
175
+ custody.quarantine()
176
+ native = native_call(connection, {'kind': 'read', 'workerId': worker['id'],
177
+ 'messageId': worker['turnId']})
178
+ receipt = _terminal(native, worker)
179
+ finally:
180
+ custody.restore()
181
+ actual = {'id': receipt['id'], 'status': receipt['status'],
182
+ 'items': [{'type': 'agentMessage', 'text': receipt['output']}] +
183
+ [{'type': 'toolUse'} for _ in range(receipt['toolItems'])],
184
+ 'nativeProof': {'harness': 'claude-desktop', 'sessionId': receipt['nativeSessionId'],
185
+ 'resultId': receipt['nativeResultId']}}
186
+ proof, payload, normalized = checkpoint(store.root, task, worker, actual)
187
+ require(payload.get('outputError') is None and normalized is not None,
188
+ 'Native output contract is not checkpoint-ready')
189
+ require(store.get(task['id']) == task and store.worker(worker['id']) == worker,
190
+ 'Task or worker changed before recovered checkpoint publication')
191
+ store.worker_update(worker['id'], {'state': 'archiving', 'checkpoint': proof})
192
+ task = store.update(task['id'], task['revision'],
193
+ {'checkpoints': task['checkpoints'] + [proof]}, 'checkpoint-saved')
194
+ custody.record('checkpoint-saved')
195
+ try:
196
+ custody.quarantine()
197
+ retired = native_call(connection, {'kind': 'archive', 'workerId': worker['id'],
198
+ 'messageId': worker['turnId']})
199
+ require(retired.get('state') == 'native-worker-archived'
200
+ and retired.get('nativeId') == worker['nativeId'],
201
+ 'Existing native worker archival was not acknowledged')
202
+ finally:
203
+ custody.restore()
204
+ require(native_call(connection, {'kind': 'status'}) == {**request['expectedNative'], 'workers': 0},
205
+ 'Native archival readback differs')
206
+ store.worker_update(worker['id'], {'state': 'archived', 'nativeArchived': True,
207
+ 'nativeRetirement': {'kind': 'native-desktop-session',
208
+ 'desktopChatArchived': True, **retired}})
209
+ task = store.update(task['id'], task['revision'],
210
+ {'state': 'reviewing', 'phase': 'review',
211
+ 'result': {**normalized, 'checkpoint': proof, 'workerId': worker['id']},
212
+ 'summary': 'Existing native completion recovered without resend; independent review pending.'},
213
+ 'worker-released')
214
+ custody.record('archived')
215
+ return {'taskId': task['id'], 'workerId': worker['id'], 'attempt': task['attempt'],
216
+ 'state': task['state'], 'nativeId': worker['nativeId'], 'turnId': worker['turnId']}