@arbiterforge/ca-pi 0.14.9 → 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 (40) hide show
  1. package/README.md +7 -2
  2. package/package.json +1 -1
  3. package/plugins/ca-pi/CHANGELOG.md +39 -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/_githooks.py +368 -39
  19. package/plugins/ca-pi/hooks/_host.py +1 -1
  20. package/plugins/ca-pi/hooks/_overrideappendlib.py +5 -1
  21. package/plugins/ca-pi/hooks/_prerequisitelib.py +44 -22
  22. package/plugins/ca-pi/hooks/_reconciliationlib.py +472 -0
  23. package/plugins/ca-pi/hooks/_sprintapprovallib.py +302 -0
  24. package/plugins/ca-pi/hooks/artifact-authority-hook.py +72 -0
  25. package/plugins/ca-pi/hooks/artifact-authority.py +80 -0
  26. package/plugins/ca-pi/hooks/artifact-reconcile.py +57 -0
  27. package/plugins/ca-pi/hooks/doctor.py +3 -1
  28. package/plugins/ca-pi/hooks/hostapi.py +1 -1
  29. package/plugins/ca-pi/hooks/prompt-submit.py +20 -3
  30. package/plugins/ca-pi/includes/artifacts.md +99 -12
  31. package/plugins/ca-pi/includes/sprint-authority.md +69 -0
  32. package/plugins/ca-pi/package.json +1 -1
  33. package/plugins/ca-pi/routines/INDEX.md +7 -4
  34. package/plugins/ca-pi/routines/brainstorming/SKILL.md +97 -29
  35. package/plugins/ca-pi/routines/commit-gate/SKILL.md +7 -1
  36. package/plugins/ca-pi/routines/executing-plans/SKILL.md +32 -15
  37. package/plugins/ca-pi/routines/skill-author/SKILL.md +26 -5
  38. package/plugins/ca-pi/routines/writing-plans/SKILL.md +22 -10
  39. package/plugins/ca-pi/skills/ca-commit/SKILL.md +170 -16
  40. 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