@arbiterforge/ca-pi 0.14.10 → 0.14.12

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 (38) hide show
  1. package/README.md +7 -2
  2. package/package.json +1 -1
  3. package/plugins/ca-pi/CHANGELOG.md +28 -0
  4. package/plugins/ca-pi/SKILLS.md +2 -2
  5. package/plugins/ca-pi/SPRINT.md +47 -0
  6. package/plugins/ca-pi/generated/command-catalog.json +2 -2
  7. package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-amd64 +0 -0
  8. package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-arm64 +0 -0
  9. package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-amd64 +0 -0
  10. package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-arm64 +0 -0
  11. package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-amd64.exe +0 -0
  12. package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-arm64.exe +0 -0
  13. package/plugins/ca-pi/helpers/artifacts/release.json +6 -6
  14. package/plugins/ca-pi/hooks/_approvallib.py +58 -22
  15. package/plugins/ca-pi/hooks/_artifactauthoritylib.py +1686 -0
  16. package/plugins/ca-pi/hooks/_artifactlib.py +14 -4
  17. package/plugins/ca-pi/hooks/_artifactpromptlib.py +177 -0
  18. package/plugins/ca-pi/hooks/_host.py +1 -1
  19. package/plugins/ca-pi/hooks/_overrideappendlib.py +5 -1
  20. package/plugins/ca-pi/hooks/_prerequisitelib.py +44 -22
  21. package/plugins/ca-pi/hooks/_reconciliationlib.py +472 -0
  22. package/plugins/ca-pi/hooks/_sprintapprovallib.py +302 -0
  23. package/plugins/ca-pi/hooks/artifact-authority-hook.py +72 -0
  24. package/plugins/ca-pi/hooks/artifact-authority.py +80 -0
  25. package/plugins/ca-pi/hooks/artifact-reconcile.py +57 -0
  26. package/plugins/ca-pi/hooks/hostapi.py +1 -1
  27. package/plugins/ca-pi/hooks/prompt-submit.py +20 -3
  28. package/plugins/ca-pi/includes/artifacts.md +99 -12
  29. package/plugins/ca-pi/includes/sprint-authority.md +69 -0
  30. package/plugins/ca-pi/package.json +1 -1
  31. package/plugins/ca-pi/routines/INDEX.md +7 -4
  32. package/plugins/ca-pi/routines/brainstorming/SKILL.md +97 -29
  33. package/plugins/ca-pi/routines/commit-gate/SKILL.md +7 -1
  34. package/plugins/ca-pi/routines/executing-plans/SKILL.md +32 -15
  35. package/plugins/ca-pi/routines/skill-author/SKILL.md +26 -5
  36. package/plugins/ca-pi/routines/writing-plans/SKILL.md +22 -10
  37. package/plugins/ca-pi/skills/ca-commit/SKILL.md +170 -16
  38. package/plugins/ca-pi/skills/ca-new-skill/SKILL.md +116 -21
