@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
@@ -319,8 +319,17 @@ def _preflight_plan_authoring(
319
319
  *,
320
320
  spec_artifact_id: str,
321
321
  spec_normative_sha256: str,
322
+ draft_for_pair: bool = False,
322
323
  ) -> Path:
323
- """Verify one approved HTML spec and return its unwritten plan target."""
324
+ """Verify the exact spec. Only initial sprint-pair preparation admits a ready draft.
325
+
326
+ Draft preparation never approves the spec or plan and never permits execution.
327
+ The ordinary feature/sequential path still requires an approved spec.
328
+ """
329
+ if type(draft_for_pair) is not bool:
330
+ raise ArtifactError("INVALID_ROUTE", "draft_for_pair must be an explicit boolean")
331
+ if draft_for_pair and (not isinstance(selected, dict) or selected.get("workflow") != "sprint" or selected.get("lane") != "full"):
332
+ raise ArtifactError("INVALID_ROUTE", "draft preview is limited to full-lane initial sprint pairs")
324
333
  if not isinstance(selected, dict):
325
334
  raise ArtifactError("INVALID_ROUTE", "plan authoring requires a selected route")
326
335
  slug = selected.get("slug")
@@ -376,7 +385,7 @@ def _preflight_plan_authoring(
376
385
  )
377
386
  approved = client.call(
378
387
  "validate",
379
- {"artifact_id": spec_artifact_id, "gate": "approved"},
388
+ {"artifact_id": spec_artifact_id, "gate": "ready" if draft_for_pair else "approved"},
380
389
  permit_invalid=True,
381
390
  )
382
391
  authority = approved.get("authority")
@@ -385,8 +394,9 @@ def _preflight_plan_authoring(
385
394
  or approved.get("artifact_id") != spec_artifact_id
386
395
  or approved.get("normative_sha256") != spec_normative_sha256
387
396
  or not isinstance(authority, dict)
388
- or authority.get("state") != "approved"
389
- or authority.get("authority_verified") is not True
397
+ or approved.get("model_sha256") != identity.get("model_sha256")
398
+ or authority.get("state") != ("draft" if draft_for_pair else "approved")
399
+ or authority.get("authority_verified") is not (not draft_for_pair)
390
400
  ):
