@arbiterforge/ca-pi 0.15.10 → 0.16.0

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 (69) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/plugins/ca-pi/CHANGELOG.md +45 -0
  4. package/plugins/ca-pi/SKILLS.md +1 -1
  5. package/plugins/ca-pi/agents/infra-author.md +7 -1
  6. package/plugins/ca-pi/agents/scout.md +40 -8
  7. package/plugins/ca-pi/generated/command-catalog.json +1 -1
  8. package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-amd64 +0 -0
  9. package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-arm64 +0 -0
  10. package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-amd64 +0 -0
  11. package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-arm64 +0 -0
  12. package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-amd64.exe +0 -0
  13. package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-arm64.exe +0 -0
  14. package/plugins/ca-pi/helpers/artifacts/release.json +104 -6
  15. package/plugins/ca-pi/hooks/_activationlib.py +97 -13
  16. package/plugins/ca-pi/hooks/_arbiterstatelib.py +7 -2
  17. package/plugins/ca-pi/hooks/_artifactauthoritylib.py +519 -16
  18. package/plugins/ca-pi/hooks/_artifactlib.py +244 -7
  19. package/plugins/ca-pi/hooks/_artifactpromptlib.py +106 -2
  20. package/plugins/ca-pi/hooks/_bashguardlib.py +23 -8
  21. package/plugins/ca-pi/hooks/_contextreportlib.py +564 -6
  22. package/plugins/ca-pi/hooks/_contextselectlib.py +464 -0
  23. package/plugins/ca-pi/hooks/_contextsnapshotlib.py +999 -0
  24. package/plugins/ca-pi/hooks/_debughandofflib.py +767 -0
  25. package/plugins/ca-pi/hooks/_hooklib.py +2 -1
  26. package/plugins/ca-pi/hooks/_host.py +1 -1
  27. package/plugins/ca-pi/hooks/_intentlib.py +5 -5
  28. package/plugins/ca-pi/hooks/_protectedlib.py +3 -3
  29. package/plugins/ca-pi/hooks/_protectedstatelib.py +152 -2
  30. package/plugins/ca-pi/hooks/_provenancelib.py +604 -34
  31. package/plugins/ca-pi/hooks/_readinjectlib.py +131 -1
  32. package/plugins/ca-pi/hooks/_sensitivelib.py +28 -14
  33. package/plugins/ca-pi/hooks/artifact-authority-hook.py +1 -0
  34. package/plugins/ca-pi/hooks/artifact-authority.py +25 -1
  35. package/plugins/ca-pi/hooks/debug-handoff.py +85 -0
  36. package/plugins/ca-pi/hooks/doctor.py +4 -2
  37. package/plugins/ca-pi/hooks/git-enforce.py +7 -6
  38. package/plugins/ca-pi/hooks/hostapi.py +1 -1
  39. package/plugins/ca-pi/hooks/init-codearbiter.py +143 -15
  40. package/plugins/ca-pi/hooks/prompt-submit.py +14 -0
  41. package/plugins/ca-pi/hooks/session-start.py +26 -7
  42. package/plugins/ca-pi/includes/artifacts.md +41 -0
  43. package/plugins/ca-pi/includes/author-tdd-workflow.md +16 -4
  44. package/plugins/ca-pi/includes/helper-invocation.md +128 -0
  45. package/plugins/ca-pi/includes/pi-host-notes.md +13 -0
  46. package/plugins/ca-pi/includes/routing-table.md +1 -1
  47. package/plugins/ca-pi/includes/security-gate-record.md +5 -3
  48. package/plugins/ca-pi/package.json +1 -1
  49. package/plugins/ca-pi/routines/INDEX.md +1 -1
  50. package/plugins/ca-pi/routines/commit-gate/SKILL.md +5 -3
  51. package/plugins/ca-pi/routines/context-check/SKILL.md +103 -17
  52. package/plugins/ca-pi/routines/context-creation/SKILL.md +290 -54
  53. package/plugins/ca-pi/routines/debug/SKILL.md +303 -55
  54. package/plugins/ca-pi/routines/debug/references/debug-handoff.schema.json +879 -0
  55. package/plugins/ca-pi/routines/debug/references/handoff.md +155 -0
  56. package/plugins/ca-pi/routines/decompose/SKILL.md +13 -0
  57. package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +34 -0
  58. package/plugins/ca-pi/routines/tdd/SKILL.md +91 -17
  59. package/plugins/ca-pi/skills/ca-commit/SKILL.md +5 -3
  60. package/plugins/ca-pi/skills/ca-context-check/SKILL.md +103 -17
  61. package/plugins/ca-pi/skills/ca-create-context/SKILL.md +290 -54
  62. package/plugins/ca-pi/skills/ca-debug/SKILL.md +303 -55
  63. package/plugins/ca-pi/skills/ca-decompose/SKILL.md +13 -0
  64. package/plugins/ca-pi/skills/ca-feature/SKILL.md +1 -1
  65. package/plugins/ca-pi/skills/ca-fix/SKILL.md +36 -3
  66. package/plugins/ca-pi/skills/ca-init/SKILL.md +19 -0
  67. package/plugins/ca-pi/skills/ca-review/SKILL.md +33 -4
  68. package/plugins/ca-pi/skills/ca-status/SKILL.md +5 -0
  69. 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}}