@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,149 @@
1
+ """Bounded allocation decisions, without claim writes, wakeups or ownership changes.
2
+
3
+ Callers supply current canonical and native observations, then atomically claim
4
+ against snapshotDigest before dispatch. A plan is never evidence of execution.
5
+ """
6
+ from .storage import PackError
7
+ from .task_models import digest, resolve
8
+
9
+
10
+ def _require(condition, message):
11
+ if not condition:
12
+ raise PackError(message)
13
+
14
+
15
+ def _keys(value, fields):
16
+ _require(isinstance(value, dict) and set(value) == set(fields.split()),
17
+ 'Allocation input requires exact fields: ' + fields)
18
+
19
+
20
+ def _text(value):
21
+ return isinstance(value, str) and 0 < len(value) <= 240
22
+
23
+
24
+ def _integer(value, minimum, maximum):
25
+ return type(value) is int and minimum <= value <= maximum
26
+
27
+
28
+ def _names(value):
29
+ return (isinstance(value, list) and len(value) <= 256 and
30
+ all(_text(item) for item in value) and len(value) == len(set(value)))
31
+
32
+
33
+ def allocate(snapshot):
34
+ """Choose compatible idle workers fairly; isolate stale/blocked work.
35
+
36
+ Observations are supplied evidence, not a native status probe. Existing
37
+ owners stay pinned. Retained custody is reusable only at an explicit safe
38
+ checkpoint with the actual native worker idle. Resource slots are separate
39
+ from chat availability and apply to both existing and proposed workers.
40
+ """
41
+ _keys(snapshot, 'schemaVersion now maxAgeSeconds slots maxNewLanes tasks lanes completed configuration catalog')
42
+ _require(snapshot['schemaVersion'] == 1 and
43
+ _integer(snapshot['now'], 1, 2**53) and
44
+ _integer(snapshot['maxAgeSeconds'], 1, 3600) and
45
+ _integer(snapshot['slots'], 0, 32) and
46
+ _integer(snapshot['maxNewLanes'], 0, 8) and
47
+ _names(snapshot['completed']), 'Invalid allocation bounds')
48
+ tasks, lanes = snapshot['tasks'], snapshot['lanes']
49
+ _require(isinstance(tasks, list) and len(tasks) <= 256 and
50
+ isinstance(lanes, list) and len(lanes) <= 256, 'Bounded task and lane lists required')
51
+ task_ids, lane_ids = set(), set()
52
+ for task in tasks:
53
+ _keys(task, 'id capability priority owner dependencies authorized paused modelTask')
54
+ _require(_text(task['id']) and task['id'] not in task_ids and
55
+ _text(task['capability']) and _integer(task['priority'], 0, 9) and
56
+ (task['owner'] is None or _text(task['owner'])) and
57
+ _names(task['dependencies']) and task['id'] not in task['dependencies'] and
58
+ type(task['authorized']) is bool and type(task['paused']) is bool,
59
+ 'Malformed or duplicate allocation task')
60
+ task_ids.add(task['id'])
61
+ for lane in lanes:
62
+ _keys(lane, 'id capabilities observedAt registryState nativeState activeTask retainedTasks reusableCheckpoint assignments lastAssigned constraints')
63
+ _require(_text(lane['id']) and lane['id'] not in lane_ids and
64
+ _names(lane['capabilities']) and _names(lane['retainedTasks']) and
65
+ _integer(lane['observedAt'], 1, 2**53) and
66
+ lane['registryState'] in {'idle', 'busy', 'held', 'paused', 'unknown'} and
67
+ lane['nativeState'] in {'idle', 'busy', 'unknown'} and
68
+ (lane['activeTask'] is None or _text(lane['activeTask'])) and
69
+ type(lane['reusableCheckpoint']) is bool and
70
+ _integer(lane['assignments'], 0, 2**53) and
71
+ _integer(lane['lastAssigned'], 0, 2**53), 'Malformed or duplicate allocation lane')
72
+ lane_ids.add(lane['id'])
73
+ completed = set(snapshot['completed'])
74
+ _require(not completed.intersection(task_ids), 'Completed tasks must not remain in the ready queue')
75
+ result = {'format': 'topolo-allocation-plan', 'schemaVersion': 1,
76
+ 'snapshotDigest': digest(snapshot), 'assignments': [], 'createRequests': [],
77
+ 'blocked': [], 'laneIssues': [], 'effectsStarted': False,
78
+ 'executionRequirements': ['current exclusive claim against snapshotDigest',
79
+ 'native model and effort acknowledgement',
80
+ 'verified authorized task delivery and result readback']}
81
+ eligible = []
82
+ for lane in lanes:
83
+ age = snapshot['now'] - lane['observedAt']
84
+ issue = None
85
+ if age < 0 or age > snapshot['maxAgeSeconds']:
86
+ issue = 'stale-observation'
87
+ elif lane['registryState'] in {'paused', 'unknown'} or lane['nativeState'] == 'unknown':
88
+ issue = 'paused-or-unknown'
89
+ elif lane['activeTask'] is not None or lane['nativeState'] == 'busy':
90
+ issue = 'occupied'
91
+ elif lane['registryState'] == 'busy':
92
+ issue = 'reconcile-registry-with-idle-native'
93
+ elif lane['retainedTasks'] or lane['registryState'] == 'held':
94
+ if not lane['reusableCheckpoint']:
95
+ issue = 'custody-checkpoint-required'
96
+ if issue:
97
+ result['laneIssues'].append({'laneId': lane['id'], 'reason': issue})
98
+ else:
99
+ eligible.append(lane)
100
+ slots = snapshot['slots']
101
+ for task in sorted(tasks, key=lambda row: (row['priority'], row['id'])):
102
+ reason = None
103
+ if task['paused']:
104
+ reason = 'task-paused'
105
+ elif not task['authorized']:
106
+ reason = 'task-unauthorized'
107
+ elif not set(task['dependencies']).issubset(completed):
108
+ reason = 'dependencies-pending'
109
+ elif not slots:
110
+ reason = 'execution-capacity-exhausted'
111
+ if reason:
112
+ result['blocked'].append({'taskId': task['id'], 'reason': reason})
113
+ continue
114
+ compatible = [lane for lane in lanes if task['capability'] in lane['capabilities'] and
115
+ (task['owner'] is None or lane['id'] == task['owner'])]
116
+ candidates = sorted((lane for lane in compatible if lane in eligible),
117
+ key=lambda row: (row['assignments'], row['lastAssigned'], row['id']))
118
+ failures = []
119
+ for lane in candidates:
120
+ try:
121
+ resolution = resolve(snapshot['configuration'], task['modelTask'],
122
+ snapshot['catalog'].get('harness'), snapshot['catalog'], lane['constraints'])
123
+ except (PackError, AttributeError) as error:
124
+ failures.append({'laneId': lane['id'], 'reason': str(error)})
125
+ continue
126
+ result['assignments'].append({'taskId': task['id'], 'laneId': lane['id'],
127
+ 'resolution': resolution,
128
+ 'retainedTasks': list(lane['retainedTasks'])})
129
+ eligible.remove(lane)
130
+ slots -= 1
131
+ break
132
+ else:
133
+ uncertain = any(issue['laneId'] in {lane['id'] for lane in compatible} and
134
+ issue['reason'] in {'stale-observation', 'paused-or-unknown',
135
+ 'reconcile-registry-with-idle-native'}
136
+ for issue in result['laneIssues'])
137
+ if (task['owner'] is None and not candidates and not uncertain and
138
+ len(result['createRequests']) < snapshot['maxNewLanes']):
139
+ result['createRequests'].append({'taskId': task['id'], 'capability': task['capability'],
140
+ 'reason': 'no-compatible-free-lane',
141
+ 'requires': 'registered native lane and verified task model before claim'})
142
+ slots -= 1
143
+ else:
144
+ result['blocked'].append({'taskId': task['id'],
145
+ 'reason': ('model-unavailable' if failures else
146
+ 'lane-state-reconciliation-required' if uncertain else
147
+ 'owner-or-capability-unavailable'),
148
+ 'modelFailures': failures})
149
+ return result
@@ -51,7 +51,7 @@ class Client:
51
51
  if any(selected.get(k) for k in ['TOPOLO_API_KEY','TOPOLO_ACCESS_TOKEN','TOPOLO_'+environment.upper()+'_API_KEY','TOPOLO_'+environment.upper()+'_ACCESS_TOKEN']):