391
401
  raise ArtifactError(
392
402
  "AUTHORITY_UNVERIFIED",
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env python3
2
+ # codeArbiter — cross-repository routing for exact artifact authority prompts.
3
+ """Register and resolve target repositories without granting prompt authority."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import hashlib
8
+ import json
9
+ import os
10
+ from pathlib import Path
11
+ import stat
12
+ import tempfile
13
+
14
+
15
+ REGISTRY_PARENT = Path(tempfile.gettempdir())
16
+ FORMAT = "codearbiter.artifact-prompt-route/0.1.0"
17
+
18
+
19
+ class PromptRouteError(RuntimeError):
20
+ pass
21
+
22
+
23
+ def _canonical(value):
24
+ return json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode()
25
+
26
+
27
+ def _digest(value: bytes) -> str:
28
+ return hashlib.sha256(value).hexdigest()
29
+
30
+
31
+ def _identity(root: Path) -> dict[str, str]:
32
+ root = root.resolve(strict=True)
33
+ info = root.lstat()
34
+ reparse = getattr(info, "st_file_attributes", 0) & 0x400
35
+ if stat.S_ISLNK(info.st_mode) or reparse or not stat.S_ISDIR(info.st_mode):
36
+ raise PromptRouteError("artifact prompt target is not a real directory")
37
+ return {
38
+ "path": str(root),
39
+ "filesystem_id": f"{int(getattr(info, 'st_dev', 0))}:{int(getattr(info, 'st_ino', 0))}",
40
+ }
41
+
42
+
43
+ def _registry() -> Path:
44
+ namespace = f"uid-{os.getuid()}" if hasattr(os, "getuid") else "current-user"
45
+ root = REGISTRY_PARENT.resolve(strict=True) / f"codearbiter-prompt-routes-{namespace}"
46
+ root.mkdir(mode=0o700, exist_ok=True)
47
+ info = root.lstat()
48
+ reparse = getattr(info, "st_file_attributes", 0) & 0x400
49
+ if stat.S_ISLNK(info.st_mode) or reparse or not stat.S_ISDIR(info.st_mode):
50
+ raise PromptRouteError("artifact prompt registry is unsafe")
51
+ if hasattr(os, "getuid"):
52
+ if info.st_uid != os.getuid():
53
+ raise PromptRouteError("artifact prompt registry has another owner")
54
+ os.chmod(root, 0o700)
55
+ return root
56
+
57
+
58
+ def _key(route: str, artifact_id: str, repository: dict[str, str]) -> str:
59
+ return _digest(_canonical({"route": route, "artifact_id": artifact_id, "repository": repository}))
60
+
61
+
62
+ def register(root: str | Path, route: str, artifact_id: str, prompt: str,
63
+ *, binding_sha256: str | None = None) -> None:
64
+ if route not in {"approval", "prerequisite", "reconciliation"} or not isinstance(prompt, str) or not prompt:
65
+ raise PromptRouteError("artifact prompt route is invalid")
66
+ if binding_sha256 is not None and (
67
+ not isinstance(binding_sha256, str)
68
+ or len(binding_sha256) != 64
69
+ or any(ch not in "0123456789abcdef" for ch in binding_sha256)
70
+ ):
71
+ raise PromptRouteError("artifact prompt binding is invalid")
72
+ repository = _identity(Path(root))
73
+ value = {
74
+ "format": FORMAT,
75
+ "route": route,
76
+ "artifact_id": artifact_id,
77
+ "prompt_sha256": _digest(prompt.encode("utf-8")),
78
+ "repository": repository,
79
+ }
80
+ if binding_sha256 is not None:
81
+ value["binding_sha256"] = binding_sha256
82
+ value["integrity_sha256"] = _digest(_canonical(value))
83
+ registry = _registry()
84
+ key = _key(route, artifact_id, repository)
85
+ path = registry / f"{key}.json"
86
+ data = _canonical(value)
87
+ # Each caller owns an exclusively created pending marker before registering.
88
+ # A prior route at this key therefore belongs to an interrupted request.
89
+ fd, temporary = tempfile.mkstemp(prefix=f".{key}.", suffix=".tmp", dir=registry)
90
+ try:
91
+ try:
92
+ view = memoryview(data)
93
+ while view:
94
+ written = os.write(fd, view)
95
+ if written <= 0:
96
+ raise OSError("short prompt-route write")
97
+ view = view[written:]
98
+ os.fsync(fd)
99
+ finally:
100
+ os.close(fd)
101
+ os.replace(temporary, path)
102
+ finally:
103
+ try:
104
+ os.unlink(temporary)
105
+ except FileNotFoundError:
106
+ pass
107
+
108
+
109
+ def resolve(route: str, prompt: str) -> Path | None:
110
+ if route not in {"approval", "prerequisite", "reconciliation"} or not isinstance(prompt, str):
111
+ return None
112
+ prompt_hash = _digest(prompt.encode("utf-8"))
113
+ matches = []
114
+ for path in _registry().glob("*.json"):
115
+ try:
116
+ raw = path.read_bytes()
117
+ if len(raw) > 16384:
118
+ continue
119
+ value = json.loads(raw.decode("utf-8"))
120
+ unsigned = dict(value)
121
+ integrity = unsigned.pop("integrity_sha256", None)
122
+ repository = value.get("repository")
123
+ if (
124
+ value.get("format") != FORMAT
125
+ or value.get("route") != route
126
+ or value.get("prompt_sha256") != prompt_hash
127
+ or integrity != _digest(_canonical(unsigned))
128
+ or not isinstance(repository, dict)
129
+ ):
130
+ continue
131
+ root = Path(repository.get("path", ""))
132
+ if _identity(root) != repository:
133
+ continue
134
+ matches.append(root.resolve(strict=True))
135
+ except (OSError, UnicodeError, ValueError, PromptRouteError):
136
+ continue
137
+ unique = {str(path): path for path in matches}
138
+ if len(unique) > 1:
139
+ raise PromptRouteError("artifact authority prompt matches multiple repositories")
140
+ return next(iter(unique.values()), None)
141
+
142
+
143
+ def unregister(root: str | Path, route: str, artifact_id: str) -> None:
144
+ repository = _identity(Path(root))
145
+ path = _registry() / f"{_key(route, artifact_id, repository)}.json"
146
+ try:
147
+ path.unlink()
148
+ except FileNotFoundError:
149
+ pass
150
+
151
+
152
+ def binding(root: str | Path, route: str, artifact_id: str) -> str | None:
153
+ repository = _identity(Path(root))
154
+ path = _registry() / f"{_key(route, artifact_id, repository)}.json"
155
+ try:
156
+ raw = path.read_bytes()
157
+ if len(raw) > 16384:
158
+ raise OSError("oversized prompt route")
159
+ value = json.loads(raw.decode("utf-8"))
160
+ unsigned = dict(value)
161
+ integrity = unsigned.pop("integrity_sha256", None)
162
+ except (OSError, UnicodeError, ValueError) as exc:
163
+ raise PromptRouteError("artifact prompt route is unreadable") from exc
164
+ if (
165
+ value.get("format") != FORMAT
166
+ or value.get("route") != route
167
+ or value.get("artifact_id") != artifact_id
168
+ or value.get("repository") != repository
169
+ or integrity != _digest(_canonical(unsigned))
170
+ ):
171
+ raise PromptRouteError("artifact prompt route is malformed")
172
+ result = value.get("binding_sha256")
173
+ if not isinstance(result, str) or len(result) != 64 or any(
174
+ ch not in "0123456789abcdef" for ch in result
175
+ ):
176
+ raise PromptRouteError("artifact prompt route has no closed binding")
177
+ return result
@@ -11,7 +11,7 @@ import hostapi # noqa: E402
11
11
  class PiHost(hostapi.Host):
12
12
  name = "pi"
13
13
  adapter_name = "@arbiterforge/ca-pi"
14
- adapter_version = "0.14.10"
14
+ adapter_version = "0.14.12"
15
15
  update_target = "ca-pi"
16
16
  update_tag_prefix = "ca-pi-v"
17
17
  update_command = "pi update npm:@arbiterforge/ca-pi"
@@ -78,7 +78,11 @@ def _validate_file_type(path):
78
78
 
79
79
  def _validate_existing(path, raw):
80
80
  _validate_file_type(path)
81
- if raw.startswith(b"\xef\xbb\xbf") or b"\x00" in raw or b"\r" in raw:
81
+ # Historical audit bytes are immutable. A stray CR can exist in an older
82
+ # row without making a new O_APPEND write unsafe, so preserve it verbatim
83
+ # and require only that newly written rows use LF. Rewriting the file to
84
+ # normalize old bytes would violate the append-only contract.
85
+ if raw.startswith(b"\xef\xbb\xbf") or b"\x00" in raw:
82
86
  raise OverrideAppendError("overrides.log is not canonical UTF-8 LF text")
83
87
  try:
84
88
  raw.decode("utf-8", errors="strict")
@@ -38,6 +38,8 @@ import time
38
38
  from typing import Any
39
39
 
40
40
  import _artifactlib
41
+ import _artifactauthoritylib
42
+ import _artifactpromptlib
41
43
  from _hooklib import acquire_lock, release_lock
42
44
 
43
45
 
@@ -393,7 +395,7 @@ def _validate_record(value: Any, prerequisite_id: str) -> bool:
393
395
  )
394
396
 
395
397
 
396
- def _validate_confirmed_state(value: dict[str, Any]) -> None:
398
+ def _validate_confirmed_state(value: dict[str, Any], *, check_source: bool = True) -> None:
397
399
  event = value["event"]
398
400
  expected_subject = {
399
401
  "artifact_id": value["artifact_id"],
@@ -444,7 +446,7 @@ def _validate_confirmed_state(value: dict[str, Any]) -> None:
444
446
  expected_source = (
445
447
  SOURCE_DIR / f"{_digest(_canonical(event))}.json"
446
448
  ).as_posix()
447
- if value["authority_source"] != expected_source:
449
+ if check_source and value["authority_source"] != expected_source:
448
450
  raise PrerequisiteError(
449
451
  "INVALID_PENDING_PREREQUISITE",
450
452
  "confirmed authority source does not match the event",
@@ -535,7 +537,7 @@ def _load_pending(root: Path, artifact_id: str) -> dict[str, Any] | None:
535
537
  "INVALID_PENDING_PREREQUISITE", "captured state is incomplete"
536
538
  )
537
539
  else:
538
- _validate_confirmed_state(value)
540
+ _validate_confirmed_state(value, check_source=False)
539
541
  if value["mutation_request"] != _expected_mutation_request(value):
540
542
  raise PrerequisiteError(
541
543
  "INVALID_PENDING_PREREQUISITE",
@@ -601,16 +603,22 @@ def arm_user_prerequisite(
601
603
  "PENDING_PREREQUISITE",
602
604
  "resolve or cancel this plan's existing prerequisite request first",
603
605
  ) from exc
606
+ reply = (
607
+ f"satisfy-prerequisite {artifact_id} {prerequisite_id} "
608
+ f"{confirmation_nonce}"
609
+ )
610
+ try:
611
+ _artifactpromptlib.register(root, "prerequisite", artifact_id, reply)
612
+ except _artifactpromptlib.PromptRouteError:
613
+ (root / relative).unlink()
614
+ raise
604
615
  return {
605
616
  "artifact_id": artifact_id,
606
617
  "prerequisite_id": prerequisite_id,
607
618
  "title": record["title"],
608
619
  "requirement": record["requirement"],
609
620
  "review_packet_sha256": packet_sha256,
610
- "reply": (
611
- f"satisfy-prerequisite {artifact_id} {prerequisite_id} "
612
- f"{confirmation_nonce}"
613
- ),
621
+ "reply": reply,
614
622
  "pending": relative.as_posix(),
615
623
  }
616
624
 
@@ -752,12 +760,17 @@ def consume_user_prerequisite(
752
760
  session_id=session_id,
753
761
  now=observed_at,
754
762
  )
755
- source_ref, source_sha256 = _publish_event(root, pending)
756
763
  if pending["state"] == "CONFIRMED":
757
- captured = client.call(
758
- "capture",
759
- {"source_ref": source_ref, "source_sha256": source_sha256},
760
- )
764
+ observed_host, separator, observed_session = pending["event"]["origin"].partition(":UserPromptSubmit:")
765
+ if not separator:
766
+ raise PrerequisiteError("INVALID_PENDING_PREREQUISITE", "confirmed prompt origin is malformed")
767
+ try:
768
+ captured = _artifactauthoritylib.capture_user_prompt(
769
+ root, client, pending["event"], host=observed_host,
770
+ session_id=observed_session,
771
+ )
772
+ except _artifactauthoritylib.AuthorityError as exc:
773
+ raise PrerequisiteError("INVALID_AUTHORITY_SOURCE", str(exc)) from exc
761
774
  receipt = captured.get("receipt")
762
775
  if not isinstance(receipt, str) or RECEIPT_RE.fullmatch(receipt) is None:
763
776
  raise PrerequisiteError("INVALID_RECEIPT", "capture returned no valid receipt")
@@ -774,18 +787,20 @@ def consume_user_prerequisite(
774
787
  }
775
788
  captured_state = dict(pending)
776
789
  captured_state.update(
777
- state="CAPTURED", receipt=receipt, mutation_request=mutation_request
790
+ state="CAPTURED", receipt=receipt, mutation_request=mutation_request,
791
+ authority_source=captured["authority_source"],
778
792
  )
779
793
  _replace_state(root, _pending_relative(artifact_id), captured_state)
780
794
  pending = captured_state
781
795
  result = client.call("prerequisite", pending["mutation_request"])
782
796
  (root / _pending_relative(artifact_id)).unlink()
797
+ _artifactpromptlib.unregister(root, "prerequisite", artifact_id)
783
798
  return {
784
799
  "matched": True,
785
800
  "satisfied": True,
786
801
  "artifact_id": artifact_id,
787
802
  "prerequisite_id": prerequisite_id,
788
- "authority_source": source_ref,
803
+ "authority_source": pending["authority_source"],
789
804
  "receipt": pending["receipt"],
790
805
  "revision": result.get("revision"),
791
806
  "replayed": bool((result.get("transaction") or {}).get("replayed")),
@@ -817,6 +832,7 @@ def cancel_user_prerequisite(
817
832
  "a confirmed prerequisite decision must be recovered, not cancelled",
818
833
  )
819
834
  (root / _pending_relative(artifact_id)).unlink()
835
+ _artifactpromptlib.unregister(root, "prerequisite", artifact_id)
820
836
  return {
821
837
  "artifact_id": artifact_id,
822
838
  "prerequisite_id": prerequisite_id,
@@ -868,6 +884,7 @@ def supersede_user_prerequisite(
868
884
  raise
869
885
  else:
870
886
  (root / _pending_relative(artifact_id)).unlink()
887
+ _artifactpromptlib.unregister(root, "prerequisite", artifact_id)
871
888
  return {
872
889
  "artifact_id": artifact_id,
873
890
  "prerequisite_id": prerequisite_id,
@@ -878,6 +895,7 @@ def supersede_user_prerequisite(
878
895
  "replayed": bool((result.get("transaction") or {}).get("replayed")),
879
896
  }
880
897
  (root / _pending_relative(artifact_id)).unlink()
898
+ _artifactpromptlib.unregister(root, "prerequisite", artifact_id)
881
899
  return {
882
900
  "artifact_id": artifact_id,
883
901
  "prerequisite_id": prerequisite_id,
@@ -904,20 +922,23 @@ def consume_from_hook(
904
922
  if attempted
905
923
  else ""
906
924
  )
907
- if not (Path(root) / _pending_relative(match[0])).exists():
908
- return (
909
- "codeArbiter: prerequisite capture failed: no matching armed request"
910
- if attempted
911
- else ""
912
- )
913
925
  try:
926
+ routed = _artifactpromptlib.resolve("prerequisite", prompt)
927
+ root = routed or Path(root)
928
+ if not (Path(root) / _pending_relative(match[0])).exists():
929
+ return (
930
+ "codeArbiter: prerequisite capture failed: no matching armed request"
931
+ if attempted
932
+ else ""
933
+ )
914
934
  client = _artifactlib.ArtifactClient(
915
935
  root, Path(plugin_root) / "helpers" / "artifacts"
916
936
  )
917
937
  result = consume_user_prerequisite(
918
938
  root, client, prompt, host=host, session_id=session_id
919
939
  )
920
- except (PrerequisiteError, _artifactlib.ArtifactError, OSError) as exc:
940
+ except (PrerequisiteError, _artifactlib.ArtifactError,
941
+ _artifactpromptlib.PromptRouteError, OSError) as exc:
921
942
  return f"codeArbiter: prerequisite capture failed: {exc}"
922
943
  if not result.get("satisfied"):
923
944
  return "codeArbiter: prerequisite capture failed: confirmation did not match"
@@ -966,6 +987,7 @@ def main(argv: list[str] | None = None) -> int:
966
987
  if __name__ == "__main__":
967
988
  try:
968
989
  raise SystemExit(main())
969
- except (PrerequisiteError, _artifactlib.ArtifactError) as exc:
990
+ except (PrerequisiteError, _artifactlib.ArtifactError,
991
+ _artifactpromptlib.PromptRouteError) as exc:
970
992
  sys.stderr.write(str(exc) + "\n")
971
993
  raise SystemExit(1)