@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
@@ -589,7 +589,7 @@ def materialize(library, components, runtime, mappings=None):
589
589
  # Generate the dual desktop/server section mapping from the selected composition.
590
590
  catalog_conn = sqlite3.connect(home / 'state_5.sqlite')
591
591
  import uuid
592
- groups = {}; pins = []; titles = {}; descriptions = {}; account_ids = set()
592
+ groups = {}; pins = []; titles = {}; descriptions = {}
593
593
  for agent in chosen:
594
594
  groupkey = (agent['pack'],agent['set'])
595
595
  if groupkey not in groups:
@@ -610,9 +610,10 @@ def materialize(library, components, runtime, mappings=None):
610
610
  ('complete',None,int(time.time()),int(time.time())))
611
611
  catalog_conn.commit(); catalog_conn.close()
612
612
  for manifest in manifests:
613
- account_ids.update(manifest['ui'].get('sourceAccountIds',[])); descriptions.update(manifest['ui'].get('descriptions',{}))
613
+ descriptions.update(manifest['ui'].get('descriptions',{}))
614
614
  groups_list = [g for (_,sid),g in groups.items() if sid!='intakes']
615
- sections = {account:{'sections':groups_list,'sectionOrder':['pinned']+[g['id'] for g in groups_list]+['chats','threads'],'collapsedSectionIds':[]} for account in account_ids}
615
+ # Portable source-account IDs are not destination authentication.
616
+ sections = {}
616
617
  gs={'thread-titles':{'titles':titles,'order':list(titles)},'pinned-thread-ids':[k.rsplit(':',1)[1] for k in pins],
617
618
  'thread-workspace-root-hints':{a['threadId']:remap(thread_sources[a['threadId']][1]['cwd'],mapping) for a in chosen},
618
619
  'electron-persisted-atom-state':{'sidebar-custom-sections-v3':sections,'unified-sidebar-pinned-order-v1':pins,'thread-descriptions-v1':descriptions,'flat-project-sidebar-preferences-v1':{'initialized':True,'mode':'project','chatSortMode':'updated_at','projectSortMode':'updated_at'}}}
@@ -640,6 +641,161 @@ def materialize(library, components, runtime, mappings=None):
640
641
  from .topolo import attach_runtime
641
642
  attach_runtime(library,runtime)
642
643
  result={'threads':len(thread_sources),'agents':len(chosen),'historyOffsetsRebased':True,'activeGoalsPaused':sorted(set(paused_goals)),
643
- 'home':str(home),'userData':str(runtime/'desktop-data'),'dependencies':dependencies,'mappings':mapping}
644
+ 'home':str(home),'userData':str(runtime/'desktop-data'),'dependencies':dependencies,'mappings':mapping,
645
+ 'sidebar':{'state':'pending-account-read','frontendVerified':False}}
644
646
  atomic_json(runtime/'verification.json',result)
645
647
  return result