52
52
  raise PackError('Pinned connections require the CLI protected account context, not injected credentials')
53
53
  self.environment=environment;self.context=context
54
- self.binary=binary or shutil.which('topolo')
54
+ self.binary=binary or shutil.which('topolo') or next((str(p) for p in [pathlib.Path('/opt/homebrew/bin/topolo'),pathlib.Path('/usr/local/bin/topolo')] if p.is_file() and os.access(p,os.X_OK)),None)
55
55
  if not self.binary:
56
56
  raise PackError('Install the public @topolo/cli and authenticate before connecting')
57
57
  self.env={**selected,'TOPOLO_CONTEXT':context}
@@ -81,6 +81,12 @@ class Client:
81
81
  if upload:
82
82
  result=self.run(['actions','upload',*common,'--file',str(upload),'--content-type',input.get('type','application/octet-stream'),'--confirm'])
83
83
  else:result=self.run(['actions','call',*common,'--detail',*(['--confirm'] if write else [])])
84
+ # Public CLI action calls wrap the full service envelope in result.
85
+ # Do not mistake that transport wrapper for the actual cloud record.
86
+ if isinstance(result,dict) and 'actionId' in result and 'result' in result:
87
+ if result['actionId']!=action:
88
+ raise PackError('Topolo action response identity mismatch')
89
+ result=result['result']
84
90
  if isinstance(result,dict) and result.get('success') is False:
85
91
  raise PackError('Topolo action failed')
86
92
  return result.get('data',result) if isinstance(result,dict) else result
