@arbiterforge/ca-pi 0.15.12 → 0.16.1
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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/plugins/ca-pi/CHANGELOG.md +33 -0
- package/plugins/ca-pi/SKILLS.md +1 -1
- package/plugins/ca-pi/agents/infra-author.md +7 -1
- package/plugins/ca-pi/agents/scout.md +40 -8
- package/plugins/ca-pi/generated/command-catalog.json +1 -1
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-amd64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-arm64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-amd64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-arm64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-amd64.exe +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-arm64.exe +0 -0
- package/plugins/ca-pi/helpers/artifacts/release.json +104 -6
- package/plugins/ca-pi/hooks/_activationlib.py +97 -13
- package/plugins/ca-pi/hooks/_arbiterstatelib.py +7 -2
- package/plugins/ca-pi/hooks/_artifactauthoritylib.py +313 -76
- package/plugins/ca-pi/hooks/_artifactlib.py +166 -2
- package/plugins/ca-pi/hooks/_artifactpromptlib.py +106 -2
- package/plugins/ca-pi/hooks/_bashguardlib.py +14 -0
- package/plugins/ca-pi/hooks/_contextreportlib.py +564 -6
- package/plugins/ca-pi/hooks/_contextselectlib.py +464 -0
- package/plugins/ca-pi/hooks/_contextsnapshotlib.py +999 -0
- package/plugins/ca-pi/hooks/_debughandofflib.py +767 -0
- package/plugins/ca-pi/hooks/_hooklib.py +1 -0
- package/plugins/ca-pi/hooks/_host.py +1 -1
- package/plugins/ca-pi/hooks/_intentlib.py +5 -5
- package/plugins/ca-pi/hooks/_protectedlib.py +3 -3
- package/plugins/ca-pi/hooks/_protectedstatelib.py +152 -2
- package/plugins/ca-pi/hooks/_provenancelib.py +604 -34
- package/plugins/ca-pi/hooks/_readinjectlib.py +131 -1
- package/plugins/ca-pi/hooks/artifact-authority.py +20 -1
- package/plugins/ca-pi/hooks/debug-handoff.py +85 -0
- package/plugins/ca-pi/hooks/doctor.py +4 -2
- package/plugins/ca-pi/hooks/hostapi.py +1 -1
- package/plugins/ca-pi/hooks/init-codearbiter.py +143 -15
- package/plugins/ca-pi/hooks/prompt-submit.py +14 -0
- package/plugins/ca-pi/hooks/session-start.py +26 -7
- package/plugins/ca-pi/includes/author-tdd-workflow.md +16 -4
- package/plugins/ca-pi/includes/helper-invocation.md +128 -0
- package/plugins/ca-pi/includes/pi-host-notes.md +13 -0
- package/plugins/ca-pi/includes/routing-table.md +1 -1
- package/plugins/ca-pi/package.json +1 -1
- package/plugins/ca-pi/routines/INDEX.md +1 -1
- package/plugins/ca-pi/routines/commit-gate/SKILL.md +5 -3
- package/plugins/ca-pi/routines/context-check/SKILL.md +103 -17
- package/plugins/ca-pi/routines/context-creation/SKILL.md +290 -54
- package/plugins/ca-pi/routines/debug/SKILL.md +303 -55
- package/plugins/ca-pi/routines/debug/references/debug-handoff.schema.json +879 -0
- package/plugins/ca-pi/routines/debug/references/handoff.md +155 -0
- package/plugins/ca-pi/routines/decompose/SKILL.md +13 -0
- package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +34 -0
- package/plugins/ca-pi/routines/tdd/SKILL.md +91 -17
- package/plugins/ca-pi/skills/ca-commit/SKILL.md +5 -3
- package/plugins/ca-pi/skills/ca-context-check/SKILL.md +103 -17
- package/plugins/ca-pi/skills/ca-create-context/SKILL.md +290 -54
- package/plugins/ca-pi/skills/ca-debug/SKILL.md +303 -55
- package/plugins/ca-pi/skills/ca-decompose/SKILL.md +13 -0
- package/plugins/ca-pi/skills/ca-feature/SKILL.md +1 -1
- package/plugins/ca-pi/skills/ca-fix/SKILL.md +36 -3
- package/plugins/ca-pi/skills/ca-init/SKILL.md +19 -0
- package/plugins/ca-pi/skills/ca-review/SKILL.md +33 -4
- package/plugins/ca-pi/skills/ca-status/SKILL.md +5 -0
- package/plugins/ca-pi/skills/ca-task/SKILL.md +55 -8
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# codeArbiter -- bounded task-time context selection.
|
|
3
|
+
# The selector is inert: it reads no files and grants no task or action authority.
|
|
4
|
+
# Its inputs are already acquired by the owning actor from the existing map,
|
|
5
|
+
# provenance assessor, command reports, and effective native instructions.
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
import copy
|
|
11
|
+
import json
|
|
12
|
+
import hashlib
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
import _contextreportlib as reports
|
|
16
|
+
import _provenancelib as provenance_lib
|
|
17
|
+
|
|
18
|
+
_MAP_ROW = re.compile(r'^- `([^`]+)`\s+(?:--|—)\s+(.+)$')
|
|
19
|
+
_STATUSES = frozenset({'current', 'unresolved', 'unknown', 'conflict'})
|
|
20
|
+
_WORK_ACTORS = frozenset({'backend-author', 'frontend-author', 'infra-author',
|
|
21
|
+
'coverage-auditor'})
|
|
22
|
+
|
|
23
|
+
class SelectionError(ValueError):
|
|
24
|
+
"""Invalid selector input. Never grants an action bypass."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _path(value):
|
|
28
|
+
try:
|
|
29
|
+
return reports._path(value, 'task_path', root=True)
|
|
30
|
+
except reports.ReportError as exc:
|
|
31
|
+
raise SelectionError(exc.code) from None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _within(path, scope):
|
|
35
|
+
return scope == '.' or path == scope or path.startswith(scope + '/')
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _touches(paths, scope):
|
|
39
|
+
return any(_within(path, scope) or _within(scope, path) for path in paths)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _text(value, field, limit=512):
|
|
43
|
+
try:
|
|
44
|
+
valid = (type(value) is str and bool(value.strip()) and
|
|
45
|
+
len(value.encode('utf-8')) <= limit)
|
|
46
|
+
except UnicodeError:
|
|
47
|
+
valid = False
|
|
48
|
+
if not valid:
|
|
49
|
+
raise SelectionError('INVALID_' + field.upper())
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _map_rows(code_map, paths):
|
|
54
|
+
if code_map is None:
|
|
55
|
+
return [], ['code map unavailable'], False
|
|
56
|
+
if type(code_map) is not str:
|
|
57
|
+
raise SelectionError('INVALID_CODE_MAP')
|
|
58
|
+
try:
|
|
59
|
+
map_size = len(code_map.encode('utf-8'))
|
|
60
|
+
except UnicodeError:
|
|
61
|
+
return [], ['code map is not valid UTF-8 text'], False
|
|
62
|
+
if map_size > 1024 * 1024:
|
|
63
|
+
return [], ['code map input exceeds read cap'], False
|
|
64
|
+
diagnostics = provenance_lib.lint_code_map(code_map)
|
|
65
|
+
rows = []
|
|
66
|
+
for line in code_map.splitlines():
|
|
67
|
+
match = _MAP_ROW.fullmatch(line)
|
|
68
|
+
if match is None:
|
|
69
|
+
continue
|
|
70
|
+
try:
|
|
71
|
+
path = _path(match.group(1).removesuffix('/'))
|
|
72
|
+
except SelectionError:
|
|
73
|
+
continue # The existing linter reports malformed map rows.
|
|
74
|
+
role = match.group(2)
|
|
75
|
+
if len(role.encode('utf-8')) > 512:
|
|
76
|
+
continue
|
|
77
|
+
rows.append({'path': path, 'role': role})
|
|
78
|
+
# A generated output may be the only changed path. Include its named source
|
|
79
|
+
# as orientation, but never infer ownership from equal role text alone.
|
|
80
|
+
selected = [row for row in rows if _touches(paths, row['path'])]
|
|
81
|
+
for row in list(selected):
|
|
82
|
+
marker = 'generated from '
|
|
83
|
+
if marker in row['role'].lower():
|
|
84
|
+
source = row['role'][row['role'].lower().index(marker) + len(marker):].split()[0]
|
|
85
|
+
selected.extend(candidate for candidate in rows
|
|
86
|
+
if candidate['path'] == source and candidate not in selected)
|
|
87
|
+
selected = [row for row in rows if row in selected]
|
|
88
|
+
if (len(selected) > provenance_lib.CODE_MAP_MAX_ENTRIES or
|
|
89
|
+
sum(len(row['path'].encode('utf-8')) +
|
|
90
|
+
len(row['role'].encode('utf-8')) for row in selected) >
|
|
91
|
+
provenance_lib.CODE_MAP_MAX_BYTES):
|
|
92
|
+
return [], diagnostics + ['code map selection exceeds packet cap'], False
|
|
93
|
+
return selected, diagnostics, True
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _provenance_summary(value):
|
|
97
|
+
allowed = frozenset(provenance_lib.V2_DOCUMENTS)
|
|
98
|
+
documents = value.get('documents')
|
|
99
|
+
summary = {'coverage': value.get('coverage') if value.get('coverage') in
|
|
100
|
+
('complete', 'incomplete') else 'incomplete',
|
|
101
|
+
'identity': value.get('identity') if value.get('identity') in
|
|
102
|
+
('current', 'stale', 'unknown') else 'unknown',
|
|
103
|
+
'documents': {}}
|
|
104
|
+
if type(documents) is dict:
|
|
105
|
+
for key in sorted(allowed):
|
|
106
|
+
status = documents.get(key)
|
|
107
|
+
if type(status) is str and status in (
|
|
108
|
+
'v2_current', 'v2_stale', 'v2_unchecked', 'missing',
|
|
109
|
+
'unsupported', 'legacy_unverified', 'store_unreadable',
|
|
110
|
+
'first_input_pending', 'corrupt', 'oversize', 'unreadable',
|
|
111
|
+
'unknown'):
|
|
112
|
+
summary['documents'][key] = status
|
|
113
|
+
return summary
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def select_task_context(task_paths, *, code_map, provenance, commands,
|
|
117
|
+
constraints, effective_instructions, evidence_ids,
|
|
118
|
+
command_bindings=None, observations=None):
|
|
119
|
+
"""Build an inert bounded packet for an actual author or reviewer.
|
|
120
|
+
|
|
121
|
+
The caller acquires the effective instruction order from its host; this
|
|
122
|
+
function only filters that order and cannot confer native precedence. A
|
|
123
|
+
current provenance identity is not semantic approval or write admission.
|
|
124
|
+
Observations/bindings must come from the owning execution collector.
|
|
125
|
+
"""
|
|
126
|
+
if type(task_paths) not in (list, tuple) or not 1 <= len(task_paths) <= 32:
|
|
127
|
+
raise SelectionError('INVALID_TASK_PATHS')
|
|
128
|
+
paths = [_path(path) for path in task_paths]
|
|
129
|
+
if (type(provenance) is not dict or type(commands) is not list or
|
|
130
|
+
type(constraints) is not list or type(effective_instructions) is not list or
|
|
131
|
+
type(evidence_ids) not in (set, frozenset, list, tuple) or
|
|
132
|
+
len(commands) > 64 or len(constraints) > 64 or
|
|
133
|
+
len(effective_instructions) > 64):
|
|
134
|
+
raise SelectionError('INVALID_CONTEXT_INPUT')
|
|
135
|
+
bindings = command_bindings or {}
|
|
136
|
+
observed = observations or {}
|
|
137
|
+
if type(bindings) is not dict or type(observed) is not dict:
|
|
138
|
+
raise SelectionError('INVALID_COMMAND_EVIDENCE')
|
|
139
|
+
map_rows, diagnostics, map_complete = _map_rows(code_map, paths)
|
|
140
|
+
chosen_commands = []
|
|
141
|
+
seen_commands = {}
|
|
142
|
+
for entry in commands:
|
|
143
|
+
if type(entry) is not tuple or len(entry) != 2:
|
|
144
|
+
raise SelectionError('INVALID_COMMAND_ENTRY')
|
|
145
|
+
command, sources = entry
|
|
146
|
+
if type(command) is not dict:
|
|
147
|
+
raise SelectionError('INVALID_COMMAND')
|
|
148
|
+
cwd = _path(command.get('cwd'))
|
|
149
|
+
if not _touches(paths, cwd):
|
|
150
|
+
continue
|
|
151
|
+
command_id = command.get('id')
|
|
152
|
+
key = (command_id, cwd)
|
|
153
|
+
raw = (command, sources)
|
|
154
|
+
if key in seen_commands:
|
|
155
|
+
if seen_commands[key] != raw:
|
|
156
|
+
raise SelectionError('CONFLICTING_COMMAND')
|
|
157
|
+
continue
|
|
158
|
+
seen_commands[key] = raw
|
|
159
|
+
try:
|
|
160
|
+
resolved = reports.resolve_scoped_command_guidance(
|
|
161
|
+
[raw], command_id=command_id, cwd=cwd,
|
|
162
|
+
current_binding=bindings.get(key), observation=observed.get(key))
|
|
163
|
+
except reports.ReportError as exc:
|
|
164
|
+
raise SelectionError(exc.code) from None
|
|
165
|
+
if 'reported_current_state' not in resolved:
|
|
166
|
+
resolved['reported_current_state'] = (
|
|
167
|
+
'declared' if resolved['verification']['state'] == 'declared' else 'unknown')
|
|
168
|
+
chosen_commands.append(resolved)
|
|
169
|
+
chosen_constraints = []
|
|
170
|
+
unresolved = []
|
|
171
|
+
seen_constraints = {}
|
|
172
|
+
for item in constraints:
|
|
173
|
+
if type(item) is not dict or set(item) != {'record', 'owner', 'authority', 'epoch', 'status'}:
|
|
174
|
+
raise SelectionError('INVALID_CONSTRAINT')
|
|
175
|
+
try:
|
|
176
|
+
record = reports.validate_constraint_reference(item['record'],
|
|
177
|
+
evidence_ids=evidence_ids)
|
|
178
|
+
except reports.ReportError as exc:
|
|
179
|
+
raise SelectionError(exc.code) from None
|
|
180
|
+
if not _touches(paths, record['scope']):
|
|
181
|
+
continue
|
|
182
|
+
owner = _path(item['owner']) if item['owner'] != 'root' else 'root'
|
|
183
|
+
authority = _text(item['authority'], 'authority', 128)
|
|
184
|
+
epoch = _text(item['epoch'], 'epoch', 128)
|
|
185
|
+
status = item['status']
|
|
186
|
+
if status not in _STATUSES:
|
|
187
|
+
raise SelectionError('INVALID_CONSTRAINT_STATUS')
|
|
188
|
+
key = (record['id'], owner, record['scope'], authority, epoch)
|
|
189
|
+
selected = {**record, 'owner': owner, 'reported_authority': authority,
|
|
190
|
+
'epoch': epoch, 'status': status}
|
|
191
|
+
if key in seen_constraints:
|
|
192
|
+
if seen_constraints[key] != selected:
|
|
193
|
+
seen_constraints[key]['status'] = 'conflict'
|
|
194
|
+
if seen_constraints[key] not in unresolved:
|
|
195
|
+
unresolved.append(seen_constraints[key])
|
|
196
|
+
continue
|
|
197
|
+
seen_constraints[key] = selected
|
|
198
|
+
chosen_constraints.append(selected)
|
|
199
|
+
if status != 'current':
|
|
200
|
+
unresolved.append(selected)
|
|
201
|
+
chosen_instructions = []
|
|
202
|
+
seen_instructions = {}
|
|
203
|
+
for item in effective_instructions:
|
|
204
|
+
if type(item) is not dict or set(item) != {'id', 'path', 'scope', 'epoch', 'text'}:
|
|
205
|
+
raise SelectionError('INVALID_INSTRUCTION')
|
|
206
|
+
scope, path = _path(item['scope']), _path(item['path'])
|
|
207
|
+
if not _touches(paths, scope):
|
|
208
|
+
continue
|
|
209
|
+
record = {'id': _text(item['id'], 'instruction_id', 128),
|
|
210
|
+
'path': path, 'scope': scope,
|
|
211
|
+
'epoch': _text(item['epoch'], 'epoch', 128),
|
|
212
|
+
'text': _text(item['text'], 'instruction', 8192)}
|
|
213
|
+
key = (record['id'], path, scope, record['epoch'])
|
|
214
|
+
if key in seen_instructions:
|
|
215
|
+
if seen_instructions[key] != record:
|
|
216
|
+
unresolved.append({'code': 'CONFLICTING_INSTRUCTION', 'id': record['id']})
|
|
217
|
+
else:
|
|
218
|
+
chosen_instructions.append(record)
|
|
219
|
+
seen_instructions[key] = record
|
|
220
|
+
if len({item['epoch'] for item in chosen_instructions}) > 1:
|
|
221
|
+
unresolved.append({'code': 'MIXED_INSTRUCTION_EPOCH'})
|
|
222
|
+
if sum(len(item['text'].encode('utf-8')) for item in chosen_instructions) > 24 * 1024:
|
|
223
|
+
for item in chosen_instructions:
|
|
224
|
+
item['text'] = None # Keep every source reference for bounded on-demand reading.
|
|
225
|
+
unresolved.append({'code': 'INSTRUCTION_PACKET_OVERSIZE'})
|
|
226
|
+
provenance_status = _provenance_summary(provenance)
|
|
227
|
+
for doc, status in provenance_status['documents'].items():
|
|
228
|
+
if status == 'v2_stale':
|
|
229
|
+
unresolved.append({'code': 'PROVENANCE_STALE', 'document': doc})
|
|
230
|
+
elif status != 'v2_current':
|
|
231
|
+
unresolved.append({'code': 'PROVENANCE_UNCHECKED', 'document': doc,
|
|
232
|
+
'status': status})
|
|
233
|
+
return {'task_paths': paths, 'map': map_rows, 'map_complete': map_complete,
|
|
234
|
+
'map_diagnostics': diagnostics,
|
|
235
|
+
'provenance': provenance_status, 'commands': chosen_commands,
|
|
236
|
+
'constraints': chosen_constraints, 'instructions': chosen_instructions,
|
|
237
|
+
'unresolved': unresolved, 'admission': 'requires_actor_check'}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def prepare_actor_delivery(packet, *, actor, task_id, worktree, source_epoch,
|
|
241
|
+
host_epoch, attempts=None, receipt=None):
|
|
242
|
+
"""Plan a bounded delivery to one actual actor, without claiming authority.
|
|
243
|
+
|
|
244
|
+
The caller supplies a freshly selected packet and current source/host epochs.
|
|
245
|
+
It resolves the worktree path but reads no source content and keeps no global
|
|
246
|
+
index. A host receipt is a caller-reported observation; admission to work
|
|
247
|
+
remains with the caller.
|
|
248
|
+
Unknown epochs cannot make a saved receipt current and allow at most two
|
|
249
|
+
counted deliveries of the same binding before an explicit gap is returned.
|
|
250
|
+
"""
|
|
251
|
+
if (type(packet) is not dict or packet.get('admission') != 'requires_actor_check'
|
|
252
|
+
or type(packet.get('task_paths')) is not list
|
|
253
|
+
or not packet['task_paths'] or len(packet['task_paths']) > 32
|
|
254
|
+
or type(packet.get('instructions')) is not list):
|
|
255
|
+
raise SelectionError('INVALID_DELIVERY_PACKET')
|
|
256
|
+
paths = [_path(path) for path in packet['task_paths']]
|
|
257
|
+
if paths != packet['task_paths']:
|
|
258
|
+
raise SelectionError('INVALID_DELIVERY_SCOPE')
|
|
259
|
+
for name, value in (('actor', actor), ('task_id', task_id)):
|
|
260
|
+
if (type(value) is not str or len(value) > 128 or
|
|
261
|
+
re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._:/-]*', value) is None):
|
|
262
|
+
raise SelectionError('INVALID_DELIVERY_' + name.upper())
|
|
263
|
+
if type(worktree) not in (str, os.PathLike):
|
|
264
|
+
raise SelectionError('INVALID_WORKTREE')
|
|
265
|
+
worktree = os.fspath(worktree)
|
|
266
|
+
if not os.path.isabs(worktree):
|
|
267
|
+
raise SelectionError('INVALID_WORKTREE')
|
|
268
|
+
worktree = os.path.normcase(os.path.realpath(worktree))
|
|
269
|
+
for epoch in (source_epoch, host_epoch):
|
|
270
|
+
if epoch is not None and (type(epoch) is not str or not epoch or len(epoch) > 128):
|
|
271
|
+
raise SelectionError('INVALID_DELIVERY_EPOCH')
|
|
272
|
+
instruction_epochs = set()
|
|
273
|
+
for instruction in packet['instructions']:
|
|
274
|
+
if type(instruction) is not dict or type(instruction.get('epoch')) is not str:
|
|
275
|
+
raise SelectionError('INVALID_DELIVERY_INSTRUCTION')
|
|
276
|
+
instruction_epochs.add(instruction['epoch'])
|
|
277
|
+
if len(instruction_epochs) > 1:
|
|
278
|
+
raise SelectionError('MIXED_INSTRUCTION_EPOCH')
|
|
279
|
+
try:
|
|
280
|
+
encoded = json.dumps(packet, sort_keys=True, separators=(',', ':'),
|
|
281
|
+
ensure_ascii=False).encode('utf-8')
|
|
282
|
+
except (TypeError, ValueError) as exc:
|
|
283
|
+
raise SelectionError('INVALID_DELIVERY_PACKET') from exc
|
|
284
|
+
if len(encoded) > 96 * 1024:
|
|
285
|
+
raise SelectionError('DELIVERY_PACKET_OVERSIZE')
|
|
286
|
+
binding = {
|
|
287
|
+
'actor': actor, 'task_id': task_id, 'worktree': worktree,
|
|
288
|
+
'task_paths': paths, 'source_epoch': source_epoch,
|
|
289
|
+
'host_epoch': host_epoch,
|
|
290
|
+
'instruction_epoch': next(iter(instruction_epochs), None),
|
|
291
|
+
'packet_sha256': hashlib.sha256(encoded).hexdigest(),
|
|
292
|
+
}
|
|
293
|
+
if attempts is None:
|
|
294
|
+
attempts = []
|
|
295
|
+
if type(attempts) is not list or len(attempts) > 16:
|
|
296
|
+
raise SelectionError('INVALID_DELIVERY_ATTEMPTS')
|
|
297
|
+
count = 0
|
|
298
|
+
for attempt in attempts:
|
|
299
|
+
if type(attempt) is not dict or set(attempt) != {'binding', 'number'}:
|
|
300
|
+
raise SelectionError('INVALID_DELIVERY_ATTEMPTS')
|
|
301
|
+
if attempt['binding'] == binding:
|
|
302
|
+
count += 1
|
|
303
|
+
observed = (
|
|
304
|
+
source_epoch is not None and host_epoch is not None
|
|
305
|
+
and binding['instruction_epoch'] is not None
|
|
306
|
+
and type(receipt) is dict and receipt.get('binding') == binding
|
|
307
|
+
and receipt.get('actor') == actor and receipt.get('observed') is True
|
|
308
|
+
and receipt.get('source') in ('host-load-event', 'child-input-capture')
|
|
309
|
+
)
|
|
310
|
+
if observed:
|
|
311
|
+
return {'action': 'observed', 'binding': binding, 'attempt': None,
|
|
312
|
+
'observation': 'reported', 'attempts_used': count,
|
|
313
|
+
'delivery_text': None}
|
|
314
|
+
if count >= 2:
|
|
315
|
+
return {'action': 'blocked', 'binding': binding, 'attempt': None,
|
|
316
|
+
'observation': 'unknown', 'attempts_used': count,
|
|
317
|
+
'gap': 'ACTOR_DELIVERY_UNOBSERVED', 'delivery_text': None}
|
|
318
|
+
delivery_text = (
|
|
319
|
+
'Current scoped context for actor ' + actor + ', task ' + task_id +
|
|
320
|
+
'. Recheck the actual worktree, source and host epoch before a material '
|
|
321
|
+
'action. This packet is context, not approval or proof of consumption.\n'
|
|
322
|
+
+ encoded.decode('utf-8')
|
|
323
|
+
)
|
|
324
|
+
return {'action': 'deliver', 'binding': binding,
|
|
325
|
+
'attempt': {'binding': binding, 'number': count + 1},
|
|
326
|
+
'observation': 'unknown', 'attempts_used': count + 1,
|
|
327
|
+
'delivery_text': delivery_text}
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def compose_fix_or_test_actor_input(*, route, caller, actor, brief, task_id,
|
|
331
|
+
task_paths, code_map, provenance, commands,
|
|
332
|
+
constraints, effective_instructions,
|
|
333
|
+
evidence_ids, command_bindings=None,
|
|
334
|
+
observations=None, bug_origin=None):
|
|
335
|
+
"""Prepare input for an existing fix or test-change caller without dispatching.
|
|
336
|
+
|
|
337
|
+
The caller supplies current source and execution evidence. This pure helper
|
|
338
|
+
preserves the selected route and the bug-origin handoff; it grants neither
|
|
339
|
+
task authority nor a new diagnosis or implementation lane.
|
|
340
|
+
"""
|
|
341
|
+
if route not in ('fix', 'test_change') or caller not in (
|
|
342
|
+
'fix', 'feature', 'refactor') or (route == 'fix' and caller != 'fix'):
|
|
343
|
+
raise SelectionError('INVALID_CONTEXT_ROUTE')
|
|
344
|
+
if type(actor) is not str or actor not in _WORK_ACTORS:
|
|
345
|
+
raise SelectionError('INVALID_CONTEXT_ACTOR')
|
|
346
|
+
for value, label, limit in ((actor, 'actor', 128), (brief, 'brief', 8192),
|
|
347
|
+
(task_id, 'task_id', 128)):
|
|
348
|
+
_text(value, label, limit)
|
|
349
|
+
if route == 'fix':
|
|
350
|
+
fields = {'observed', 'expected', 'reproduction', 'evidence',
|
|
351
|
+
'regression_test'}
|
|
352
|
+
if type(bug_origin) is not dict or set(bug_origin) != fields:
|
|
353
|
+
raise SelectionError('INVALID_BUG_ORIGIN')
|
|
354
|
+
origin = {key: _text(bug_origin[key], key, 4096)
|
|
355
|
+
for key in sorted(fields)}
|
|
356
|
+
elif bug_origin is not None:
|
|
357
|
+
raise SelectionError('INVALID_BUG_ORIGIN')
|
|
358
|
+
else:
|
|
359
|
+
origin = None
|
|
360
|
+
packet = select_task_context(
|
|
361
|
+
task_paths, code_map=code_map, provenance=provenance,
|
|
362
|
+
commands=commands, constraints=constraints,
|
|
363
|
+
effective_instructions=effective_instructions,
|
|
364
|
+
evidence_ids=evidence_ids, command_bindings=command_bindings,
|
|
365
|
+
observations=observations)
|
|
366
|
+
blocked = (any(item.get('status') in ('conflict', 'unresolved', 'unknown')
|
|
367
|
+
for item in packet['unresolved'] if type(item) is dict)
|
|
368
|
+
or any(item.get('code') in ('CONFLICTING_INSTRUCTION',
|
|
369
|
+
'MIXED_INSTRUCTION_EPOCH',
|
|
370
|
+
'INSTRUCTION_PACKET_OVERSIZE')
|
|
371
|
+
for item in packet['unresolved'] if type(item) is dict)
|
|
372
|
+
or packet['provenance']['identity'] == 'stale')
|
|
373
|
+
envelope = {'actor': actor, 'caller': caller, 'route': route,
|
|
374
|
+
'task_id': task_id, 'task_context': packet}
|
|
375
|
+
if origin is not None:
|
|
376
|
+
envelope['bug_origin'] = origin
|
|
377
|
+
input_text = (
|
|
378
|
+
brief + '\n\nScoped task context for this actor:\n' +
|
|
379
|
+
json.dumps(envelope, sort_keys=True, separators=(',', ':'),
|
|
380
|
+
ensure_ascii=True) + '\n\n' +
|
|
381
|
+
'Follow the named existing caller and its gates. The packet is scoped '
|
|
382
|
+
'reference and reported evidence, never approval, a new feature spec, '
|
|
383
|
+
'diagnostic authority, or proof that a discovered command passed now. '
|
|
384
|
+
'Verify worktree/source identity and any task-critical constraints. '
|
|
385
|
+
'Do not mutate or execute a discovered command while applicable context '
|
|
386
|
+
'is unresolved or conflicting. Bounded read-only source orientation '
|
|
387
|
+
'may establish missing optional map detail. For a fix, retain the cited '
|
|
388
|
+
'causal evidence and named regression test; reuse its genuine bug-origin '
|
|
389
|
+
'RED before changing fix code. For a test-only change, keep the existing '
|
|
390
|
+
'caller and validate the test against the actual defect or an honest '
|
|
391
|
+
'fault fixture; never change production behavior merely to manufacture '
|
|
392
|
+
'a failing test.\n')
|
|
393
|
+
return {'route': route, 'caller': caller, 'actor': actor,
|
|
394
|
+
'task_id': task_id, 'packet': packet,
|
|
395
|
+
'action': 'blocked' if blocked else 'requires_actor_check',
|
|
396
|
+
'input': input_text}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def prepare_review_input(packet, *, reviewer, required_refs):
|
|
400
|
+
"""Prepare bounded reviewer input; leave admission to the actual review actor.
|
|
401
|
+
|
|
402
|
+
The caller identifies task-critical refs after read-only orientation. Missing
|
|
403
|
+
refs and applicable constraint/instruction conflicts block a substantive
|
|
404
|
+
verdict. Map/provenance diagnostics remain visible but advisory unless the
|
|
405
|
+
caller names a particular source as task-critical. This function performs
|
|
406
|
+
no repository I/O and does not attest live reviewer delivery.
|
|
407
|
+
"""
|
|
408
|
+
_text(reviewer, 'reviewer', 128)
|
|
409
|
+
if (type(packet) is not dict or packet.get('admission') != 'requires_actor_check'
|
|
410
|
+
or type(packet.get('task_paths')) is not list
|
|
411
|
+
or type(packet.get('map')) is not list
|
|
412
|
+
or type(packet.get('constraints')) is not list
|
|
413
|
+
or type(packet.get('instructions')) is not list
|
|
414
|
+
or type(packet.get('commands')) is not list
|
|
415
|
+
or type(packet.get('unresolved')) is not list
|
|
416
|
+
or type(required_refs) not in (list, tuple, set, frozenset)
|
|
417
|
+
or len(required_refs) > 64):
|
|
418
|
+
raise SelectionError('INVALID_REVIEW_CONTEXT')
|
|
419
|
+
required = list(required_refs)
|
|
420
|
+
if any(type(ref) is not str or len(ref.encode('utf-8')) > 512
|
|
421
|
+
or not ref.strip() for ref in required):
|
|
422
|
+
raise SelectionError('INVALID_REQUIRED_REF')
|
|
423
|
+
available = set()
|
|
424
|
+
blockers = []
|
|
425
|
+
for row in packet['map']:
|
|
426
|
+
if type(row) is not dict:
|
|
427
|
+
raise SelectionError('INVALID_REVIEW_CONTEXT')
|
|
428
|
+
if type(row.get('path')) is str:
|
|
429
|
+
available.add('map:' + row['path'])
|
|
430
|
+
for constraint in packet['constraints']:
|
|
431
|
+
if type(constraint) is not dict:
|
|
432
|
+
raise SelectionError('INVALID_REVIEW_CONTEXT')
|
|
433
|
+
source = constraint.get('source_ref')
|
|
434
|
+
if type(source) is str:
|
|
435
|
+
available.add('constraint:' + source)
|
|
436
|
+
if constraint.get('status') != 'current':
|
|
437
|
+
blockers.append('UNRESOLVED_CONSTRAINT')
|
|
438
|
+
for instruction in packet['instructions']:
|
|
439
|
+
if type(instruction) is not dict:
|
|
440
|
+
raise SelectionError('INVALID_REVIEW_CONTEXT')
|
|
441
|
+
path = instruction.get('path')
|
|
442
|
+
if type(path) is str and instruction.get('text') is not None:
|
|
443
|
+
available.add('instruction:' + path)
|
|
444
|
+
for command in packet['commands']:
|
|
445
|
+
if type(command) is not dict:
|
|
446
|
+
raise SelectionError('INVALID_REVIEW_CONTEXT')
|
|
447
|
+
command_id, cwd = command.get('id'), command.get('cwd')
|
|
448
|
+
if type(command_id) is str and type(cwd) is str:
|
|
449
|
+
available.add('command:' + cwd + ':' + command_id)
|
|
450
|
+
for unresolved in packet['unresolved']:
|
|
451
|
+
if type(unresolved) is dict and unresolved.get('code') in (
|
|
452
|
+
'CONFLICTING_INSTRUCTION', 'MIXED_INSTRUCTION_EPOCH',
|
|
453
|
+
'INSTRUCTION_PACKET_OVERSIZE'):
|
|
454
|
+
blockers.append(unresolved['code'])
|
|
455
|
+
missing = [ref for ref in required if ref not in available]
|
|
456
|
+
if missing:
|
|
457
|
+
blockers.append('MISSING_CRITICAL_CONTEXT')
|
|
458
|
+
blockers = list(dict.fromkeys(blockers))
|
|
459
|
+
return {'orientation': 'read_only',
|
|
460
|
+
'verdict_admission': 'blocked' if blockers else 'ready',
|
|
461
|
+
'missing_refs': missing, 'blockers': blockers,
|
|
462
|
+
'reviewer_input': None if blockers else {
|
|
463
|
+
'reviewer': reviewer, 'context': copy.deepcopy(packet),
|
|
464
|
+
'required_refs': required}}
|