@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.
- package/README.md +7 -2
- package/package.json +1 -1
- package/plugins/ca-pi/CHANGELOG.md +39 -0
- package/plugins/ca-pi/SKILLS.md +2 -2
- package/plugins/ca-pi/SPRINT.md +47 -0
- package/plugins/ca-pi/generated/command-catalog.json +2 -2
- 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 +6 -6
- package/plugins/ca-pi/hooks/_approvallib.py +58 -22
- package/plugins/ca-pi/hooks/_artifactauthoritylib.py +1686 -0
- package/plugins/ca-pi/hooks/_artifactlib.py +14 -4
- package/plugins/ca-pi/hooks/_artifactpromptlib.py +177 -0
- package/plugins/ca-pi/hooks/_githooks.py +368 -39
- package/plugins/ca-pi/hooks/_host.py +1 -1
- package/plugins/ca-pi/hooks/_overrideappendlib.py +5 -1
- package/plugins/ca-pi/hooks/_prerequisitelib.py +44 -22
- package/plugins/ca-pi/hooks/_reconciliationlib.py +472 -0
- package/plugins/ca-pi/hooks/_sprintapprovallib.py +302 -0
- package/plugins/ca-pi/hooks/artifact-authority-hook.py +72 -0
- package/plugins/ca-pi/hooks/artifact-authority.py +80 -0
- package/plugins/ca-pi/hooks/artifact-reconcile.py +57 -0
- package/plugins/ca-pi/hooks/doctor.py +3 -1
- package/plugins/ca-pi/hooks/hostapi.py +1 -1
- package/plugins/ca-pi/hooks/prompt-submit.py +20 -3
- package/plugins/ca-pi/includes/artifacts.md +99 -12
- package/plugins/ca-pi/includes/sprint-authority.md +69 -0
- package/plugins/ca-pi/package.json +1 -1
- package/plugins/ca-pi/routines/INDEX.md +7 -4
- package/plugins/ca-pi/routines/brainstorming/SKILL.md +97 -29
- package/plugins/ca-pi/routines/commit-gate/SKILL.md +7 -1
- package/plugins/ca-pi/routines/executing-plans/SKILL.md +32 -15
- package/plugins/ca-pi/routines/skill-author/SKILL.md +26 -5
- package/plugins/ca-pi/routines/writing-plans/SKILL.md +22 -10
- package/plugins/ca-pi/skills/ca-commit/SKILL.md +170 -16
- 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
|
|
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
|
|
389
|
-
or authority.get("
|
|
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
|