648
+
649
+
650
+ def sidebar_binding(runtime, account_response):
651
+ """Bind the existing roster to a native account/read result, never pack IDs.
652
+
653
+ The caller owns the selected idle runtime or current managed generation.
654
+ Authentication and provider configuration are not read or changed here.
655
+ """
656
+ from .storage import read_json
657
+ import uuid
658
+ runtime = pathlib.Path(runtime)
659
+ if (not isinstance(account_response, dict) or
660
+ type(account_response.get('requiresOpenaiAuth')) is not bool):
661
+ raise PackError('Native account/read response unavailable')
662
+ account = account_response.get('account')
663
+ routing = account_response.get('workspaceRouting')
664
+ account_id = None
665
+ if account is None:
666
+ state = 'pending-sign-in'
667
+ elif not isinstance(account, dict) or account.get('type') not in {'chatgpt', 'apiKey', 'amazonBedrock'}:
668
+ raise PackError('Unknown native account type')
669
+ elif account['type'] != 'chatgpt':
670
+ state = 'pending-chatgpt-account'
671
+ elif routing is None:
672
+ state = 'pending-account-identity'
673
+ elif (not isinstance(routing, dict) or not isinstance(routing.get('chatgptAccountId'), str) or
674
+ not re.fullmatch(r'[A-Za-z0-9_-]{1,200}', routing['chatgptAccountId'])):
675
+ raise PackError('Native workspace account identity unavailable')
676
+ else:
677
+ account_id = routing['chatgptAccountId']
678
+ state = 'configured-rendering-unverified'
679
+ agents = read_json(runtime / 'agents.json')['agents']
680
+ groups = {}; pins = []
681
+ conn = ro(runtime / 'home/state_5.sqlite')
682
+ try:
683
+ for agent in agents:
684
+ row = conn.execute('SELECT thread_section_id FROM threads WHERE id=?', (agent['threadId'],)).fetchone()
685
+ if not row or not row[0]:
686
+ raise PackError('Native sidebar roster membership unavailable')
687
+ key = 'codex:thread:local:' + agent['threadId']
688
+ if agent.get('pinned'):
689
+ pins.append(key)
690
+ continue
691
+ groupkey = (agent['pack'], agent['set'])
692
+ if groupkey not in groups:
693
+ groups[groupkey] = {'id':str(uuid.uuid5(uuid.NAMESPACE_URL, 'codex-pack-desktop:' + '/'.join(groupkey))),
694
+ 'name':agent['sectionName'], 'hostSectionIds':{'local':row[0]},
695
+ 'itemKeys':[], 'appearance':None}
696
+ if groups[groupkey]['hostSectionIds']['local'] != row[0]:
697
+ raise PackError('Native sidebar group membership conflicts')
698
+ groups[groupkey]['itemKeys'].append(key)
699
+ finally:
700
+ conn.close()
701
+ layout = {'sections':list(groups.values()),
702
+ 'sectionOrder':['pinned'] + [g['id'] for g in groups.values()] + ['chats','threads'],
703
+ 'collapsedSectionIds':[]}
704
+ return {'state':state, 'accountId':account_id, 'sections':{account_id:layout} if account_id else {},
705
+ 'pins':pins, 'agents':len(agents), 'frontendVerified':False, 'sectionsRenderedVerified':False}
706
+
707
+
708
+ class SidebarJournal:
709
+ """Presentation steps use the existing library transaction/recovery store."""
710
+ def __init__(self, lib, runtime, context, before, expected, report, parent=None):
711
+ import uuid
712
+ from .storage import read_json
713
+ self.lib=lib;self.runtime=pathlib.Path(runtime).resolve()
714
+ if not self.runtime.is_relative_to(lib.root.resolve()):
715
+ raise PackError('Sidebar runtime is outside the owned library')
716
+ self.parent=parent is not None;self.dispatched=False
717
+ if parent is None:
718
+ if lib.pending():raise PackError('Pending operation requires original-context recovery')
719
+ transaction=uuid.uuid4().hex
720
+ self.path=lib.root/'journals'/(transaction+'.json')
721
+ self.document={'id':transaction,'phase':'sidebar-binding','operation':'sidebar-bind',
722
+ 'before':lib.active(),'targetRuntime':str(self.runtime),'createdAt':now()}
723
+ else:
724
+ self.path,self.document=parent
725
+ self.path=pathlib.Path(self.path)
726
+ if (self.path.parent!=lib.root/'journals' or read_json(self.path)!=self.document or
727
+ str(self.runtime) not in {self.document.get('targetRuntime'),self.document.get('before',{}).get('runtime'),self.document.get('after',{}).get('runtime')} or
728
+ self.document.get('phase') in {'complete','rolled-back'} or
729
+ any(path!=self.path for path in lib.pending())):
730
+ raise PackError('Original presentation transaction changed')
731
+ if self.document.get('sidebar',{}).get('state') in {'UNKNOWN','may-have-effects'}:
732
+ raise PackError('Original sidebar effects require reconciliation before another write')
733
+ self.document['sidebar']={'state':'may-have-effects','runtime':str(self.runtime),
734
+ 'context':context,'before':before,'expected':expected,'report':report}
735
+ atomic_json(self.path,self.document)
736
+
737
+ def complete(self):
738
+ self.document['sidebar']['state']='complete'
739
+ if not self.parent:self.document['phase']='complete'
740
+ atomic_json(self.path,self.document)
741
+
742
+ def failed(self,error):
743
+ self.document['sidebar']['state']='UNKNOWN' if self.dispatched else 'not-executed'
744
+ if not self.dispatched and not self.parent:self.document['phase']='rolled-back'
745
+ self.document['sidebar']['errorClass']=type(error).__name__
746
+ try:atomic_json(self.path,self.document)
747
+ except Exception as journal_error:
748
+ # The original may-have-effects entry is already durable. Preserve
749
+ # the primary failure; never turn a failed status write into success.
750
+ error.add_note('Sidebar uncertainty journal update failed: '+type(journal_error).__name__)
751
+
752
+
753
+ def bind_sidebar(runtime, account_response, *, lib, guard, journal=None, context=None):
754
+ """Write only under an existing/new journal and a current native guard."""
755
+ from .storage import read_json
756
+ binding=sidebar_binding(runtime,account_response)
757
+ path=pathlib.Path(runtime)/'home/.codex-global-state.json'
758
+ before=read_json(path);gs=copy.deepcopy(before)
759
+ atoms=gs.setdefault('electron-persisted-atom-state',{})
760
+ if binding['accountId']:
761
+ sections=dict(atoms.get('sidebar-custom-sections-v3',{}));sections.update(binding['sections'])
762
+ atoms['sidebar-custom-sections-v3']=sections
763
+ atoms['unified-sidebar-pinned-order-v1']=binding['pins']
764
+ report={k:v for k,v in binding.items() if k not in {'sections','pins'}}
765
+ current=guard()
766
+ mutation=SidebarJournal(lib,runtime,context or current,before,gs,report,parent=journal)
767
+ try:
768
+ if read_json(path)!=before:raise PackError('Sidebar file changed before write')
769
+ if guard()!=current:raise PackError('Sidebar native context changed before write')
770
+ mutation.dispatched=True;atomic_json(path,gs)
771
+ if read_json(path)!=gs or guard()!=current:raise PackError('Sidebar file or native context changed after write')
772
+ report_path=pathlib.Path(runtime)/'sidebar-binding.json';atomic_json(report_path,report)
773
+ if read_json(report_path)!=report or guard()!=current:raise PackError('Sidebar report/context read-back differs')
774
+ mutation.complete();return report
775
+ except BaseException as error:
776
+ mutation.failed(error);raise
777
+
778
+
779
+ def reconcile_sidebar(lib,path,document,current,actual,guard):
780
+ """Reconcile exact observed presentation; never replay a presentation setter."""
781
+ from .storage import read_json
782
+ entry=document.get('sidebar')
783
+ targets={document.get('targetRuntime'),document.get('before',{}).get('runtime'),document.get('after',{}).get('runtime')}
784
+ if not entry or entry['runtime'] not in targets or actual!=entry['expected']:
785
+ raise PackError('Sidebar effects UNKNOWN; original destination presentation needs reconciliation')
786
+ original=entry['context']
787
+ # A new read-only connection is allowed; it never impersonates the original
788
+ # process generation. Account/home/config/binary/roster scope must agree.
789
+ stable=lambda value:{k:v for k,v in value.items() if k not in {'generation','readerPID'}}
790
+ if stable(current)!=stable(original) or guard()!=current:
791
+ raise PackError('Sidebar recovery owner/account/home/config changed')
792
+ report_path=pathlib.Path(entry['runtime'])/'sidebar-binding.json'
793
+ # Completing a missing local report follows exact authoritative readback;
794
+ # neither offline state nor the managed desktop setter is replayed.
795
+ atomic_json(report_path,entry['report'])
796
+ if read_json(report_path)!=entry['report'] or guard()!=current:
797
+ raise PackError('Sidebar reconciliation report/context changed')
798
+ entry['state']='complete';entry['reconciledCurrentContext']=current
799
+ if document.get('operation')=='sidebar-bind':document['phase']='complete'
800
+ atomic_json(path,document)
801
+ return entry['report']
@@ -59,6 +59,14 @@ def settings(agent, harness, consent=None):
59
59
  spec = PROFILES[profile]