@@ -0,0 +1,472 @@
1
+ #!/usr/bin/env python3
2
+ """Exact-prompt user authority for task/scope reconciliation mutations."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from contextlib import contextmanager
7
+ import hashlib
8
+ import json
9
+ import os
10
+ from pathlib import Path
11
+ import re
12
+ import secrets
13
+ import stat
14
+ import time
15
+ from typing import Any
16
+
17
+ import _approvallib
18
+ import _artifactauthoritylib
19
+ import _artifactlib
20
+ import _artifactpromptlib
21
+
22
+
23
+ ID_RE = re.compile(r"[A-Z][A-Z0-9_-]{0,127}")
24
+ TOKEN_RE = re.compile(r"[A-Za-z0-9_-]{12,128}")
25
+ PENDING_DIR = Path(".codearbiter/.markers/reconciliations")
26
+ ATTEMPT_DIR = Path(".codearbiter/.markers/reconciliation-attempts")
27
+ SOURCE_DIR = Path(".codearbiter/.artifacts/authority-sources")
28
+ PROVEN_UNCOMMITTED = frozenset({"REVISION_CONFLICT", "OPERATION_ROLLED_BACK"})
29
+
30
+
31
+ class ReconciliationError(RuntimeError):
32
+ pass
33
+
34
+
35
+ def _canonical(value: Any) -> bytes:
36
+ return json.dumps(value, ensure_ascii=True, allow_nan=False, sort_keys=True, separators=(",", ":")).encode()
37
+
38
+
39
+ def _digest(value: bytes) -> str:
40
+ return hashlib.sha256(value).hexdigest()
41
+
42
+
43
+ def _pending(artifact_id: str) -> Path:
44
+ return PENDING_DIR / f"{artifact_id}.json"
45
+
46
+
47
+ def _operation_id(pending: dict[str, Any]) -> str:
48
+ return f"host-user-reconcile-{_digest(_canonical(pending))[:24]}"
49
+
50
+
51
+ def _attempt(pending: dict[str, Any]) -> Path:
52
+ return ATTEMPT_DIR / f"{_operation_id(pending)}.json"
53
+
54
+
55
+ def _mutation_request(pending: dict[str, Any], receipt: str) -> dict[str, Any]:
56
+ identity = pending["identity"]
57
+ request = {
58
+ "artifact_id": pending["artifact_id"], "operation_id": _operation_id(pending),
59
+ "expected": {"revision": identity["revision"], "model_sha256": identity["model_sha256"]},
60
+ "receipt": receipt,
61
+ }
62
+ if pending["operation"] == "task-reconcile":
63
+ request.update(task=pending["target_id"], state=pending["target_state"], reason=pending["reason"])
64
+ else:
65
+ request.update(scope=pending["target_id"])
66
+ return request
67
+
68
+
69
+ @contextmanager
70
+ def _reconciliation_lock(root: Path, artifact_id: str):
71
+ """Serialize all transitions for one artifact, including authority revocation."""
72
+ if ID_RE.fullmatch(artifact_id) is None:
73
+ raise ReconciliationError("reconciliation ID is invalid")
74
+ parent = _approvallib._safe_directory(root, PENDING_DIR, create=True)
75
+ path = parent / f"{artifact_id}.lock"
76
+ flags = os.O_RDWR | os.O_CREAT | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
77
+ fd = os.open(path, flags, 0o600)
78
+ try:
79
+ info = path.lstat()
80
+ opened = os.fstat(fd)
81
+ reparse = getattr(info, "st_file_attributes", 0) & 0x400
82
+ if (
83
+ stat.S_ISLNK(info.st_mode) or reparse or not stat.S_ISREG(info.st_mode)
84
+ or (info.st_dev, info.st_ino) != (opened.st_dev, opened.st_ino)
85
+ ):
86
+ raise ReconciliationError("reconciliation lock path is unsafe")
87
+ deadline = time.monotonic() + 30
88
+ if os.name == "nt":
89
+ import msvcrt
90
+ os.lseek(fd, 0, os.SEEK_SET)
91
+ while True:
92
+ try:
93
+ msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
94
+ break
95
+ except OSError:
96
+ if time.monotonic() >= deadline:
97
+ raise ReconciliationError("reconciliation transition lock timed out")
98
+ time.sleep(0.05)
99
+ else:
100
+ import fcntl
101
+ while True:
102
+ try:
103
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
104
+ break
105
+ except BlockingIOError:
106
+ if time.monotonic() >= deadline:
107
+ raise ReconciliationError("reconciliation transition lock timed out")
108
+ time.sleep(0.05)
109
+ try:
110
+ yield
111
+ finally:
112
+ if os.name == "nt":
113
+ os.lseek(fd, 0, os.SEEK_SET)
114
+ msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
115
+ else:
116
+ fcntl.flock(fd, fcntl.LOCK_UN)
117
+ finally:
118
+ os.close(fd)
119
+
120
+
121
+ def _write_atomic_new(root: Path, relative: Path, payload: bytes) -> None:
122
+ """Publish complete bytes without exposing a partial or replacing a peer."""
123
+ parent = _approvallib._safe_directory(root, relative.parent, create=True)
124
+ target = parent / relative.name
125
+ temporary = parent / f".{target.name}.{secrets.token_hex(8)}.tmp"
126
+ try:
127
+ fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0), 0o600)
128
+ try:
129
+ data = memoryview(payload)
130
+ while data:
131
+ written = os.write(fd, data)
132
+ if written <= 0:
133
+ raise OSError("short reconciliation state write")
134
+ data = data[written:]
135
+ os.fsync(fd)
136
+ finally:
137
+ os.close(fd)
138
+ os.link(temporary, target)
139
+ finally:
140
+ temporary.unlink(missing_ok=True)
141
+
142
+
143
+ def _write_attempt(root: Path, pending: dict[str, Any], request: dict[str, Any]) -> None:
144
+ """Durably bind the exact idempotent mutation request before dispatch."""
145
+ record = {
146
+ "format": "codearbiter.reconciliation-attempt/0.1.0",
147
+ "pending_sha256": _digest(_canonical(pending)), "request": request,
148
+ }
149
+ _write_atomic_new(root, _attempt(pending), _canonical(record))
150
+
151
+
152
+ def _load_attempt(root: Path, pending: dict[str, Any]) -> dict[str, Any] | None:
153
+ path = root / _attempt(pending)
154
+ try:
155
+ info = path.lstat()
156
+ except FileNotFoundError:
157
+ return None
158
+ reparse = getattr(info, "st_file_attributes", 0) & 0x400
159
+ if stat.S_ISLNK(info.st_mode) or reparse or not stat.S_ISREG(info.st_mode) or info.st_size > 16384:
160
+ raise ReconciliationError("in-flight reconciliation record is unsafe")
161
+ try:
162
+ value = json.loads(path.read_text(encoding="utf-8"))
163
+ except (OSError, UnicodeError, ValueError) as exc:
164
+ raise ReconciliationError("in-flight reconciliation record is unreadable") from exc
165
+ if (
166
+ not isinstance(value, dict) or set(value) != {"format", "pending_sha256", "request"}
167
+ or value["format"] != "codearbiter.reconciliation-attempt/0.1.0"
168
+ or value["pending_sha256"] != _digest(_canonical(pending))
169
+ or not isinstance(value["request"], dict)
170
+ or not isinstance(value["request"].get("receipt"), str)
171
+ or not value["request"]["receipt"].startswith(".codearbiter/.artifacts/receipts/")
172
+ or value["request"] != _mutation_request(pending, value["request"]["receipt"])
173
+ ):
174
+ raise ReconciliationError("in-flight reconciliation record is malformed")
175
+ return value["request"]
176
+
177
+
178
+ def _current(client: Any, artifact_id: str, target_id: str) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
179
+ identity = client.call("identity", {"artifact_id": artifact_id})
180
+ snapshot = client.call("snapshot", {"artifact_id": artifact_id})
181
+ record = client.call("read", {"artifact_id": artifact_id, "symbol": target_id, "mode": "exact"})
182
+ record = record.get("record") if isinstance(record, dict) else None
183
+ if (
184
+ not isinstance(identity, dict) or identity.get("artifact_id") != artifact_id
185
+ or not isinstance(identity.get("revision"), int)
186
+ or not isinstance(identity.get("model_sha256"), str)
187
+ or not isinstance(identity.get("normative_sha256"), str)
188
+ or not isinstance(snapshot, dict) or not isinstance(snapshot.get("sha256"), str)
189
+ or not isinstance(record, dict) or record.get("id") != target_id
190
+ ):
191
+ raise ReconciliationError("engine reconciliation target is malformed")
192
+ return identity, snapshot, record
193
+
194
+
195
+ def arm_reconciliation(
196
+ root: str | Path, client: Any, artifact_id: str, target_id: str,
197
+ operation: str, *, target_state: str | None = None, reason: str = "",
198
+ assessment: str, token: str | None = None,
199
+ ) -> dict[str, Any]:
200
+ resolved = Path(root).resolve(strict=True)
201
+ with _reconciliation_lock(resolved, artifact_id):
202
+ return _arm_reconciliation_unlocked(
203
+ resolved, client, artifact_id, target_id, operation,
204
+ target_state=target_state, reason=reason, assessment=assessment, token=token,
205
+ )
206
+
207
+
208
+ def _arm_reconciliation_unlocked(
209
+ root: Path, client: Any, artifact_id: str, target_id: str,
210
+ operation: str, *, target_state: str | None = None, reason: str = "",
211
+ assessment: str, token: str | None = None,
212
+ ) -> dict[str, Any]:
213
+ root = Path(root).resolve(strict=True)
214
+ if operation not in {"task-reconcile", "scope-reconcile"}:
215
+ raise ReconciliationError("reconciliation operation is unsupported")
216
+ if ID_RE.fullmatch(artifact_id) is None or ID_RE.fullmatch(target_id) is None:
217
+ raise ReconciliationError("reconciliation IDs are invalid")
218
+ if operation == "task-reconcile" and target_state not in {"PENDING", "REVIEW", "BLOCKED"}:
219
+ raise ReconciliationError("task reconciliation state is invalid")
220
+ if operation == "scope-reconcile" and target_state is not None:
221
+ raise ReconciliationError("scope reconciliation has no target state")
222
+ if not isinstance(assessment, str) or not assessment.strip() or not isinstance(reason, str):
223
+ raise ReconciliationError("reconciliation assessment/reason is invalid")
224
+ token = token or secrets.token_urlsafe(24)
225
+ if TOKEN_RE.fullmatch(token) is None:
226
+ raise ReconciliationError("reconciliation token is invalid")
227
+ identity, snapshot, record = _current(client, artifact_id, target_id)
228
+ reply = (
229
+ f"reconcile-task {artifact_id} {target_id} {target_state} {token}"
230
+ if operation == "task-reconcile"
231
+ else f"reconcile-scope {artifact_id} {target_id} {token}"
232
+ )
233
+ pending = {
234
+ "format": "codearbiter.pending-reconciliation/0.1.0",
235
+ "operation": operation, "artifact_id": artifact_id, "target_id": target_id,
236
+ "target_state": target_state, "reason": reason, "assessment": assessment,
237
+ "identity": identity, "input_sha256": snapshot["sha256"],
238
+ "record": record, "record_sha256": _digest(_canonical(record)),
239
+ "prompt_sha256": _digest(reply.encode()),
240
+ }
241
+ relative = _pending(artifact_id)
242
+ created = False
243
+ try:
244
+ pending_bytes = _canonical(pending)
245
+ _write_atomic_new(root, relative, pending_bytes)
246
+ created = True
247
+ _artifactpromptlib.register(
248
+ root, "reconciliation", artifact_id, reply,
249
+ binding_sha256=_digest(pending_bytes),
250
+ )
251
+ except FileExistsError as exc:
252
+ raise ReconciliationError("PENDING_RECONCILIATION: reconciliation is already armed") from exc
253
+ except Exception:
254
+ if created:
255
+ try:
256
+ (root / relative).unlink()
257
+ except OSError:
258
+ pass
259
+ raise
260
+ return {"artifact_id": artifact_id, "target_id": target_id, "operation": operation, "reply": reply}
261
+
262
+
263
+ def _load(root: Path, artifact_id: str) -> dict[str, Any]:
264
+ try:
265
+ path = root / _pending(artifact_id)
266
+ info = path.lstat()
267
+ reparse = getattr(info, "st_file_attributes", 0) & 0x400
268
+ if stat.S_ISLNK(info.st_mode) or reparse or not stat.S_ISREG(info.st_mode) or info.st_size > 131072:
269
+ raise OSError("unsafe pending reconciliation")
270
+ value = json.loads(path.read_text(encoding="utf-8"))
271
+ except (OSError, UnicodeError, ValueError) as exc:
272
+ raise ReconciliationError("pending reconciliation is unreadable") from exc
273
+ expected = {
274
+ "format", "operation", "artifact_id", "target_id", "target_state",
275
+ "reason", "assessment", "identity", "input_sha256", "record",
276
+ "record_sha256", "prompt_sha256",
277
+ }
278
+ if (
279
+ not isinstance(value, dict) or set(value) != expected
280
+ or value.get("format") != "codearbiter.pending-reconciliation/0.1.0"
281
+ or value.get("artifact_id") != artifact_id
282
+ or ID_RE.fullmatch(value.get("artifact_id", "")) is None
283
+ or ID_RE.fullmatch(value.get("target_id", "")) is None
284
+ or value.get("operation") not in {"task-reconcile", "scope-reconcile"}
285
+ or not isinstance(value.get("reason"), str)
286
+ or not isinstance(value.get("assessment"), str) or not value["assessment"].strip()
287
+ or not isinstance(value.get("identity"), dict)
288
+ or not isinstance(value.get("record"), dict)
289
+ or not isinstance(value.get("input_sha256"), str)
290
+ or not isinstance(value.get("record_sha256"), str)
291
+ or not isinstance(value.get("prompt_sha256"), str)
292
+ ):
293
+ raise ReconciliationError("pending reconciliation is malformed")
294
+ if value["operation"] == "task-reconcile":
295
+ if value["target_state"] not in {"PENDING", "REVIEW", "BLOCKED"}:
296
+ raise ReconciliationError("pending reconciliation is malformed")
297
+ else:
298
+ if value["target_state"] is not None:
299
+ raise ReconciliationError("pending reconciliation is malformed")
300
+ if _digest(_canonical(value["record"])) != value["record_sha256"]:
301
+ raise ReconciliationError("pending reconciliation is malformed")
302
+ return value
303
+
304
+
305
+ def cancel_reconciliation(root: str | Path, artifact_id: str, prompt: str) -> dict[str, Any]:
306
+ """Cancel only an exact armed prompt with no durable mutation attempt."""
307
+ root = Path(root).resolve(strict=True)
308
+ with _reconciliation_lock(root, artifact_id):
309
+ return _cancel_reconciliation_unlocked(root, artifact_id, prompt)
310
+
311
+
312
+ def _cancel_reconciliation_unlocked(root: Path, artifact_id: str, prompt: str) -> dict[str, Any]:
313
+ if ID_RE.fullmatch(artifact_id) is None or not isinstance(prompt, str):
314
+ raise ReconciliationError("cancellation target is invalid")
315
+ pending = _load(root, artifact_id)
316
+ if _digest(prompt.encode()) != pending["prompt_sha256"]:
317
+ raise ReconciliationError("cancellation prompt does not match the armed request")
318
+ if _load_attempt(root, pending) is not None:
319
+ raise ReconciliationError("in-flight reconciliation must be retried or recovered, not cancelled")
320
+ try:
321
+ binding = _artifactpromptlib.binding(root, "reconciliation", artifact_id)
322
+ except _artifactpromptlib.PromptRouteError as exc:
323
+ raise ReconciliationError("reconciliation authority binding is unavailable") from exc
324
+ if binding != _digest(_canonical(pending)):
325
+ raise ReconciliationError("pending reconciliation changed after arming")
326
+ (root / _pending(artifact_id)).unlink()
327
+ _artifactpromptlib.unregister(root, "reconciliation", artifact_id)
328
+ return {"artifact_id": artifact_id, "cancelled": True}
329
+
330
+
331
+ def cancel_orphaned_reconciliation(root: str | Path, artifact_id: str) -> dict[str, Any]:
332
+ """Recover a complete pending marker stranded before route registration."""
333
+ root = Path(root).resolve(strict=True)
334
+ with _reconciliation_lock(root, artifact_id):
335
+ pending = _load(root, artifact_id)
336
+ if _load_attempt(root, pending) is not None:
337
+ raise ReconciliationError("in-flight reconciliation must be retried or recovered, not cancelled")
338
+ try:
339
+ binding = _artifactpromptlib.binding(root, "reconciliation", artifact_id)
340
+ except _artifactpromptlib.PromptRouteError as exc:
341
+ if isinstance(exc.__cause__, FileNotFoundError):
342
+ binding = None
343
+ else:
344
+ raise ReconciliationError("reconciliation authority binding is unreadable") from exc
345
+ if binding == _digest(_canonical(pending)):
346
+ raise ReconciliationError("reconciliation route is active; cancel with its exact prompt")
347
+ (root / _pending(artifact_id)).unlink()
348
+ if binding is not None:
349
+ _artifactpromptlib.unregister(root, "reconciliation", artifact_id)
350
+ return {"artifact_id": artifact_id, "cancelled": True, "orphaned": True}
351
+
352
+
353
+ def recover_reconciliation(root: str | Path, client: Any, artifact_id: str) -> dict[str, Any]:
354
+ """Replay a durable attempt; clear it only after a proven commit or non-commit."""
355
+ root = Path(root).resolve(strict=True)
356
+ with _reconciliation_lock(root, artifact_id):
357
+ pending = _load(root, artifact_id)
358
+ request = _load_attempt(root, pending)
359
+ if request is None:
360
+ raise ReconciliationError("no in-flight reconciliation to recover")
361
+ try:
362
+ result = client.call(pending["operation"], request)
363
+ transaction = result.get("transaction") if isinstance(result, dict) else None
364
+ if not isinstance(result, dict) or not (
365
+ type(result.get("revision")) is int or result.get("refresh_required") is True
366
+ ) or not isinstance(transaction, dict) or (
367
+ transaction.get("operation_id") != request["operation_id"]
368
+ or transaction.get("state") != "committed"
369
+ ):
370
+ raise ReconciliationError("reconciliation replay response is malformed")
371
+ committed = True
372
+ except _artifactlib.ArtifactError as exc:
373
+ if exc.code not in PROVEN_UNCOMMITTED:
374
+ raise
375
+ result = None
376
+ committed = False
377
+ (root / _pending(artifact_id)).unlink()
378
+ _artifactpromptlib.unregister(root, "reconciliation", artifact_id)
379
+ (root / _attempt(pending)).unlink()
380
+ return {
381
+ "artifact_id": artifact_id, "committed": committed,
382
+ "revision": result.get("revision") if committed else None,
383
+ }
384
+
385
+
386
+ def consume_reconciliation(
387
+ root: str | Path, client: Any, prompt: str, *, host: str, session_id: str,
388
+ ) -> dict[str, Any]:
389
+ root = Path(root).resolve(strict=True)
390
+ parts = prompt.split(" ") if isinstance(prompt, str) else []
391
+ if len(parts) not in {4, 5} or parts[0] not in {"reconcile-task", "reconcile-scope"}:
392
+ return {"matched": False, "reconciled": False}
393
+ if ID_RE.fullmatch(parts[1]) is None or ID_RE.fullmatch(parts[2]) is None:
394
+ return {"matched": False, "reconciled": False}
395
+ with _reconciliation_lock(root, parts[1]):
396
+ return _consume_reconciliation_unlocked(root, client, prompt, host=host, session_id=session_id)
397
+
398
+
399
+ def _consume_reconciliation_unlocked(
400
+ root: Path, client: Any, prompt: str, *, host: str, session_id: str,
401
+ ) -> dict[str, Any]:
402
+ parts = prompt.split(" ")
403
+ artifact_id, target_id = parts[1], parts[2]
404
+ pending = _load(root, artifact_id)
405
+ if not isinstance(host, str) or not host or not isinstance(session_id, str) or not session_id:
406
+ raise ReconciliationError("reconciliation host context is invalid")
407
+ try:
408
+ binding = _artifactpromptlib.binding(root, "reconciliation", artifact_id)
409
+ except _artifactpromptlib.PromptRouteError as exc:
410
+ raise ReconciliationError("reconciliation authority binding is unavailable") from exc
411
+ if binding != _digest(_canonical(pending)):
412
+ raise ReconciliationError("pending reconciliation changed after arming")
413
+ expected_prompt = (
414
+ f"reconcile-task {pending['artifact_id']} {pending['target_id']} {pending['target_state']} {parts[-1]}"
415
+ if pending["operation"] == "task-reconcile"
416
+ else f"reconcile-scope {pending['artifact_id']} {pending['target_id']} {parts[-1]}"
417
+ )
418
+ if prompt != expected_prompt or _digest(prompt.encode()) != pending["prompt_sha256"]:
419
+ return {"matched": False, "reconciled": False}
420
+ if artifact_id != pending["artifact_id"] or target_id != pending["target_id"]:
421
+ return {"matched": False, "reconciled": False}
422
+ request = _load_attempt(root, pending)
423
+ if request is None:
424
+ identity, snapshot, record = _current(client, artifact_id, target_id)
425
+ if identity != pending["identity"] or snapshot["sha256"] != pending["input_sha256"] or record != pending["record"]:
426
+ raise ReconciliationError("reconciliation target changed after arming")
427
+ payload = {"input_sha256": pending["input_sha256"], "assessment": pending["assessment"]}
428
+ if pending["operation"] == "task-reconcile":
429
+ payload.update(task_sha256=pending["record_sha256"], target_state=pending["target_state"])
430
+ event = {
431
+ "kind": "reconciliation",
432
+ "authority_kind": "user_workflow",
433
+ "subject": {"artifact_id": artifact_id, "normative_sha256": identity["normative_sha256"], "record_id": target_id},
434
+ "actor": "interactive repository user", "origin": f"{host}:UserPromptSubmit:{session_id}",
435
+ "verdict": "reconciled", "payload": payload, "source_text": prompt,
436
+ }
437
+ try:
438
+ captured = _artifactauthoritylib.capture_user_prompt(
439
+ root, client, event, host=host, session_id=session_id,
440
+ )
441
+ except _artifactauthoritylib.AuthorityError as exc:
442
+ raise ReconciliationError(str(exc)) from exc
443
+ receipt = captured.get("receipt")
444
+ if not isinstance(receipt, str) or not receipt.startswith(".codearbiter/.artifacts/receipts/"):
445
+ raise ReconciliationError("capture returned no valid reconciliation receipt")
446
+ request = _mutation_request(pending, receipt)
447
+ try:
448
+ _write_attempt(root, pending, request)
449
+ except FileExistsError:
450
+ request = _load_attempt(root, pending)
451
+ if request is None:
452
+ raise ReconciliationError("in-flight reconciliation record disappeared")
453
+ result = client.call(pending["operation"], request)
454
+ (root / _pending(artifact_id)).unlink()
455
+ _artifactpromptlib.unregister(root, "reconciliation", artifact_id)
456
+ (root / _attempt(pending)).unlink()
457
+ return {"matched": True, "reconciled": True, "artifact_id": artifact_id, "target_id": target_id, "receipt": request["receipt"], "revision": result.get("revision")}
458
+
459
+
460
+ def consume_from_hook(*, root: str | Path, plugin_root: str | Path, prompt: str, host: str, session_id: str) -> str:
461
+ attempted = isinstance(prompt, str) and prompt.startswith(("reconcile-task ", "reconcile-scope "))
462
+ if not attempted:
463
+ return ""
464
+ try:
465
+ routed = _artifactpromptlib.resolve("reconciliation", prompt)
466
+ if routed is None:
467
+ raise ReconciliationError("no matching armed reconciliation")
468
+ client = _artifactlib.ArtifactClient(routed, Path(plugin_root) / "helpers" / "artifacts")
469
+ result = consume_reconciliation(routed, client, prompt, host=host, session_id=session_id)
470
+ except (ReconciliationError, _artifactlib.ArtifactError, _artifactpromptlib.PromptRouteError, OSError) as exc:
471
+ return f"codeArbiter: reconciliation capture failed: {exc}"
472
+ return f"codeArbiter: workflow reconciliation recorded for {result['artifact_id']}#{result['target_id']} (receipt {result['receipt']})."