@@ -0,0 +1,322 @@
1
+ """Durable task records and single-use native workers; no model or transport mocks.
2
+
3
+ The SQLite record owns continuity. Native threads are disposable execution slots.
4
+ A reserved/uncertain send is never replayed by process recovery.
5
+ """
6
+ import copy
7
+ import hashlib
8
+ import json
9
+ import os
10
+ import pathlib
11
+ import re
12
+ import sqlite3
13
+ import time
14
+ import uuid
15
+ from contextlib import contextmanager
16
+ from .storage import PackError, canonical
17
+
18
+ STATES={'queued','routing','executing','reviewing','completed','needs_input','blocked','failed','unknown'}
19
+ TERMINAL={'completed','needs_input','blocked','failed','unknown'}
20
+
21
+ def require(ok,message):
22
+ if not ok:raise PackError(message)
23
+
24
+ def text(value,limit=16000):
25
+ return isinstance(value,str) and 0<len(value.strip())<=limit
26
+
27
+ class Store:
28
+ def __init__(self,root):
29
+ self.root=pathlib.Path(root)/'work';self.root.mkdir(mode=0o700,exist_ok=True)
30
+ require(not self.root.is_symlink() and self.root.stat().st_uid==os.getuid() and not self.root.stat().st_mode&0o022,'Work store must be privately owned')
31
+ self.path=self.root/'tasks.sqlite'
32
+ require(not self.path.is_symlink(),'Work database cannot be a symlink')
33
+ with self.db() as db:
34
+ db.executescript('''CREATE TABLE IF NOT EXISTS tasks(id TEXT PRIMARY KEY, input_hash TEXT NOT NULL, body TEXT NOT NULL);
35
+ CREATE TABLE IF NOT EXISTS workers(id TEXT PRIMARY KEY,task_id TEXT NOT NULL,body TEXT NOT NULL);
36
+ CREATE TABLE IF NOT EXISTS events(sequence INTEGER PRIMARY KEY AUTOINCREMENT, task_id TEXT NOT NULL, body TEXT NOT NULL);
37
+ CREATE TABLE IF NOT EXISTS retained(id TEXT PRIMARY KEY,body TEXT NOT NULL);''')
38
+ db.execute('CREATE TABLE IF NOT EXISTS responses(id TEXT PRIMARY KEY,input_hash TEXT NOT NULL,task_id TEXT NOT NULL)')
39
+ self.path.chmod(0o600)
40
+ @contextmanager
41
+ def db(self):
42
+ require(not (self.root/'cutover.json').exists(),'Legacy work is durably fenced for cloud cutover')
43
+ db=sqlite3.connect(self.path,timeout=10)
44
+ try:
45
+ db.execute('BEGIN IMMEDIATE')
46
+ require(not (self.root/'cutover.json').exists(),'Legacy work is durably fenced for cloud cutover')
47
+ yield db;db.commit()
48
+ except BaseException:db.rollback();raise
49
+ finally:db.close()
50
+ def event(self,db,tid,kind,summary):
51
+ db.execute('INSERT INTO events(task_id,body) VALUES (?,?)',(tid,json.dumps({'taskId':tid,'kind':kind,'summary':summary,'at':time.time()})))
52
+ def submit(self,request):
53
+ require(isinstance(request,dict) and set(request)=={'requestId','objective','channel','canonicalClaimId'},'Exact work request fields required')
54
+ try:valid=str(uuid.UUID(request['requestId']))==request['requestId']
55
+ except (ValueError,TypeError,AttributeError):valid=False
56
+ require(valid and text(request['objective']) and request['channel'] in {'engineering','operations','cicd'} and (request['canonicalClaimId'] is None or text(request['canonicalClaimId'],240)),'Invalid work request')
57
+ identity=request['requestId'];fingerprint=hashlib.sha256(canonical(request)).hexdigest()
58
+ with self.db() as db:
59
+ row=db.execute('SELECT input_hash,body FROM tasks WHERE id=?',(identity,)).fetchone()
60
+ if row:
61
+ require(row[0]==fingerprint,'Request ID already has a different objective');return json.loads(row[1]),False
62
+ claim=request['canonicalClaimId']
63
+ if claim:
64
+ retained=db.execute('SELECT body FROM retained WHERE id=?',(claim,)).fetchone()
65
+ require(retained is None,'Existing claim retains its owner and checkpoint; per-task adoption is required before dispatch')
66
+ raise PackError('Unknown canonical claim; reconcile its current owner before dispatch')
67
+ task={**copy.deepcopy(request),'id':identity,'revision':1,'attempt':1,'state':'queued','phase':'route','summary':'Request recorded; routing pending.','createdAt':time.time(),'updatedAt':time.time(),'deliveryOwner':'task:'+identity,'workerIds':[],'checkpoints':[]}
68
+ db.execute('INSERT INTO tasks VALUES (?,?,?)',(identity,fingerprint,json.dumps(task)));self.event(db,identity,'requested',task['summary']);return task,True
69
+ def get(self,tid):
70
+ with self.db() as db:
71
+ row=db.execute('SELECT body FROM tasks WHERE id=?',(tid,)).fetchone();require(row is not None,'Unknown task');return json.loads(row[0])
72
+ def adopt_retained(self,request,prepared,receipt,parent_proof=None):
73
+ """Commit a broker-verified receipt and unique continuation atomically.
74
+
75
+ Retained inventory is immutable. The original delivery task, source
76
+ owner, failed jobs and all uncertain effects retain their custody.
77
+ """
78
+ from .work_adoption import validate,digest,request_digest
79
+ validate(request)
80
+ require(prepared.get('requestSHA256')==request_digest(request)
81
+ and receipt.get('planSHA256')==digest(prepared)
82
+ and receipt.get('authority')=='current-authenticated-human-instruction'
83
+ and receipt.get('scope')==prepared.get('scope')=='read-only-checkpoint-diagnostic',
84
+ 'Verified current human diagnostic authorization required')
85
+ identity=request['requestId'];fingerprint=digest(request)
86
+ with self.db() as db:
87
+ row=db.execute('SELECT input_hash,body FROM tasks WHERE id=?',(identity,)).fetchone()
88
+ if row:
89
+ require(row[0]==fingerprint,'Request ID already has a different adoption')
90
+ task=json.loads(row[1])
91
+ require(task.get('adoption',{}).get('receipt')==receipt,'Adoption receipt differs')
92
+ return task,False
93
+ row=db.execute('SELECT body FROM retained WHERE id=?',(request['canonicalClaimId'],)).fetchone()
94
+ require(row is not None and digest(json.loads(row[0]))==prepared['retainedSHA256'],
95
+ 'Retained claim changed; preserve original custody')
96
+ tasks=[json.loads(r[0]) for r in db.execute('SELECT body FROM tasks')]
97
+ require(not any(t.get('canonicalClaimId')==request['canonicalClaimId'] for t in tasks),
98
+ 'Canonical claim already has a continuation; no duplicate dispatch')
99
+ related={t['id'] for t in tasks if t.get('canonicalClaimId')==request['canonicalClaimId']}|{request['continuationOwnerTaskId']}
100
+ workers=[json.loads(r[0]) for r in db.execute('SELECT body FROM workers')]
101
+ require(all(w['state']=='archived' and w.get('nativeArchived') is True for w in workers if w['taskId'] in related),
102
+ 'Current cloud worker retains active or unresolved custody')
103
+ parents=[t for t in tasks if t['id']==request['continuationOwnerTaskId']]
104
+ require(len(parents)==1 and parents[0]['channel']=='engineering'
105
+ and parents[0]['state'] not in {'unknown','completed','failed'},
106
+ 'Originating delivery task is missing, closed or uncertain')
107
+ parent=parents[0]
108
+ require(parent['revision']==request['continuationRevision'],'Parent revision changed; reread before adoption')
109
+ from .work_adoption import parent_heads
110
+ owned_workers=[w for w in workers if w['taskId']==parent['id']]
111
+ require(digest(parent_heads(parent,owned_workers))==request['parentCheckpointSHA256'],'Parent checkpoint heads changed')
112
+ if parent.get('ownerInstallationId')!=receipt['installationId']:
113
+ require(parent_proof is not None and parent['state'] in {'blocked','needs_input'}
114
+ and parent_proof['fromInstallationId']==parent.get('ownerInstallationId')
115
+ and parent_proof['toInstallationId']==receipt['installationId']
116
+ and parent_proof['revision']==parent['revision']
117
+ and parent_proof['checkpointHeadsSHA256']==request['parentCheckpointSHA256'],
118
+ 'Closed parent cloud checkpoint transfer is unverified')
119
+ parent['ownerInstallationId']=receipt['installationId']
120
+ self.event(db,parent['id'],'closed-parent-owner-adopted','Current human authorized portable checkpoint ownership; parent state and holds unchanged.')
121
+ task={k:copy.deepcopy(request[k]) for k in ['requestId','objective','channel','canonicalClaimId']}
122
+ task.update(id=identity,revision=1,attempt=1,state='executing',phase='execute',
123
+ summary='Current human authorized diagnostic-only continuation; original source and UNKNOWN holds preserved.',
124
+ createdAt=time.time(),updatedAt=time.time(),
125
+ deliveryOwner=parents[0]['deliveryOwner'],workerIds=[],checkpoints=[],
126
+ route={'role':request['role'],'task':{'type':'retained-checkpoint-diagnostic',
127
+ 'complexity':'complex','risk':'security-critical'},'acceptance':request['acceptance']},
128
+ adoption={'request':copy.deepcopy(request),'plan':copy.deepcopy(prepared),'receipt':copy.deepcopy(receipt),'parentTransfer':copy.deepcopy(parent_proof)},
129
+ effectBoundary='Read-only checkpoint diagnosis. Source, tests, provider writes and releases require their separate existing gates.')
130
+ db.execute('INSERT INTO tasks VALUES (?,?,?)',(identity,fingerprint,json.dumps(task)))
131
+ parent=parents[0];parent.setdefault('continuations',[]).append({'taskId':identity,'canonicalClaimId':request['canonicalClaimId']})
132
+ parent.update(revision=parent['revision']+1,updatedAt=time.time())
133
+ db.execute('UPDATE tasks SET body=? WHERE id=?',(json.dumps(parent),parent['id']))
134
+ self.event(db,identity,'retained-checkpoint-adopted',task['summary'])
135
+ self.event(db,parent['id'],'continuation-linked','Canonical claim checkpoint continuation linked; delivery acceptance remains open.')
136
+ return task,True
137
+ def list(self):
138
+ with self.db() as db:return [json.loads(x[0]) for x in db.execute('SELECT body FROM tasks ORDER BY rowid DESC LIMIT 500')]
139
+ def update(self,tid,revision,changes,kind):
140
+ require(isinstance(changes,dict) and not(set(changes)-{'state','phase','summary','route','workerIds','checkpoints','result','verification','archiveError','contextRef'}),'Unsupported work transition')
141
+ if 'state' in changes:require(changes['state'] in STATES,'Unknown work state')
142
+ with self.db() as db:
143
+ row=db.execute('SELECT body FROM tasks WHERE id=?',(tid,)).fetchone();require(row is not None,'Unknown task');task=json.loads(row[0]);require(task['revision']==revision,'Task changed; reread before transition')
144
+ task.update(copy.deepcopy(changes));task.update(revision=revision+1,updatedAt=time.time());db.execute('UPDATE tasks SET body=? WHERE id=?',(json.dumps(task),tid));self.event(db,tid,kind,task['summary']);return task
145
+ def re_review(self,request):
146
+ require(isinstance(request,dict) and set(request)=={'requestId','taskId','revision','reviewWorkerId','reviewCheckpoint','note'},'Exact review-only continuation required')
147
+ require(type(request['revision']) is int and text(request['requestId'],100) and text(request['note'],8000),'Review continuation identity and reason required')
148
+ fingerprint=hashlib.sha256(canonical(request)).hexdigest()
149
+ with self.db() as db:
150
+ previous=db.execute('SELECT input_hash,task_id FROM responses WHERE id=?',(request['requestId'],)).fetchone()
151
+ if previous:
152
+ require(previous==(fingerprint,request['taskId']),'Review continuation identity already used')
153
+ return json.loads(db.execute('SELECT body FROM tasks WHERE id=?',(request['taskId'],)).fetchone()[0]),False
154
+ row=db.execute('SELECT body FROM tasks WHERE id=?',(request['taskId'],)).fetchone()
155
+ require(row is not None,'Unknown review task');task=json.loads(row[0])
156
+ require(task['revision']==request['revision'] and task['state'] in {'blocked','needs_input'}
157
+ and task.get('phase')=='review','Task is not the exact rejected review checkpoint')
158
+ workers=[json.loads(w[0]) for w in db.execute('SELECT body FROM workers WHERE task_id=?',(task['id'],))]
159
+ require(all(w['state']=='archived' and w.get('nativeArchived') is True or w['state']=='failed' and not w.get('turnId') and (not w.get('nativeId') or w.get('nativeArchived') is True) for w in workers),'Existing native closure is unresolved; no review continuation')
160
+ rejected=[w for w in workers if w['id']==request['reviewWorkerId']]
161
+ require(len(rejected)==1 and rejected[0]['phase']=='review' and rejected[0]['state']=='archived'
162
+ and rejected[0].get('nativeArchived') is True
163
+ and rejected[0].get('attempt',1)==task.get('attempt',1)
164
+ and rejected[0].get('reviewGeneration',0)==task.get('reviewGeneration',0)
165
+ and rejected[0].get('checkpoint')==request['reviewCheckpoint']
166
+ and task.get('verification',{}).get('nativeCheckpoint')==request['reviewCheckpoint']
167
+ and task['verification']['verdict'] in {'blocked','needs_input'},'Exact latest rejected review must be preserved')
168
+ result=task.get('result',{});execution=[w for w in workers if w['id']==result.get('workerId')]
169
+ require(len(execution)==1 and execution[0]['phase']=='execute' and execution[0]['state']=='archived'
170
+ and execution[0].get('nativeArchived') is True
171
+ and execution[0].get('attempt',1)==task.get('attempt',1)
172
+ and execution[0].get('checkpoint')==result.get('checkpoint') and task.get('route'),
173
+ 'Existing exact execution result is required; execution cannot replay')
174
+ task.setdefault('reviewHistory',[]).append({'reviewWorkerId':request['reviewWorkerId'],
175
+ 'generation':task.get('reviewGeneration',0),'verification':copy.deepcopy(task['verification']),
176
+ 'continuation':copy.deepcopy(request)})
177
+ task.update(state='reviewing',phase='review',reviewGeneration=task.get('reviewGeneration',0)+1,
178
+ revision=task['revision']+1,updatedAt=time.time(),
179
+ summary='Review-only continuation authorized; original execution and rejected review retained.')
180
+ db.execute('UPDATE tasks SET body=? WHERE id=?',(json.dumps(task),task['id']))
181
+ db.execute('INSERT INTO responses VALUES (?,?,?)',(request['requestId'],fingerprint,task['id']))
182
+ self.event(db,task['id'],'review-continuation-requested',task['summary'])
183
+ return task,True
184
+
185
+ def reserve(self,tid,phase,role,resolution,capacity):
186
+ require(phase in {'route','execute','review'} and type(capacity) is int and 1<=capacity<=16,'Invalid worker reservation')
187
+ with self.db() as db:
188
+ rows=[json.loads(r[0]) for r in db.execute('SELECT body FROM workers')]
189
+ require(sum(w['state'] in {'reserved','creating','running','archiving','unknown'} for w in rows)<capacity,'Execution capacity occupied')
190
+ task=json.loads(db.execute('SELECT body FROM tasks WHERE id=?',(tid,)).fetchone()[0]);attempt=task.get('attempt',1)
191
+ require(not any(w['taskId']==tid and w['phase']==phase and w.get('attempt',1)==attempt and (phase!='review' or w.get('reviewGeneration',0)==task.get('reviewGeneration',0)) for w in rows),'A task phase already has a worker; never reuse or replay it')
192
+ wid=str(uuid.uuid4());worker={'id':wid,'taskId':tid,'phase':phase,'attempt':attempt,'role':role,'resolution':copy.deepcopy(resolution),'state':'reserved','nativeId':None,'turnId':None,'createdAt':time.time()}
193
+ if phase=='review':worker['reviewGeneration']=task.get('reviewGeneration',0)
194
+ db.execute('INSERT INTO workers VALUES (?,?,?)',(wid,tid,json.dumps(worker)));self.event(db,tid,'worker-reserved',phase+' worker reserved');return worker
195
+ def worker(self,wid):
196
+ with self.db() as db:
197
+ row=db.execute('SELECT body FROM workers WHERE id=?',(wid,)).fetchone();require(row is not None,'Unknown worker');return json.loads(row[0])
198
+ def workers(self):
199
+ with self.db() as db:return [json.loads(r[0]) for r in db.execute('SELECT body FROM workers')]
200
+ def worker_update(self,wid,changes):
201
+ require(not(set(changes)-{'state','nativeId','turnId','cwd','checkpoint','error','nativeArchived','roleSource','resolution','nativeRetirement','nativePlan'}),'Unsupported worker transition')
202
+ with self.db() as db:
203
+ row=db.execute('SELECT body FROM workers WHERE id=?',(wid,)).fetchone();require(row is not None,'Unknown worker');worker=json.loads(row[0]);worker.update(copy.deepcopy(changes));db.execute('UPDATE workers SET body=? WHERE id=?',(json.dumps(worker),wid));return worker
204
+ def progress(self,wid,item_id,summary):
205
+ require(text(item_id,200) and text(summary,1600),'Invalid worker progress')
206
+ with self.db() as db:
207
+ row=db.execute('SELECT body FROM workers WHERE id=?',(wid,)).fetchone();require(row is not None,'Unknown worker')
208
+ worker=json.loads(row[0])
209
+ if worker['state']!='running' or item_id in worker.get('progressItems',[]):
210
+ return False
211
+ task=json.loads(db.execute('SELECT body FROM tasks WHERE id=?',(worker['taskId'],)).fetchone()[0])
212
+ if task['state'] in TERMINAL:
213
+ return False
214
+ worker.setdefault('progressItems',[]).append(item_id)
215
+ task.update(summary='Reported progress: '+summary,revision=task['revision']+1,updatedAt=time.time())
216
+ db.execute('UPDATE workers SET body=? WHERE id=?',(json.dumps(worker),wid))
217
+ db.execute('UPDATE tasks SET body=? WHERE id=?',(json.dumps(task),task['id']))
218
+ self.event(db,task['id'],'worker-progress',task['summary'])
219
+ return True
220
+ def adopt_ready(self,tid,revision,installation):
221
+ require(text(installation,200),'Installation identity required')
222
+ with self.db() as db:
223
+ row=db.execute('SELECT body FROM tasks WHERE id=?',(tid,)).fetchone();require(row is not None,'Unknown task');task=json.loads(row[0])
224
+ require(task['revision']==revision,'Task changed; reread before checkpoint adoption')
225
+ require(task.get('resumeAfterCheckpoint') is True and task['state'] in {'queued','executing','reviewing'},'Task is held, uncertain or has no checkpoint-resumption authority')
226
+ workers=[json.loads(w[0]) for w in db.execute('SELECT body FROM workers WHERE task_id=?',(tid,))]
227
+ require(all(w['state']=='archived' and w.get('nativeArchived') is True or w['state']=='failed' and not w.get('turnId') and (not w.get('nativeId') or w.get('nativeArchived') is True) for w in workers),'Existing worker closure is unresolved; preserve its owner')
228
+ phase={'queued':'route','executing':'execute','reviewing':'review'}[task['state']]
229
+ require(not any(w['phase']==phase and w.get('attempt',1)==task.get('attempt',1) and (phase!='review' or w.get('reviewGeneration',0)==task.get('reviewGeneration',0)) for w in workers),'Existing phase result needs reconciliation, not replay')
230
+ if task.get('ownerInstallationId')==installation:
231
+ return task
232
+ previous=task.get('ownerInstallationId');require(isinstance(previous,str) and previous,'Previous installation ownership is missing')
233
+ task.update(ownerInstallationId=installation,revision=revision+1,updatedAt=time.time(),summary='Verified closed checkpoint adopted; only the pending phase may execute.')
234
+ task.setdefault('ownershipCheckpoints',[]).append({'from':previous,'to':installation,'at':task['updatedAt'],'pendingPhase':phase})
235
+ db.execute('UPDATE tasks SET body=? WHERE id=?',(json.dumps(task),tid));self.event(db,tid,'checkpoint-owner-adopted',task['summary']);return task
236
+ def portable_delta(self,tid,revision,source_digest,workers,changes,operation):
237
+ require(set(changes)=={'contextRef','result','verification'} and isinstance(workers,dict) and isinstance(operation,str) and len(operation)==64,'Exact portable delta transition required')
238
+ with self.db() as db:
239
+ row=db.execute('SELECT body FROM tasks WHERE id=?',(tid,)).fetchone();require(row is not None,'Unknown task');task=json.loads(row[0])
240
+ current=[json.loads(w[0]) for w in db.execute('SELECT body FROM workers WHERE task_id=? ORDER BY id',(tid,))]
241
+ require(task['state'] in TERMINAL and set(workers)=={w['id'] for w in current} and all(w['state']=='archived' and w.get('nativeArchived') is True for w in current),'Only verified archived terminal task checkpoints may be converted')
242
+ migration={'operationId':operation,'sourceDigest':source_digest,'nativeIdentities':{w['id']:{k:w.get(k) for k in ['nativeId','turnId','phase']} for w in current}}
243
+ if task.get('portableMigration')==migration:
244
+ require(all(task.get(k)==v for k,v in changes.items()) and all(w['checkpoint']==workers[w['id']] for w in current),'Applied portable delta differs');return task
245
+ require(task['revision']==revision and hashlib.sha256(canonical({'task':task,'workers':current})).hexdigest()==source_digest,'Portable delta source changed; no records overwritten')
246
+ require(isinstance(changes['contextRef'].get('artifact'),dict) and all(isinstance(p.get('artifact'),dict) for p in workers.values()),'Verified cloud references are required')
247
+ for worker in current:
248
+ worker['historicalCheckpoint']=worker['checkpoint'];worker['checkpoint']=copy.deepcopy(workers[worker['id']]);db.execute('UPDATE workers SET body=? WHERE id=?',(json.dumps(worker),worker['id']))
249
+ task.update(copy.deepcopy(changes),portableMigration=migration,revision=revision+1,updatedAt=time.time())
250
+ db.execute('UPDATE tasks SET body=? WHERE id=?',(json.dumps(task),tid));self.event(db,tid,'portable-delta-bound','Existing closed task checkpoints are verified in cloud storage; no work was replayed.');return task
251
+ def retained_import(self,claims,source):
252
+ with self.db() as db:
253
+ for index,claim in enumerate(claims):
254
+ identity=claim.get('claimId') or 'legacy:'+str(index)+':'+str(claim.get('taskId'))
255
+ value={k:claim.get(k) for k in ['claimId','taskId','ownerRole','ownerThreadId','state','worktree','branch']};value.update(source=source,pointer='/claims/'+str(index),disposition='retained_original_owner_no_dispatch')
256
+ db.execute('INSERT INTO retained VALUES (?,?) ON CONFLICT(id) DO UPDATE SET body=excluded.body',(identity,json.dumps(value)))
257
+ return db.execute('SELECT COUNT(*) FROM retained').fetchone()[0]
258
+ def retained(self):
259
+ with self.db() as db:return [json.loads(r[0]) for r in db.execute('SELECT body FROM retained')]
260
+ def retained_page(self,params):
261
+ require(isinstance(params,dict) and not set(params)-{'cursor','limit'},'Unsupported retained inventory fields')
262
+ cursor=params.get('cursor',0);limit=params.get('limit',50)
263
+ require(type(cursor) is int and cursor>=0 and type(limit) is int and 1<=limit<=100,'Invalid retained inventory page')
264
+ with self.db() as db:
265
+ total=db.execute('SELECT COUNT(*) FROM retained').fetchone()[0]
266
+ rows=db.execute('SELECT rowid,body FROM retained WHERE rowid>? ORDER BY rowid LIMIT ?',(cursor,limit+1)).fetchall()
267
+ chosen=[];size=0;last=cursor
268
+ for rowid,body in rows[:limit]:
269
+ value=json.loads(body);count=len(canonical(value))
270
+ if count>800000:
271
+ value={'source':value.get('source'),'pointer':value.get('pointer'),'oversizedRecord':True,'bytes':count,'disposition':'retained_original_owner_no_dispatch'};count=len(canonical(value))
272
+ if size+count>900000 and chosen:break
273
+ chosen.append(value);size+=count;last=rowid
274
+ more=bool(rows and (len(rows)>len(chosen)))
275
+ return {'claims':chosen,'total':total,'nextCursor':last if more else None,'disposition':'inventory_only_no_work_resumed'}
276
+
277
+ def events(self,tid=None):
278
+ with self.db() as db:
279
+ rows=db.execute('SELECT sequence,body FROM events'+(' WHERE task_id=?' if tid else '')+' ORDER BY sequence', (tid,) if tid else ())
280
+ return [{'sequence':r[0],**json.loads(r[1])} for r in rows]
281
+ def recover(self):
282
+ # Native effects may have happened between a send and its receipt. Keep
283
+ # custody and prohibit replay; recovery never starts a thread or turn.
284
+ for worker in self.workers():
285
+ if worker['state'] not in {'archived','failed','unknown'}:
286
+ self.worker_update(worker['id'],{'state':'unknown','error':'Owning connection ended; reconcile native receipt before any replay.'})
287
+ task=self.get(worker['taskId'])
288
+ self.update(task['id'],task['revision'],{'state':'unknown','summary':'Native delivery requires reconciliation; no retry was started.'},'recovery-required')
289
+
290
+ def respond(self,request):
291
+ require(isinstance(request,dict) and set(request)=={'requestId','taskId','revision','intent','note'},'Exact task response required')
292
+ require(text(request['requestId'],100) and text(request['note'],8000) and request['intent'] in {'continue','request_changes','hold'},'Invalid task response')
293
+ fingerprint=hashlib.sha256(canonical(request)).hexdigest()
294
+ with self.db() as db:
295
+ previous=db.execute('SELECT input_hash,task_id FROM responses WHERE id=?',(request['requestId'],)).fetchone()
296
+ if previous:
297
+ require(previous==(fingerprint,request['taskId']),'Response identity already used')
298
+ return json.loads(db.execute('SELECT body FROM tasks WHERE id=?',(request['taskId'],)).fetchone()[0]),False
299
+ row=db.execute('SELECT body FROM tasks WHERE id=?',(request['taskId'],)).fetchone();require(row is not None,'Unknown task');task=json.loads(row[0])
300
+ require(task['revision']==request['revision'],'Task changed; review current progress before responding')
301
+ require(task['state'] in TERMINAL and task['state']!='unknown','Task is active or delivery is uncertain; reconcile before a new attempt')
302
+ workers=[json.loads(w[0]) for w in db.execute('SELECT body FROM workers WHERE task_id=?',(task['id'],))]
303
+ require(all(w['state']=='archived' or (w['state']=='failed' and not w.get('turnId') and (not w.get('nativeId') or w.get('nativeArchived') is True)) for w in workers),'Native worker closure is unresolved; no new attempt')
304
+ task.setdefault('humanResponses',[]).append(copy.deepcopy(request));task['revision']+=1;task['updatedAt']=time.time()
305
+ if request['intent']=='hold':task.update(state='blocked',summary='Held by your request: '+request['note'])
306
+ else:task.update(state='queued',phase='route',attempt=task.get('attempt',1)+1,objective=task['objective']+'\n\nCurrent human response: '+request['note'],summary='Your response is recorded; a fresh specialist attempt will be routed.')
307
+ db.execute('UPDATE tasks SET body=? WHERE id=?',(json.dumps(task),task['id']));db.execute('INSERT INTO responses VALUES (?,?,?)',(request['requestId'],fingerprint,task['id']));self.event(db,task['id'],'human-response',task['summary']);return task,True
308
+
309
+
310
+ def parse_route(text_value,roles):
311
+ raw=text_value.strip()
312
+ if raw.startswith('```'):raw=re.sub(r'^```(?:json)?\s*|\s*```$','',raw)
313
+ try:value=json.loads(raw)
314
+ except (TypeError,ValueError):raise PackError('Coordinator did not return a valid route; no worker started')
315
+ require(isinstance(value,dict) and set(value)=={'role','task','acceptance'},'Coordinator route has unsupported fields')
316
+ require(value['role'] in roles,'Coordinator selected an unknown discipline')
317
+ if isinstance(value['acceptance'],list):
318
+ require(1<=len(value['acceptance'])<=30 and all(text(v,4000) for v in value['acceptance']),'Coordinator acceptance criteria are invalid')
319
+ value['acceptance']='\n'.join(value['acceptance'])
320
+ require(text(value['acceptance'],12000),'Coordinator returned no bounded acceptance criteria')
321
+ require(isinstance(value['task'],dict) and set(value['task'])=={'type','complexity','risk'},'Coordinator must classify type, complexity and risk')
322
+ return value