60
60
  model = MODELS[harness][spec['tier']]
61
61
  if harness == 'codex':
62
+ if 'modelPreset' in agent:
63
+ preset = agent['modelPreset']
64
+ if (not isinstance(preset, dict) or set(preset) != {'model', 'reasoningEffort'} or
65
+ not isinstance(preset['model'], str) or not preset['model'] or
66
+ preset['model'].endswith('-latest') or preset['reasoningEffort'] not in {'low', 'medium', 'high'}):
67
+ raise PackError('Invalid explicit fresh-role model preset')
68
+ model = preset['model']
69
+ spec = {**spec, 'effort': preset['reasoningEffort']}
62
70
  native = {'model': model, 'model_reasoning_effort': spec['effort'],
63
71
  'approval_policy': 'on-request', 'approvals_reviewer': 'user',
64
72
  'sandbox_mode': spec['localAccess'],
@@ -0,0 +1,33 @@
1
+ """Selected canonical lifecycle evidence for one independent task review."""
2
+ import copy
3
+ from .storage import atomic_json
4
+ from .work import require
5
+
6
+
7
+ def materialize(store, task, cwd):
8
+ current = store.get(task['id'])
9
+ require(current == task, 'Review task changed before workflow context materialization')
10
+ workers = [w for w in store.workers() if w['taskId'] == task['id']
11
+ and w.get('attempt', 1) == task.get('attempt', 1)]
12
+ require(len(workers) <= 32 and any(w['phase'] == 'execute' and w['id'] == task['result']['workerId']
13
+ and w['state'] == 'archived' and w.get('nativeArchived') is True
14
+ and w.get('checkpoint') == task['result']['checkpoint'] for w in workers),
15
+ 'Review requires the exact archived execution checkpoint')
16
+ fields = ['id', 'phase', 'role', 'state', 'nativeId', 'turnId', 'resolution', 'roleSource',
17
+ 'checkpoint', 'reviewGeneration', 'nativeArchived', 'nativeRetirement', 'createdAt']
18
+ selected = [{k: copy.deepcopy(w[k]) for k in fields if k in w} for w in workers]
19
+ started = min(w['createdAt'] for w in workers)
20
+ events = [{k: e[k] for k in ['sequence', 'kind', 'at']}
21
+ for e in store.events(task['id']) if e['at'] >= started]
22
+ require(len(events) <= 512, 'Selected review workflow events exceed boundary')
23
+ packet = {'format': 'topolo-task-review-workflow-v1', 'taskId': task['id'],
24
+ 'attempt': task.get('attempt', 1), 'revision': task['revision'],
25
+ 'cloudReference': {k: store.binding[k] for k in
26
+ ['organizationId', 'workspaceId', 'documentId', 'datasetId']},
27
+ 'route': copy.deepcopy(task['route']), 'workers': selected, 'events': events,
28
+ 'evidenceContract': {'workspace': str(cwd.resolve()),
29
+ 'readOutputExample': str(cwd.resolve() / 'evidence/retained-page-0.json'),
30
+ 'instructions': 'Create an evidence directory in this reviewer workspace and save newly performed read-only command output there before citing it. Return absolute paths to existing nonempty regular files inside this workspace. Existing inspected context files may also be cited. Never cite /tmp, /private/tmp, another worker directory, a symlink or a path containing .. . Do not copy external temporary files to bypass this boundary, invent evidence or claim a read that was not performed. Each declared file is validated before any evidence upload; one external path rejects the entire declared evidence list.'},
31
+ 'boundary': 'Canonical cloud lifecycle records for this task attempt only. A specialization role and its task type are separate fields. Event persistence proves recorded progress, not rendered browser delivery. The current reviewer is intentionally not archived until its result is checkpointed; the coordinator must verify that archival and live Activity delivery afterward.'}
32
+ atomic_json(cwd / 'context/workflow-evidence.json', packet)
33
+ return packet
@@ -0,0 +1,137 @@
1
+ """Specialization metadata independent of native chats and captured histories."""
2
+ import copy,hashlib,json,pathlib
3
+ from .storage import Library,atomic_json,canonical,read_json,component_key
4
+ from .work import require
5
+ from .task_models import default_configuration
6
+ from .cicd import DISCIPLINES
7
+ from .domain_context import references
8
+
9
+ COORDINATORS={'engineering':'engineering','operations':'operator-01','cicd':'cicd'}
10
+ REVIEWS={'engineering':'adversarial-qa','operations':'workflow-integration-specialist','cicd':'release-verification'}
11
+
12
+ def digest(value):
13
+ return hashlib.sha256(canonical(value)).hexdigest()
14
+
15
+ def install(lib,pack,agent_workspace,bytes_workspace,max_workers=4,client=None):
16
+ manifest,revision=lib.load(pack)
17
+ require(len(manifest['agents'])==3 and {(a['set'],a['id']) for a in manifest['agents']}=={('intakes',v) for v in COORDINATORS.values()},'A fresh three-coordinator pack is required; no specialist native chats')
18
+ require(type(max_workers) is int and 1<=max_workers<=8,'Invalid dispatch capacity')
19
+ root=lib.root/'work';root.mkdir(exist_ok=True,mode=0o700)
20
+ require(not (root/'config.json').exists(),'Existing task installation must be reconciled before replacement')
21
+ if client is None:
22
+ from .topolo import Client
23
+ client=Client('production','org:org_topolo_platform')
24
+ rows=client.action('app_topolo_agent.agents.list',{},agent_workspace)['agents'];roles=[];refs={};seen=set()
25
+ for agent in rows:
26
+ config=json.loads(agent['config']) if isinstance(agent['config'],str) else agent['config'];spec=config.get('specialization',{})
27
+ if spec.get('format')!='topolo-role-specialization-v1':continue
28
+ logical=spec.get('logicalId','');parts=logical.split('/')
29
+ require(len(parts)==2,'Invalid logical specialization identity')
30
+ group,identifier=parts
31
+ if group=='intakes' and identifier not in COORDINATORS.values():continue
32
+ require(group in {'intakes','specialists','operators','operator-skills'} and logical not in seen,'Unknown or duplicate specialization')
33
+ instructions=spec.get('instructions')
34
+ require(agent['organizationId']=='org_topolo_platform' and agent['workspaceId']==agent_workspace and isinstance(instructions,str) and 100<=len(instructions)<=16000 and config.get('purpose')==instructions,'Unqualified specialization definition')
35
+ seen.add(logical);role={'pack':pack,'set':group,'id':identifier,'title':identifier.replace('-',' ').title()};key=component_key(role)
36
+ roles.append(role);refs[key]={'agentId':agent['id'],'workspaceId':agent_workspace,'logicalId':logical,'instructionsSHA256':hashlib.sha256(instructions.encode()).hexdigest()}
37
+ domain=references(config,logical)
38
+ if domain:refs[key]['domainContextRefs']=domain
39
+ channels={}
40
+ for channel,intake in COORDINATORS.items():
41
+ eligible=[r for r in roles if (r['set']=='intakes' and r['id']==intake) or
42
+ (channel=='engineering' and r['set']=='specialists' and r['id'] not in DISCIPLINES) or
43
+ (channel=='operations' and r['set'] in {'operators','operator-skills'}) or
44
+ (channel=='cicd' and r['set']=='specialists' and r['id'] in DISCIPLINES)]
45
+ require(sum(r['set']=='intakes' for r in eligible)==1 and sum(r['id']==REVIEWS[channel] for r in eligible)==1,'Channel specialization or independent reviewer is missing')
46
+ require(len({r['id'] for r in eligible})==len(eligible),'Ambiguous discipline names in channel')
47
+ channels[channel]={'intakeRole':intake,'reviewRole':REVIEWS[channel],'roleKeys':[component_key(r) for r in eligible]}
48
+ configuration=default_configuration();ceiling={'allowedModels':sorted({model for harness in configuration['harnesses'].values() for model in harness['tiers'].values()}),'allowedEfforts':['low','medium','high']}
49
+ constraints={'pack':ceiling,'groups':{r['set']:copy.deepcopy(ceiling) for r in roles},'roles':{component_key(r):copy.deepcopy(ceiling) for r in roles}}
50
+ registry={'format':'topolo-role-bindings-v1','organizationId':'org_topolo_platform','roles':refs}
51
+ catalog={'format':'topolo-task-role-catalog-v1','pack':pack,'nativePackRevision':revision,'roles':roles,'bindings':refs,'channels':channels,'configuration':configuration,'constraints':constraints}
52
+ atomic_json(root/'role-catalog.json',catalog);atomic_json(root/'role-bindings.json',registry)
53
+ atomic_json(root/'context-authority.json',{'format':'topolo-task-context-authority-v1','organizationId':'org_topolo_platform','workspaceId':bytes_workspace,'scope':'org'})
54
+ atomic_json(root/'config.json',{'format':'topolo-task-dispatch-v2','pack':pack,'catalogDigest':digest(catalog),'maxWorkers':max_workers,'frontdoorDispatch':True,'requireCloudAuthority':True})
55
+ return {'state':'three-coordinator-dispatch-prepared','nativeCoordinators':3,'specializationReferences':len(refs),'specialistNativeChatsCreated':0,'instructionsCached':False,'cloudAuthorityRequired':True,'nativeActivationVerified':False}
56
+
57
+ def configuration(owner,saved,channel):
58
+ require(set(saved)=={'format','pack','catalogDigest','maxWorkers','frontdoorDispatch','requireCloudAuthority'} and saved['frontdoorDispatch'] is True and saved['requireCloudAuthority'] is True,'Unqualified source-independent task configuration')
59
+ require(channel in COORDINATORS,'Unknown task channel')
60
+ root=owner.lib.root/'work';catalog=read_json(root/'role-catalog.json')
61
+ require(catalog.get('format')=='topolo-task-role-catalog-v1' and catalog.get('pack')==saved['pack'] and digest(catalog)==saved['catalogDigest'],'Specialization catalog changed; review before dispatch')
62
+ registry=read_json(root/'role-bindings.json');require(registry.get('organizationId')=='org_topolo_platform' and registry.get('roles')==catalog['bindings'],'Role reference registry differs from the reviewed catalog')
63
+ native,revision=owner.lib.load(saved['pack']);require(revision==catalog['nativePackRevision'] and len(native['agents'])==3,'Native coordinator roster changed')
64
+ selected=catalog['channels'][channel];expected=saved['pack']+'/intakes/'+selected['intakeRole']
65
+ matches=[c for c in owner.lib.active()['components'] if c['key']==expected]
66
+ require(len(matches)==1 and owner.execution.roles.get(matches[0]['threadId'],{}).get('key')==expected,'Channel has no current owned coordinator')
67
+ require(type(saved['maxWorkers']) is int and 1<=saved['maxWorkers']<=8,'Invalid native capacity')
68
+ manifest={'packId':saved['pack'],'sourceIndependent':True,'agents':[r for r in catalog['roles'] if component_key(r) in selected['roleKeys']]}
69
+ return {'pack':saved['pack'],'revision':revision,'maxWorkers':saved['maxWorkers'],'catalogOnly':True,**selected},manifest,{'configuration':catalog['configuration'],'constraints':catalog['constraints']}
70
+
71
+
72
+ def repair_model_ceilings(lib,expected_catalog_digest,expected_native,client=None,native_call=None,live_request=None):
73
+ with Library(lib.root).lock():
74
+ return _repair_model_ceilings_locked(lib,expected_catalog_digest,expected_native,client,native_call,live_request)
75
+
76
+ def _repair_model_ceilings_locked(lib,expected_catalog_digest,expected_native,client,native_call,live_request):
77
+ """Add only the three already-configured Claude IDs to stale GPT-only ceilings.
78
+
79
+ No task, role definition, native session, permission or model preference is
80
+ changed. A digest mismatch fences dispatch throughout metadata publication.
81
+ """
82
+ from .claude_desktop import call
83
+ from .live import request
84
+ from .cloud_work import CloudStore
85
+ native_call=native_call or call;live_request=live_request or request
86
+ root=lib.root/'work';journal=root/'model-ceiling-repair.json'
87
+ require(not journal.exists() and not journal.is_symlink(),'Existing model ceiling repair requires reconciliation; no replay')
88
+ saved=read_json(root/'config.json');catalog=read_json(root/'role-catalog.json');registry=read_json(root/'role-bindings.json')
89
+ require(saved.get('format')=='topolo-task-dispatch-v2' and saved.get('requireCloudAuthority') is True and saved.get('catalogDigest')==expected_catalog_digest==digest(catalog) and catalog.get('format')=='topolo-task-role-catalog-v1' and catalog.get('pack')==saved.get('pack') and registry.get('roles')==catalog.get('bindings'),'Expected source-independent catalog beforeimage changed')
90
+ native_pack,revision=lib.load(saved['pack'])
91
+ require(revision==catalog['nativePackRevision'] and len(native_pack['agents'])==3,'Original three-coordinator pack changed')
92
+ connection=read_json(root/'claude-frontdoors.json')['nativeConnection']
93
+ require(isinstance(expected_native,dict) and set(expected_native)=={'harness','pid','identity','models','workers','codeIdentity','qualified'} and expected_native['harness']=='claude-desktop' and expected_native['qualified'] is True and expected_native['workers']==0 and type(expected_native['pid']) is int and expected_native['pid']>1 and expected_native['pid']==connection['pid'] and expected_native['identity']==connection['identity'] and isinstance(expected_native['models'],list) and expected_native['models'] and all(isinstance(v,str) and v for v in expected_native['models']) and len(expected_native['models'])==len(set(expected_native['models'])),'Exact qualified idle native model catalog required')
94
+ configured=catalog['configuration']['harnesses'];claude=configured['claude']['tiers'];codex=set(configured['codex']['tiers'].values())
95
+ require(set(claude)=={'economy','standard','deep'} and len(set(claude.values()))==3 and all(isinstance(v,str) and v.startswith('claude-') and not v.endswith('-latest') and v in expected_native['models'] for v in claude.values()),'Configured three Claude tiers are not in the current native catalog')
96
+ reviewed=copy.deepcopy(catalog);constraints=reviewed['constraints']
97
+ require(set(constraints)=={'pack','groups','roles'} and set(constraints['roles'])==set(catalog['bindings']) and set(constraints['groups'])=={r['set'] for r in catalog['roles']},'Exact catalog role/group constraint coverage required')
98
+ for layer in [constraints['pack'],*constraints['groups'].values(),*constraints['roles'].values()]:
99
+ require(isinstance(layer,dict) and set(layer)=={'allowedModels','allowedEfforts'} and set(layer['allowedModels'])==codex and len(layer['allowedModels'])==len(codex) and layer['allowedEfforts']==['low','medium','high'],'Only unchanged stale GPT-only model ceilings may be repaired')
100
+ layer['allowedModels']=sorted(codex|set(claude.values()))
101
+ require(digest(reviewed)!=expected_catalog_digest,'Catalog has no stale model ceilings to repair')
102
+ if client is None:
103
+ from .topolo import Client
104
+ client=Client('production','org:org_topolo_platform')
105
+ store=CloudStore(lib.root,client=client)
106
+ def quiescent():
107
+ require(native_call(connection,{'kind':'status'})==expected_native,'Current native model catalog or idle connection changed')
108
+ status=live_request(lib,'status',{})
109
+ require(status.get('harness')=='claude' and status.get('cloudDispatchReady') is True and status.get('desktopPid')==expected_native['pid'] and status.get('activeTurns')==[],'Owning Claude dispatcher is not quiescent')
110
+ version,state=store.load()
111
+ for row in state['tables']['workers']:
112
+ w=json.loads(row[2])
113
+ require(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),'Existing cloud worker custody is unresolved')
114
+ for row in state['tables']['tasks']:
115
+ t=json.loads(row[2])
116
+ require(t.get('ownerInstallationId')!=store.binding['installationId'] or t['state'] not in {'queued','routing','executing','reviewing'},'Runnable owned cloud work must settle before model ceiling repair')
117
+ return {'versionId':version,'stateSHA256':digest(state),'datasetId':state['datasetId']}
118
+ cloud=quiescent()
119
+ held={**saved,'catalogDigest':'0'*64};after={**saved,'catalogDigest':digest(reviewed)}
120
+ record={'format':'topolo-model-ceiling-repair-v1','state':'prepared','harness':'claude','native':expected_native,'cloud':cloud,'before':{'config':saved,'catalog':catalog},'after':{'config':after,'catalog':reviewed},'addedModels':sorted(claude.values()),'cloudTaskStateChanged':False,'nativeSessionsCreated':0}
121
+ require(read_json(root/'config.json')==saved and read_json(root/'role-catalog.json')==catalog and read_json(root/'role-bindings.json')==registry,'Catalog metadata changed before publication')
122
+ atomic_json(journal,record)
123
+ try:
124
+ atomic_json(root/'config.json',held)
125
+ atomic_json(root/'role-catalog.json',reviewed)
126
+ require(quiescent()==cloud,'Protected cloud/native beforeimage changed during repair')
127
+ require(read_json(root/'config.json')==held and read_json(root/'role-catalog.json')==reviewed and read_json(root/'role-bindings.json')==registry,'Protected catalog metadata changed during repair')
128
+ atomic_json(root/'config.json',after)
129
+ require(read_json(root/'config.json')==after and read_json(root/'role-catalog.json')==reviewed,'Model ceiling publication readback differs')
130
+ atomic_json(journal,{**record,'state':'verified'})
131
+ except Exception:
132
+ # Retain a digest fence after partial publication; never roll back cloud
133
+ # work, rewrite a foreign catalog, or silently resume from an old copy.
134
+ if read_json(root/'config.json') in (held,after):
135
+ atomic_json(root/'config.json',held)
136
+ raise
137
+ return {'state':'reviewed-model-ceilings-repaired','catalogDigest':after['catalogDigest'],'addedModels':sorted(claude.values()),'roles':len(constraints['roles']),'cloudTaskStateChanged':False,'nativeSessionsCreated':0,'existingRoleSettingsChanged':False}
@@ -0,0 +1,63 @@
1
+ """Fetch exactly one Agent specialization. Never load a pack export or memory."""
2
+ import hashlib,json,pathlib
3
+ from .storage import read_json,atomic_json,canonical
4
+ from .work import require
5
+ from .domain_context import references,materialize as materialize_domain
6
+
7
+ def materialize(root,key,cwd,client=None,*,harness='codex'):
8
+ require(harness in {'codex','claude'},'Unsupported role instruction harness')
9
+ require(read_json(pathlib.Path(root)/'role-refresh.json',{}).get('state') in {None,'verified'},'Role metadata refresh is incomplete; reconcile before native startup')
10
+ registry=read_json(pathlib.Path(root)/'role-bindings.json')
11
+ require(registry.get('format')=='topolo-role-bindings-v1' and registry.get('organizationId')=='org_topolo_platform','Unqualified Agent role bindings')
12
+ ref=registry['roles'].get(key)
13
+ require(isinstance(ref,dict) and set(ref) in [{'agentId','workspaceId','logicalId','instructionsSHA256'},{'agentId','workspaceId','logicalId','instructionsSHA256','domainContextRefs'}],'Selected role has no qualified Agent definition')
14
+ if client is None:
15
+ from .topolo import Client
16
+ client=Client('production','org:org_topolo_platform')
17
+ result=client.action('app_topolo_agent.agents.get',{'id':ref['agentId']},ref['workspaceId'])
18
+ agent=result['agent']
19
+ require(agent['id']==ref['agentId'] and agent['workspaceId']==ref['workspaceId'] and agent['organizationId']==registry['organizationId'],'Agent specialization identity differs')
20
+ config=json.loads(agent['config']) if isinstance(agent['config'],str) else agent['config']
21
+ spec=config.get('specialization',{})
22
+ require(set(spec)=={'format','logicalId','instructions'} and spec['format']=='topolo-role-specialization-v1' and spec['logicalId']==ref['logicalId'],'Agent specialization is missing or belongs to another role')
23
+ instructions=spec['instructions']
24
+ require(isinstance(instructions,str) and 100<=len(instructions)<=16000 and config.get('purpose')==instructions,'Agent specialization body is unqualified')
25
+ digest=hashlib.sha256(instructions.encode()).hexdigest()
26
+ require(digest==ref['instructionsSHA256'],'Agent role changed; review its new definition before dispatch')
27
+ refs=references(config,spec['logicalId'])
28
+ require(refs==ref.get('domainContextRefs',[]),'Reviewed operating reference binding changed')
29
+ cwd=pathlib.Path(cwd);require(cwd.is_dir() and not cwd.is_symlink(),'New worker directory required')
30
+ filename='CLAUDE.md' if harness=='claude' else 'AGENTS.md'
31
+ target=cwd/filename
32
+ require(not target.is_symlink(),'Role instruction file cannot be a symlink')
33
+ domain=materialize_domain(refs,cwd,client)
34
+ target.write_text(instructions+'\n')
35
+ receipt={'source':'topolo-agent-definition','agentId':agent['id'],'workspaceId':agent['workspaceId'],'logicalId':spec['logicalId'],'instructionsSHA256':digest,'instructionBytes':len(instructions.encode()),'action':'app_topolo_agent.agents.get','rolesLoaded':1,'wholePackExportLoaded':False,'memoryLoaded':False}
36
+ if domain:receipt['domainContextRefs']=domain
37
+ atomic_json(cwd/'role-source.json',receipt)
38
+ return receipt
39
+
40
+ def bind(root,manifests,workspace,client=None):
41
+ """Installation discovery only; registry contains references, never bodies."""
42
+ if client is None:
43
+ from .topolo import Client
44
+ client=Client('production','org:org_topolo_platform')
45
+ agents=client.action('app_topolo_agent.agents.list',{},workspace)['agents']
46
+ roles={}
47
+ for manifest in manifests:
48
+ for role in manifest['agents']:
49
+ logical=role['set']+'/'+role['id'];matches=[]
50
+ for agent in agents:
51
+ config=json.loads(agent['config']) if isinstance(agent['config'],str) else agent['config']
52
+ spec=config.get('specialization',{})
53
+ if spec.get('format')=='topolo-role-specialization-v1' and spec.get('logicalId')==logical:
54
+ require(agent['organizationId']=='org_topolo_platform' and agent['workspaceId']==workspace and config.get('purpose')==spec.get('instructions'),'Agent specialization discovery differs')
55
+ matches.append((agent,spec))
56
+ require(len(matches)==1,'Missing or ambiguous Agent specialization: '+logical)
57
+ agent,spec=matches[0];key=role['pack']+'/'+logical
58
+ roles[key]={'agentId':agent['id'],'workspaceId':workspace,'logicalId':logical,'instructionsSHA256':hashlib.sha256(spec['instructions'].encode()).hexdigest()}
59
+ refs=references(json.loads(agent['config']) if isinstance(agent['config'],str) else agent['config'],logical)
60
+ if refs:roles[key]['domainContextRefs']=refs
61
+ registry={'format':'topolo-role-bindings-v1','organizationId':'org_topolo_platform','roles':roles}
62
+ atomic_json(pathlib.Path(root)/'role-bindings.json',registry)
63
+ return {'state':'agent-role-references-bound','roles':len(roles),'instructionsCached':False,'dispatchLookup':'one Agent id per selected worker'}
@@ -0,0 +1,50 @@
1
+ """Explicit reviewed metadata refresh for the same inactive cloud installation."""
2
+ import copy,hashlib,json,os,pathlib
3
+ from .storage import read_json,atomic_json,canonical
4
+ from .work import require
5
+ from .domain_context import references
6
+
7
+
8
+ def refresh_refs(lib,expected_catalog_digest,expected_bindings,reviewed_bindings,client=None):
9
+ root=lib.root/'work';journal=root/'role-refresh.json'
10
+ require(not journal.exists(),'Role metadata refresh already recorded; reconcile its existing journal')
11
+ saved=read_json(root/'config.json');catalog=read_json(root/'role-catalog.json');registry=read_json(root/'role-bindings.json')
12
+ digest=lambda value:hashlib.sha256(canonical(value)).hexdigest()
13
+ require(saved.get('catalogDigest')==expected_catalog_digest==digest(catalog) and registry.get('roles')==catalog.get('bindings')==expected_bindings,'Expected role metadata changed')
14
+ require(not lib.active().get('components'),'Native owner must be stopped before role metadata refresh')
15
+ for record,field in [(root/'claude-frontdoors.json','desktopPid'),(lib.root/'live-bridge.json','pid')]:
16
+ owner=read_json(record,{})
17
+ if owner:
18
+ pid=owner.get(field);require(type(pid) is int and pid>1,'Native owner identity is unverified')
19
+ try:os.kill(pid,0)
20
+ except ProcessLookupError:pass
21
+ else:require(False,'Native owner must be stopped before role metadata refresh')
22
+ if client is None:
23
+ from .topolo import Client
24
+ client=Client('production','org:org_topolo_platform')
25
+ binding=read_json(root/'cloud.json')
26
+ require(binding.get('format')=='topolo-cloud-work-binding-v1' and binding.get('organizationId')=='org_topolo_platform','Inactive cloud installation binding required')
27
+ response=client.action('app_topolo_docs.documents.get',{'documentId':binding['documentId']},binding['workspaceId']);doc=response['document'];versions=[v for v in response['versions'] if v['id']==doc['currentVersionId']]
28
+ require(doc['id']==binding['documentId'] and doc['workspaceId']==binding['workspaceId'] and doc['organizationId']==binding['organizationId'] and len(versions)==1,'Inactive cloud installation identity differs')
29
+ state=json.loads(versions[0]['bodyMarkdown'])
30
+ require(state.get('format')=='topolo-cloud-work-state-v1' and state.get('datasetId')==binding['datasetId'] and state.get('activated') is False,'Role refresh requires the existing inactive prepared dataset')
31
+ for row in state['tables']['workers']:
32
+ worker=json.loads(row[2])
33
+ 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),'Existing native worker closure is unresolved before role refresh')
34
+ required={key for key,ref in expected_bindings.items() if ref['logicalId']=='intakes/cicd' or ref['logicalId'] in {'specialists/'+v for v in ['queue-recovery','runner-capacity','pipeline-engineering','promotion-deployment','release-verification']}}
35
+ require(len(required)==6 and isinstance(reviewed_bindings,dict) and set(reviewed_bindings)==required,'Exactly the six existing CI/CD roles must be reviewed')
36
+ updated=copy.deepcopy(expected_bindings)
37
+ for key,ref in reviewed_bindings.items():
38
+ previous=expected_bindings[key]
39
+ require(set(ref)=={'agentId','workspaceId','logicalId','instructionsSHA256','domainContextRefs'} and all(ref[field]==previous[field] for field in ['agentId','workspaceId','logicalId']),'Reviewed role identity changed')
40
+ agent=client.action('app_topolo_agent.agents.get',{'id':ref['agentId']},ref['workspaceId'])['agent'];config=json.loads(agent['config']) if isinstance(agent['config'],str) else agent['config'];spec=config.get('specialization',{})
41
+ require(agent['id']==ref['agentId'] and agent['workspaceId']==ref['workspaceId'] and agent['organizationId']=='org_topolo_platform' and spec.get('format')=='topolo-role-specialization-v1' and spec.get('logicalId')==ref['logicalId'] and isinstance(spec.get('instructions'),str) and config.get('purpose')==spec['instructions'] and hashlib.sha256(spec['instructions'].encode()).hexdigest()==ref['instructionsSHA256'] and references(config,ref['logicalId'])==ref['domainContextRefs'],'Reviewed live Agent definition differs')
42
+ updated[key]=copy.deepcopy(ref)
43
+ new_catalog={**catalog,'bindings':updated};new_registry={**registry,'roles':updated};new_saved={**saved,'catalogDigest':digest(new_catalog)}
44
+ record={'format':'topolo-role-refresh-v1','state':'prepared','datasetId':binding['datasetId'],'cloudVersionId':doc['currentVersionId'],'oldCatalogDigest':expected_catalog_digest,'newCatalogDigest':new_saved['catalogDigest'],'before':{'catalog':catalog,'registry':registry,'config':saved},'after':{'catalog':new_catalog,'registry':new_registry,'config':new_saved}}
45
+ atomic_json(journal,record)
46
+ # Partial writes remain a visible administrative hold, never a fallback.
47
+ atomic_json(root/'role-catalog.json',new_catalog);atomic_json(root/'role-bindings.json',new_registry);atomic_json(root/'config.json',new_saved)
48
+ require(read_json(root/'role-catalog.json')==new_catalog and read_json(root/'role-bindings.json')==new_registry and read_json(root/'config.json')==new_saved,'Role metadata refresh readback differs')
49
+ atomic_json(journal,{**record,'state':'verified'})
50
+ return {'state':'reviewed-role-metadata-refreshed','roles':6,'catalogDigest':new_saved['catalogDigest'],'datasetId':binding['datasetId'],'cloudTaskStateChanged':False,'nativeOwnersStarted':False}
@@ -16,23 +16,27 @@ def digest(value):
16
16
  def default_configuration():
17
17
  """Explicit template for a new pack configuration; existing roles are unchanged."""
18
18
  return {
19
- 'format': 'topolo-task-model-configuration', 'schemaVersion': 1, 'revision': '1',
19
+ 'format': 'topolo-task-model-configuration', 'schemaVersion': 1, 'revision': '2',
20
20
  'rules': [
21
- {'id': 'routine', 'complexity': ['routine'], 'risk': ['low'], 'profile': 'routine'},
21
+ {'id': 'trivial', 'complexity': ['trivial'], 'risk': ['low'], 'profile': 'trivial',
22
+ 'types': ['format', 'extract-fields', 'reference-lookup', 'status-read']},
23
+ {'id': 'routine', 'complexity': ['routine'], 'risk': ['low', 'normal'], 'profile': 'routine'},
22
24
  {'id': 'deep', 'complexity': ['complex'], 'risk': ['low', 'normal', 'security-critical'], 'profile': 'deep'},
23
25
  {'id': 'security', 'complexity': ['routine', 'standard'], 'risk': ['security-critical'], 'profile': 'deep'},
24
26
  {'id': 'standard', 'complexity': ['standard'], 'risk': ['low', 'normal'], 'profile': 'standard'},
25
27
  ],
26
28
  'harnesses': {
27
29
  'codex': {
28
- 'tiers': {'standard': 'gpt-6.1-sol', 'deep': 'gpt-6-astra'},
29
- 'profiles': {'routine': {'tier': 'standard', 'efforts': ['low', 'medium']},
30
+ 'tiers': {'standard': 'gpt-6.1-sol', 'deep': 'gpt-6-astra', 'economy': 'gpt-6-luna'},
31
+ 'profiles': {'trivial': {'tier': 'economy', 'efforts': ['low']},
32
+ 'routine': {'tier': 'standard', 'efforts': ['low', 'medium']},
30
33
  'standard': {'tier': 'standard', 'efforts': ['medium']},
31
- 'deep': {'tier': 'deep', 'efforts': ['high']}},
34
+ 'deep': {'tier': 'standard', 'efforts': ['high']}},
32
35
  },
33
36
  'claude': {
34
37
  'tiers': {'economy': 'claude-haiku-5-5', 'standard': 'claude-sonnet-5-5', 'deep': 'claude-opus-5-5'},
35
- 'profiles': {'routine': {'tier': 'economy', 'efforts': ['low', 'medium']},
38
+ 'profiles': {'trivial': {'tier': 'economy', 'efforts': ['low']},
39
+ 'routine': {'tier': 'standard', 'efforts': ['low', 'medium']},
36
40
  'standard': {'tier': 'standard', 'efforts': ['medium']},
37
41
  'deep': {'tier': 'deep', 'efforts': ['high']}},
38
42
  },
@@ -80,7 +84,7 @@ def _selection(configuration, task, harness, constraints, *, override_authorized
80
84
  if (not isinstance(task, dict) or not {'type', 'complexity', 'risk'} <= set(task) or
81
85
  set(task) - {'type', 'complexity', 'risk', 'override'} or
82
86
  not isinstance(task['type'], str) or not task['type'] or len(task['type']) > 120 or
83
- task['complexity'] not in {'routine', 'standard', 'complex'} or
87
+ task['complexity'] not in {'trivial', 'routine', 'standard', 'complex'} or
84
88
  task['risk'] not in {'low', 'normal', 'security-critical'}):
85
89
  raise PackError('An intake must supply an exact task type, complexity and risk')
86
90
  rules = configuration.get('rules')