@arbiterforge/ca-pi 0.15.12 → 0.16.1
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 +1 -1
- package/package.json +1 -1
- package/plugins/ca-pi/CHANGELOG.md +33 -0
- package/plugins/ca-pi/SKILLS.md +1 -1
- package/plugins/ca-pi/agents/infra-author.md +7 -1
- package/plugins/ca-pi/agents/scout.md +40 -8
- package/plugins/ca-pi/generated/command-catalog.json +1 -1
- 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 +104 -6
- package/plugins/ca-pi/hooks/_activationlib.py +97 -13
- package/plugins/ca-pi/hooks/_arbiterstatelib.py +7 -2
- package/plugins/ca-pi/hooks/_artifactauthoritylib.py +313 -76
- package/plugins/ca-pi/hooks/_artifactlib.py +166 -2
- package/plugins/ca-pi/hooks/_artifactpromptlib.py +106 -2
- package/plugins/ca-pi/hooks/_bashguardlib.py +14 -0
- package/plugins/ca-pi/hooks/_contextreportlib.py +564 -6
- package/plugins/ca-pi/hooks/_contextselectlib.py +464 -0
- package/plugins/ca-pi/hooks/_contextsnapshotlib.py +999 -0
- package/plugins/ca-pi/hooks/_debughandofflib.py +767 -0
- package/plugins/ca-pi/hooks/_hooklib.py +1 -0
- package/plugins/ca-pi/hooks/_host.py +1 -1
- package/plugins/ca-pi/hooks/_intentlib.py +5 -5
- package/plugins/ca-pi/hooks/_protectedlib.py +3 -3
- package/plugins/ca-pi/hooks/_protectedstatelib.py +152 -2
- package/plugins/ca-pi/hooks/_provenancelib.py +604 -34
- package/plugins/ca-pi/hooks/_readinjectlib.py +131 -1
- package/plugins/ca-pi/hooks/artifact-authority.py +20 -1
- package/plugins/ca-pi/hooks/debug-handoff.py +85 -0
- package/plugins/ca-pi/hooks/doctor.py +4 -2
- package/plugins/ca-pi/hooks/hostapi.py +1 -1
- package/plugins/ca-pi/hooks/init-codearbiter.py +143 -15
- package/plugins/ca-pi/hooks/prompt-submit.py +14 -0
- package/plugins/ca-pi/hooks/session-start.py +26 -7
- package/plugins/ca-pi/includes/author-tdd-workflow.md +16 -4
- package/plugins/ca-pi/includes/helper-invocation.md +128 -0
- package/plugins/ca-pi/includes/pi-host-notes.md +13 -0
- package/plugins/ca-pi/includes/routing-table.md +1 -1
- package/plugins/ca-pi/package.json +1 -1
- package/plugins/ca-pi/routines/INDEX.md +1 -1
- package/plugins/ca-pi/routines/commit-gate/SKILL.md +5 -3
- package/plugins/ca-pi/routines/context-check/SKILL.md +103 -17
- package/plugins/ca-pi/routines/context-creation/SKILL.md +290 -54
- package/plugins/ca-pi/routines/debug/SKILL.md +303 -55
- package/plugins/ca-pi/routines/debug/references/debug-handoff.schema.json +879 -0
- package/plugins/ca-pi/routines/debug/references/handoff.md +155 -0
- package/plugins/ca-pi/routines/decompose/SKILL.md +13 -0
- package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +34 -0
- package/plugins/ca-pi/routines/tdd/SKILL.md +91 -17
- package/plugins/ca-pi/skills/ca-commit/SKILL.md +5 -3
- package/plugins/ca-pi/skills/ca-context-check/SKILL.md +103 -17
- package/plugins/ca-pi/skills/ca-create-context/SKILL.md +290 -54
- package/plugins/ca-pi/skills/ca-debug/SKILL.md +303 -55
- package/plugins/ca-pi/skills/ca-decompose/SKILL.md +13 -0
- package/plugins/ca-pi/skills/ca-feature/SKILL.md +1 -1
- package/plugins/ca-pi/skills/ca-fix/SKILL.md +36 -3
- package/plugins/ca-pi/skills/ca-init/SKILL.md +19 -0
- package/plugins/ca-pi/skills/ca-review/SKILL.md +33 -4
- package/plugins/ca-pi/skills/ca-status/SKILL.md +5 -0
- package/plugins/ca-pi/skills/ca-task/SKILL.md +55 -8
|
@@ -123,6 +123,8 @@ import json
|
|
|
123
123
|
import math
|
|
124
124
|
import os
|
|
125
125
|
import re
|
|
126
|
+
import tempfile
|
|
127
|
+
import time
|
|
126
128
|
import unicodedata
|
|
127
129
|
|
|
128
130
|
import _provenancelib
|
|
@@ -791,11 +793,23 @@ def governing_docs(rel, index, runner=None):
|
|
|
791
793
|
# paths. This is git-free. A non-provenance Read exits here with zero
|
|
792
794
|
# git calls.
|
|
793
795
|
entry_paths = set()
|
|
796
|
+
v2_docs = []
|
|
794
797
|
if isinstance(provenance, dict):
|
|
795
|
-
for record in provenance.
|
|
798
|
+
for doc_name, record in provenance.items():
|
|
796
799
|
try:
|
|
797
800
|
if not isinstance(record, dict):
|
|
798
801
|
continue
|
|
802
|
+
if record.get("schema") == _provenancelib.V2_SCHEMA_VERSION:
|
|
803
|
+
if not _provenancelib.valid_provenance_record(record):
|
|
804
|
+
continue
|
|
805
|
+
cited = any(
|
|
806
|
+
evidence.get("kind") == "content" and evidence.get("path") == rel
|
|
807
|
+
for field in record["fields"] for claim in field["claims"]
|
|
808
|
+
for evidence in claim["evidence"]
|
|
809
|
+
)
|
|
810
|
+
if cited or record["document"]["path"] == rel:
|
|
811
|
+
v2_docs.append(doc_name)
|
|
812
|
+
continue
|
|
799
813
|
entries = record.get("entries")
|
|
800
814
|
if not isinstance(entries, list):
|
|
801
815
|
continue
|
|
@@ -810,6 +824,13 @@ def governing_docs(rel, index, runner=None):
|
|
|
810
824
|
except Exception: # noqa: BLE001
|
|
811
825
|
continue
|
|
812
826
|
|
|
827
|
+
# A v2 content dependency is relevant advisory context, but its raw
|
|
828
|
+
# digest and semantic review cannot be proved by the legacy Git-hash
|
|
829
|
+
# gate. Point to the action-boundary audit without claiming freshness.
|
|
830
|
+
for doc_name in v2_docs:
|
|
831
|
+
result.append({"text": "{}.md context identity and semantic review unverified here -- run context-check".format(
|
|
832
|
+
_strip_context_controls(doc_name)), "tier": "standards"})
|
|
833
|
+
|
|
813
834
|
# Step B: only call batch_hash when rel is a known provenance entry path.
|
|
814
835
|
# Absence from entry_paths → SKIP; zero git calls.
|
|
815
836
|
if rel and rel in entry_paths:
|
|
@@ -1166,6 +1187,104 @@ def spec_pointers(rel, index):
|
|
|
1166
1187
|
|
|
1167
1188
|
_artifact_legacy_compute_injection = compute_injection
|
|
1168
1189
|
|
|
1190
|
+
def _session_context_generation_path(root, session_id):
|
|
1191
|
+
# One bounded value per session avoids a shared unbounded session map and
|
|
1192
|
+
# lets unrelated sessions advance without a read/modify/write race.
|
|
1193
|
+
return marker_path(root, session_id, 'compaction-generation',
|
|
1194
|
+
prefix='readinject-context-')
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
def _linklike(path):
|
|
1198
|
+
return os.path.islink(path) or getattr(os.path, 'isjunction', lambda _: False)(path)
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
def _safe_context_generation_path(root, session_id):
|
|
1202
|
+
if not isinstance(root, (str, os.PathLike)) or not os.path.isabs(root):
|
|
1203
|
+
return None
|
|
1204
|
+
path = _session_context_generation_path(root, session_id)
|
|
1205
|
+
parent = os.path.dirname(path)
|
|
1206
|
+
if (_linklike(root) or _linklike(os.path.join(root, '.codearbiter')) or _linklike(parent)
|
|
1207
|
+
or _linklike(path) or _linklike(path + '.lock')):
|
|
1208
|
+
return None
|
|
1209
|
+
return path
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
def read_context_generation(root, session_id):
|
|
1213
|
+
"""Read a bounded worktree-local epoch; None means corrupt/unsafe/unknown."""
|
|
1214
|
+
try:
|
|
1215
|
+
path = _safe_context_generation_path(root, session_id)
|
|
1216
|
+
if path is None:
|
|
1217
|
+
return None
|
|
1218
|
+
with open(path, 'rb') as stream:
|
|
1219
|
+
raw = stream.read(257)
|
|
1220
|
+
if len(raw) > 256 or not raw or not raw.isdigit():
|
|
1221
|
+
return None
|
|
1222
|
+
value = int(raw)
|
|
1223
|
+
return value if value <= 1_000_000_000 else None
|
|
1224
|
+
except FileNotFoundError:
|
|
1225
|
+
return 0
|
|
1226
|
+
except Exception: # noqa: BLE001 - advisory hook never blocks a read
|
|
1227
|
+
return None
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
def bump_context_generation(root, session_id):
|
|
1231
|
+
"""Invalidate surviving read markers after compaction, without a scan."""
|
|
1232
|
+
if not session_id:
|
|
1233
|
+
return False
|
|
1234
|
+
lock = None
|
|
1235
|
+
lock_acquired = False
|
|
1236
|
+
temporary = None
|
|
1237
|
+
try:
|
|
1238
|
+
path = _safe_context_generation_path(root, session_id)
|
|
1239
|
+
if path is None:
|
|
1240
|
+
return False
|
|
1241
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
1242
|
+
if _safe_context_generation_path(root, session_id) != path:
|
|
1243
|
+
return False
|
|
1244
|
+
lock = path + '.lock'
|
|
1245
|
+
for _ in range(25):
|
|
1246
|
+
try:
|
|
1247
|
+
os.mkdir(lock)
|
|
1248
|
+
lock_acquired = True
|
|
1249
|
+
break
|
|
1250
|
+
except FileExistsError:
|
|
1251
|
+
time.sleep(0.01)
|
|
1252
|
+
else:
|
|
1253
|
+
return False
|
|
1254
|
+
current = read_context_generation(root, session_id)
|
|
1255
|
+
if current is None or current >= 1_000_000_000:
|
|
1256
|
+
return False # Corrupt bytes stay intact and force redelivery.
|
|
1257
|
+
fd, temporary = tempfile.mkstemp(prefix='readinject-context-',
|
|
1258
|
+
dir=os.path.dirname(path))
|
|
1259
|
+
with os.fdopen(fd, 'wb') as stream:
|
|
1260
|
+
stream.write(str(current + 1).encode('ascii'))
|
|
1261
|
+
if _safe_context_generation_path(root, session_id) != path:
|
|
1262
|
+
return False
|
|
1263
|
+
os.replace(temporary, path)
|
|
1264
|
+
temporary = None
|
|
1265
|
+
return True
|
|
1266
|
+
except Exception: # noqa: BLE001 - task-time delivery remains the primary route
|
|
1267
|
+
return False
|
|
1268
|
+
finally:
|
|
1269
|
+
if temporary is not None:
|
|
1270
|
+
try:
|
|
1271
|
+
os.unlink(temporary)
|
|
1272
|
+
except OSError:
|
|
1273
|
+
pass
|
|
1274
|
+
if lock_acquired:
|
|
1275
|
+
try:
|
|
1276
|
+
os.rmdir(lock)
|
|
1277
|
+
except OSError:
|
|
1278
|
+
pass
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
def _context_epoch_session(root, session_id):
|
|
1282
|
+
generation = read_context_generation(root, session_id)
|
|
1283
|
+
if generation is None:
|
|
1284
|
+
return None
|
|
1285
|
+
return (str(session_id) + '|context:' + str(generation)
|
|
1286
|
+
if generation else session_id)
|
|
1287
|
+
|
|
1169
1288
|
|
|
1170
1289
|
def compute_injection(root, session_id, rel, runner=None):
|
|
1171
1290
|
# Bind the injected pointers and dedup epoch to ONE validated HTML snapshot.
|
|
@@ -1173,8 +1292,19 @@ def compute_injection(root, session_id, rel, runner=None):
|
|
|
1173
1292
|
# under B's marker, suppressing a later stable B read. HTML therefore builds
|
|
1174
1293
|
# once before dedup; the same entries drive both context and epoch. This is
|
|
1175
1294
|
# advisory identity only, never approval or execution authority.
|
|
1295
|
+
# Preserve the legacy self-read zero-I/O path before any epoch lookup.
|
|
1176
1296
|
if not isinstance(rel, str) or rel.replace("\\", "/").split("/", 1)[0] == ".codearbiter":
|
|
1177
1297
|
return _artifact_legacy_compute_injection(root, session_id, rel, runner)
|
|
1298
|
+
session_id = _context_epoch_session(root, session_id)
|
|
1299
|
+
if session_id is None:
|
|
1300
|
+
# Unknown/corrupt epoch: bounded advisory redelivery without writing
|
|
1301
|
+
# a fresh marker for every read. The actor delivery planner owns its
|
|
1302
|
+
# separate two-attempt accounting and action gap.
|
|
1303
|
+
try:
|
|
1304
|
+
index = build_index(root)
|
|
1305
|
+
return assemble_context(governing_docs(rel, index, runner), budget=150)
|
|
1306
|
+
except Exception: # noqa: BLE001
|
|
1307
|
+
return ""
|
|
1178
1308
|
try:
|
|
1179
1309
|
from _artifactlib import has_html
|
|
1180
1310
|
if not has_html(root):
|
|
@@ -11,6 +11,7 @@ import sys
|
|
|
11
11
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
12
12
|
import _artifactauthoritylib # noqa: E402
|
|
13
13
|
import _artifactlib # noqa: E402
|
|
14
|
+
import _artifactpromptlib # noqa: E402
|
|
14
15
|
import hostapi # noqa: E402
|
|
15
16
|
|
|
16
17
|
|
|
@@ -54,6 +55,15 @@ def main(argv=None) -> int:
|
|
|
54
55
|
"--codex-review-profile", choices=("native-v1",), default=None,
|
|
55
56
|
help="select only after qualifying the actual registered native V1 UUID interface",
|
|
56
57
|
)
|
|
58
|
+
context_arm = sub.add_parser("arm-context")
|
|
59
|
+
context_arm.add_argument("--root", required=True)
|
|
60
|
+
context_arm.add_argument("--request-file", required=True)
|
|
61
|
+
context_cancel = sub.add_parser("cancel-context")
|
|
62
|
+
context_cancel.add_argument("--root", required=True)
|
|
63
|
+
context_cancel.add_argument("--document-id", required=True)
|
|
64
|
+
context_apply = sub.add_parser("apply-context")
|
|
65
|
+
context_apply.add_argument("--root", required=True)
|
|
66
|
+
context_apply.add_argument("--receipt", required=True)
|
|
57
67
|
verify = sub.add_parser("verify")
|
|
58
68
|
verify.add_argument("--root", required=True)
|
|
59
69
|
verify.add_argument("--request-id", required=True)
|
|
@@ -66,6 +76,9 @@ def main(argv=None) -> int:
|
|
|
66
76
|
recover.add_argument("--disposition", required=True, choices=("failed", "abandoned"))
|
|
67
77
|
args = parser.parse_args(argv)
|
|
68
78
|
root = Path(args.root).resolve(strict=True)
|
|
79
|
+
if args.command == "cancel-context":
|
|
80
|
+
print(json.dumps(_artifactauthoritylib.cancel_context_preview(root, args.document_id), sort_keys=True))
|
|
81
|
+
return 0
|
|
69
82
|
client = _artifactlib.ArtifactClient(
|
|
70
83
|
root, _artifactlib.helper_installation(__file__)
|
|
71
84
|
)
|
|
@@ -96,6 +109,11 @@ def main(argv=None) -> int:
|
|
|
96
109
|
result["verify_command"] = (
|
|
97
110
|
f'python "{script}" verify --root "{root}" --request-id {result["request_id"]}'
|
|
98
111
|
)
|
|
112
|
+
elif args.command == "arm-context":
|
|
113
|
+
preview = _artifactlib.read_context_preview_request(root, args.request_file)
|
|
114
|
+
result = _artifactauthoritylib.arm_context_preview(root, client, preview)
|
|
115
|
+
elif args.command == "apply-context":
|
|
116
|
+
result = client.call("context-apply", {"receipt": args.receipt})
|
|
99
117
|
elif args.command == "verify":
|
|
100
118
|
result = _artifactauthoritylib.run_verification(
|
|
101
119
|
root, client, args.request_id,
|
|
@@ -113,6 +131,7 @@ def main(argv=None) -> int:
|
|
|
113
131
|
if __name__ == "__main__":
|
|
114
132
|
try:
|
|
115
133
|
raise SystemExit(main())
|
|
116
|
-
except (_artifactauthoritylib.AuthorityError, _artifactlib.ArtifactError
|
|
134
|
+
except (_artifactauthoritylib.AuthorityError, _artifactlib.ArtifactError,
|
|
135
|
+
_artifactpromptlib.PromptRouteError) as exc:
|
|
117
136
|
sys.stderr.write(str(exc) + "\n")
|
|
118
137
|
raise SystemExit(1)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# codeArbiter — private bounded debug handoff validation entry.
|
|
2
|
+
# Read only stdin and the installed validator; emit closed, redacted diagnostics.
|
|
3
|
+
# Bytecode suppression precedes the local import and no project state is touched.
|
|
4
|
+
#
|
|
5
|
+
# main(argv) -> int: validate one bounded packet and write one result envelope.
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
sys.dont_write_bytecode = True
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
import _debughandofflib as handoff
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
PROTOCOL = "codearbiter.debug-validation/1.0.0"
|
|
17
|
+
DIRECT_CODES = frozenset({
|
|
18
|
+
"INPUT_LIMIT", "DEPTH_LIMIT", "INVALID_UTF8", "INVALID_JSON",
|
|
19
|
+
"DUPLICATE_KEY", "UNSUPPORTED_PROTOCOL", "REQUIRED_FIELD",
|
|
20
|
+
"UNKNOWN_FIELD", "TYPE", "VALUE",
|
|
21
|
+
})
|
|
22
|
+
REFERENCE_CODES = frozenset({"DUPLICATE_ID", "DANGLING_REFERENCE"})
|
|
23
|
+
PATH = re.compile(r"\$(?:\.[A-Za-z_]+|\[[0-9]+\])*\Z", re.ASCII)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _result(valid, errors, truncated=False):
|
|
27
|
+
return {"protocol": PROTOCOL, "valid": valid,
|
|
28
|
+
"errors": errors, "truncated": truncated}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _diagnostic(code, path="$"):
|
|
32
|
+
if code in DIRECT_CODES or code in ("USAGE", "INPUT_IO", "INTERNAL"):
|
|
33
|
+
public_code = code
|
|
34
|
+
elif code in REFERENCE_CODES:
|
|
35
|
+
public_code = "REFERENCE"
|
|
36
|
+
else:
|
|
37
|
+
public_code = "SEMANTIC"
|
|
38
|
+
if type(path) is not str or len(path) > 256 or not PATH.fullmatch(path):
|
|
39
|
+
return {"code": "INTERNAL", "path": "$"}
|
|
40
|
+
return {"code": public_code, "path": path}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _write(result):
|
|
44
|
+
raw = (json.dumps(result, ensure_ascii=True, separators=(",", ":"))
|
|
45
|
+
+ "\n").encode("utf-8")
|
|
46
|
+
if len(raw) > 8192:
|
|
47
|
+
result = _result(False, [{"code": "INTERNAL", "path": "$"}])
|
|
48
|
+
raw = (json.dumps(result, separators=(",", ":")) + "\n").encode("utf-8")
|
|
49
|
+
exit_code = 3
|
|
50
|
+
else:
|
|
51
|
+
exit_code = 0 if result["valid"] else 2
|
|
52
|
+
if result["errors"] and result["errors"][0]["code"] in ("INPUT_IO", "INTERNAL"):
|
|
53
|
+
exit_code = 3
|
|
54
|
+
try:
|
|
55
|
+
sys.stdout.buffer.write(raw)
|
|
56
|
+
sys.stdout.buffer.flush()
|
|
57
|
+
except OSError:
|
|
58
|
+
return 3
|
|
59
|
+
return exit_code
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def main(argv=None):
|
|
63
|
+
if argv is None:
|
|
64
|
+
argv = sys.argv[1:]
|
|
65
|
+
if argv != ["validate"]:
|
|
66
|
+
return _write(_result(False, [_diagnostic("USAGE")]))
|
|
67
|
+
try:
|
|
68
|
+
raw = sys.stdin.buffer.read(65537)
|
|
69
|
+
except OSError:
|
|
70
|
+
return _write(_result(False, [_diagnostic("INPUT_IO")]))
|
|
71
|
+
except Exception:
|
|
72
|
+
return _write(_result(False, [_diagnostic("INTERNAL")]))
|
|
73
|
+
try:
|
|
74
|
+
packet, error = handoff.decode_packet(raw)
|
|
75
|
+
if error is not None:
|
|
76
|
+
return _write(_result(False, [_diagnostic(error)]))
|
|
77
|
+
found, truncated = handoff.validate_semantics_bounded(packet, ceiling=16)
|
|
78
|
+
errors = [_diagnostic(code, path) for code, path in found]
|
|
79
|
+
return _write(_result(not found, errors, truncated))
|
|
80
|
+
except Exception:
|
|
81
|
+
return _write(_result(False, [_diagnostic("INTERNAL")]))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
raise SystemExit(main())
|
|
@@ -23,7 +23,9 @@ from _gitexec import git_executable, root_bound_git_env # noqa: E402
|
|
|
23
23
|
import hostapi # noqa: E402 — host seam (ADR-0011): plugin-root resolution
|
|
24
24
|
import _entrylib # noqa: E402 — shared run() dispatch (jscpd dedup)
|
|
25
25
|
import _githooks # noqa: E402 — #556: git-hook drop-in registry freshness
|
|
26
|
-
from _hooklib import
|
|
26
|
+
from _hooklib import ( # noqa: E402
|
|
27
|
+
frontmatter_enabled, get_host, initialized_body_text, set_host, utf8_stdio,
|
|
28
|
+
)
|
|
27
29
|
|
|
28
30
|
HOOK_SCRIPTS = ("session-start.py", "pre-bash.py", "pre-write.py",
|
|
29
31
|
"pre-edit.py", "post-write-edit.py", "prune-transcript.py",
|
|
@@ -298,7 +300,7 @@ def check_repo():
|
|
|
298
300
|
if enabled:
|
|
299
301
|
with open(ctx, encoding="utf-8", errors="replace") as f:
|
|
300
302
|
body = f.read()
|
|
301
|
-
if
|
|
303
|
+
if initialized_body_text(body):
|
|
302
304
|
ok("project is initialized (<!--INITIALIZED--> marker present)")
|
|
303
305
|
else:
|
|
304
306
|
warn(f"no <!--INITIALIZED--> marker — startup will route to "
|
|
@@ -313,7 +313,7 @@ class Host:
|
|
|
313
313
|
|
|
314
314
|
name = "claude"
|
|
315
315
|
adapter_name = "ca"
|
|
316
|
-
adapter_version = "2.
|
|
316
|
+
adapter_version = "2.24.1"
|
|
317
317
|
|
|
318
318
|
# Update-notifier descriptor. Each independently versioned host overrides
|
|
319
319
|
# these three values in its per-plugin _host.py. Keeping the target,
|
|
@@ -12,11 +12,16 @@
|
|
|
12
12
|
# Usage:
|
|
13
13
|
# python init-codearbiter.py [--root PATH] [--stage N]
|
|
14
14
|
# python init-codearbiter.py --check # report state, create nothing
|
|
15
|
+
# python init-codearbiter.py --check --passive --root PATH # external pre-session read
|
|
15
16
|
# python init-codearbiter.py --repair-lock-exclusion [--root PATH]
|
|
17
|
+
# python init-codearbiter.py --root PATH --context-request FILE # bounded writer bridge, including initialized refresh
|
|
16
18
|
|
|
17
19
|
import argparse
|
|
20
|
+
import json
|
|
18
21
|
import subprocess
|
|
19
22
|
import os
|
|
23
|
+
import re
|
|
24
|
+
import stat
|
|
20
25
|
import sys
|
|
21
26
|
|
|
22
27
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
@@ -25,6 +30,8 @@ from _taskboardlib import DONE_TASKS # noqa: E402 — shared archive template (
|
|
|
25
30
|
import hostapi # noqa: E402 — host seam (ADR-0011)
|
|
26
31
|
import _hooklib # noqa: E402 — set_host DI seam (#257)
|
|
27
32
|
import _entrylib # noqa: E402 — shared run() dispatch (jscpd dedup)
|
|
33
|
+
from _activationlib import passive_activation_inventory # noqa: E402
|
|
34
|
+
import _artifactlib # noqa: E402
|
|
28
35
|
|
|
29
36
|
# NOTE: this stub deliberately does NOT contain the initialization sentinel
|
|
30
37
|
# (an HTML comment wrapping the word INITIALIZED). The SessionStart hook greps
|
|
@@ -122,17 +129,132 @@ def project_root(opt):
|
|
|
122
129
|
return os.path.abspath(os.getcwd())
|
|
123
130
|
|
|
124
131
|
|
|
132
|
+
def _regular_scaffold_entry(path, *, directory):
|
|
133
|
+
"""Reject aliases and reparse points before reading or creating scaffold state."""
|
|
134
|
+
try:
|
|
135
|
+
info = os.lstat(path)
|
|
136
|
+
except OSError:
|
|
137
|
+
return False
|
|
138
|
+
reparse = getattr(info, "st_file_attributes", 0) & getattr(
|
|
139
|
+
stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0)
|
|
140
|
+
return not reparse and (stat.S_ISDIR(info.st_mode) if directory
|
|
141
|
+
else stat.S_ISREG(info.st_mode))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _scaffold_inventory(cad, ctx, requested_stage):
|
|
145
|
+
"""Return absent scaffold names only when surviving state is safe to retain."""
|
|
146
|
+
names = ("CONTEXT.md", *FILES)
|
|
147
|
+
if os.path.lexists(cad) and not _regular_scaffold_entry(cad, directory=True):
|
|
148
|
+
raise SystemExit(f"REFUSING: {cad} is not a regular project-state directory.")
|
|
149
|
+
present = {name for name in names if os.path.lexists(os.path.join(cad, name))}
|
|
150
|
+
if not present:
|
|
151
|
+
if os.path.isdir(cad) and os.listdir(cad):
|
|
152
|
+
raise SystemExit(f"REFUSING: {cad} has existing state without a scaffold; inspect it first.")
|
|
153
|
+
return list(names), False
|
|
154
|
+
for name in present:
|
|
155
|
+
path = os.path.join(cad, name)
|
|
156
|
+
if not _regular_scaffold_entry(path, directory=False):
|
|
157
|
+
raise SystemExit(f"REFUSING: {path} is not a regular scaffold file.")
|
|
158
|
+
if "CONTEXT.md" not in present:
|
|
159
|
+
# A missing stub amid content/provenance may be an interrupted native
|
|
160
|
+
# transaction. Only the known scaffold prefix can be resumed here.
|
|
161
|
+
unexpected = set(os.listdir(cad)) - set(FILES)
|
|
162
|
+
if unexpected:
|
|
163
|
+
raise SystemExit(f"REFUSING: {ctx} is absent amid other project state; inspect prior write outcome.")
|
|
164
|
+
else:
|
|
165
|
+
try:
|
|
166
|
+
with open(ctx, encoding="utf-8") as handle:
|
|
167
|
+
body = handle.read()
|
|
168
|
+
except (OSError, UnicodeError) as exc:
|
|
169
|
+
raise SystemExit(f"REFUSING: cannot inspect {ctx}: {exc}") from None
|
|
170
|
+
enabled, malformed = _hooklib.frontmatter_enabled_text(body)
|
|
171
|
+
lines = body.splitlines()
|
|
172
|
+
boundary = next((index for index, line in enumerate(lines[1:], 1)
|
|
173
|
+
if line.strip() == "---"), None)
|
|
174
|
+
stages = ([match.group(1) for line in lines[1:boundary]
|
|
175
|
+
if (match := re.fullmatch(r"\s*stage:\s*([1-9][0-9]*)\s*", line))]
|
|
176
|
+
if boundary is not None else [])
|
|
177
|
+
if (not enabled or malformed or len(stages) != 1 or
|
|
178
|
+
_hooklib.initialized_body_text(body)):
|
|
179
|
+
raise SystemExit(f"REFUSING: {ctx} is invalid or initialized; inspect before recovery.")
|
|
180
|
+
if requested_stage is not None and int(stages[0]) != requested_stage:
|
|
181
|
+
raise SystemExit(f"REFUSING: {ctx} stage differs from --stage; preserve existing context.")
|
|
182
|
+
headers = {"open-tasks.md": "# Open tasks", "done-tasks.md": "# Done tasks",
|
|
183
|
+
"open-questions.md": "# Open questions",
|
|
184
|
+
"overrides.log": "# codeArbiter override log"}
|
|
185
|
+
for name in present & headers.keys():
|
|
186
|
+
path = os.path.join(cad, name)
|
|
187
|
+
try:
|
|
188
|
+
with open(path, encoding="utf-8") as handle:
|
|
189
|
+
valid = handle.readline().rstrip("\r\n").startswith(headers[name])
|
|
190
|
+
except (OSError, UnicodeError):
|
|
191
|
+
valid = False
|
|
192
|
+
if not valid:
|
|
193
|
+
raise SystemExit(f"REFUSING: {path} has an unrecognized header; preserve it for review.")
|
|
194
|
+
if "last-checkpoint" in present:
|
|
195
|
+
try:
|
|
196
|
+
with open(os.path.join(cad, "last-checkpoint"), encoding="utf-8") as handle:
|
|
197
|
+
valid = bool(re.fullmatch(r"[0-9]+\s*", handle.read()))
|
|
198
|
+
except (OSError, UnicodeError):
|
|
199
|
+
valid = False
|
|
200
|
+
if not valid:
|
|
201
|
+
raise SystemExit("REFUSING: last-checkpoint is malformed; preserve it for review.")
|
|
202
|
+
return [name for name in names if name not in present], True
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _create_scaffold_file(path, content, *, newline=None):
|
|
206
|
+
"""Create once; a concurrent new file is a conflict, never an overwrite."""
|
|
207
|
+
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
|
208
|
+
flags |= getattr(os, "O_NOFOLLOW", 0)
|
|
209
|
+
try:
|
|
210
|
+
fd = os.open(path, flags, 0o666)
|
|
211
|
+
except FileExistsError:
|
|
212
|
+
raise SystemExit(f"REFUSING: {path} appeared during scaffold recovery.") from None
|
|
213
|
+
with os.fdopen(fd, "w", encoding="utf-8", newline=newline) as handle:
|
|
214
|
+
handle.write(content)
|
|
215
|
+
|
|
216
|
+
|
|
125
217
|
def main(argv=None):
|
|
126
218
|
ap = argparse.ArgumentParser(add_help=True)
|
|
127
219
|
ap.add_argument("--root")
|
|
128
220
|
ap.add_argument("--stage", type=int)
|
|
129
221
|
ap.add_argument("--check", action="store_true")
|
|
222
|
+
ap.add_argument("--passive", action="store_true", help=(
|
|
223
|
+
"inspect explicit root before host activation; no Git or network probes"))
|
|
224
|
+
ap.add_argument("--context-request", help=argparse.SUPPRESS)
|
|
130
225
|
ap.add_argument("--repair-lock-exclusion", action="store_true", help=(
|
|
131
226
|
"repair only Git-local info/exclude for an existing scaffold; retain the "
|
|
132
227
|
"task-board OS lock, never rewrite project state (cannot combine with --check/--stage)"))
|
|
133
228
|
args = ap.parse_args(argv)
|
|
229
|
+
if args.context_request:
|
|
230
|
+
if not args.root or args.stage is not None or args.check or args.passive or args.repair_lock_exclusion:
|
|
231
|
+
ap.error("--context-request requires --root and excludes scaffold flags")
|
|
232
|
+
try:
|
|
233
|
+
root = _artifactlib._trusted_directory(args.root, "UNSAFE_ROOT")
|
|
234
|
+
request = _artifactlib.read_context_preview_request(root, args.context_request)
|
|
235
|
+
operation, payload = _artifactlib.validate_context_writer_request(request)
|
|
236
|
+
client = _artifactlib.context_writer_client(root)
|
|
237
|
+
if operation == "preview":
|
|
238
|
+
from _artifactauthoritylib import arm_context_preview
|
|
239
|
+
result = arm_context_preview(root, client, payload)
|
|
240
|
+
else:
|
|
241
|
+
result = client.call("context-apply", payload)
|
|
242
|
+
except RuntimeError as exc:
|
|
243
|
+
raise SystemExit(f"REFUSING context request: {exc}") from None
|
|
244
|
+
print(json.dumps(result, sort_keys=True))
|
|
245
|
+
return
|
|
246
|
+
if args.passive:
|
|
247
|
+
if not args.check or not args.root or args.stage is not None or args.repair_lock_exclusion:
|
|
248
|
+
ap.error("--passive requires --check and --root; excludes --stage and repair")
|
|
249
|
+
try:
|
|
250
|
+
report = passive_activation_inventory(args.root)
|
|
251
|
+
except (OSError, UnicodeError, ValueError) as exc:
|
|
252
|
+
raise SystemExit(f"REFUSING passive inspection: {exc}") from None
|
|
253
|
+
print(json.dumps(report, sort_keys=True))
|
|
254
|
+
return
|
|
134
255
|
if args.repair_lock_exclusion and (args.check or args.stage is not None):
|
|
135
256
|
ap.error("--repair-lock-exclusion cannot combine with --check or --stage")
|
|
257
|
+
requested_stage = args.stage
|
|
136
258
|
if args.stage is None:
|
|
137
259
|
args.stage = 1
|
|
138
260
|
|
|
@@ -149,13 +271,17 @@ def main(argv=None):
|
|
|
149
271
|
name = os.path.basename(root.rstrip("/\\")) or "project"
|
|
150
272
|
|
|
151
273
|
if args.check:
|
|
274
|
+
if os.path.lexists(cad) and not _regular_scaffold_entry(cad, directory=True):
|
|
275
|
+
raise SystemExit(f"REFUSING: {cad} is not a regular project-state directory.")
|
|
276
|
+
if os.path.lexists(ctx) and not _regular_scaffold_entry(ctx, directory=False):
|
|
277
|
+
raise SystemExit(f"REFUSING: {ctx} is not a regular scaffold file.")
|
|
152
278
|
if os.path.exists(ctx):
|
|
153
279
|
print(f"ALREADY SCAFFOLDED: {ctx} exists.")
|
|
154
280
|
with open(ctx, encoding="utf-8", errors="replace") as f:
|
|
155
281
|
text = f.read()
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
print("arbiter: " + ("enabled" if
|
|
282
|
+
initd = _hooklib.initialized_body_text(text)
|
|
283
|
+
enabled, _malformed = _hooklib.frontmatter_enabled_text(text)
|
|
284
|
+
print("arbiter: " + ("enabled" if enabled else "not enabled"))
|
|
159
285
|
print("initialized body: " + ("yes" if initd else
|
|
160
286
|
"no (run " + _cc + " or " + _dc + ")"))
|
|
161
287
|
else:
|
|
@@ -164,7 +290,8 @@ def main(argv=None):
|
|
|
164
290
|
|
|
165
291
|
from _taskexcludelib import ensure_task_lock_excluded, TaskExclusionError
|
|
166
292
|
if args.repair_lock_exclusion:
|
|
167
|
-
if not
|
|
293
|
+
if (not _regular_scaffold_entry(cad, directory=True) or
|
|
294
|
+
not _regular_scaffold_entry(ctx, directory=False)):
|
|
168
295
|
raise SystemExit("REFUSING: task lock exclusion repair requires an existing scaffold.")
|
|
169
296
|
try:
|
|
170
297
|
result = ensure_task_lock_excluded(root, required=True)
|
|
@@ -173,7 +300,8 @@ def main(argv=None):
|
|
|
173
300
|
print(f"task lock exclusion: {result}")
|
|
174
301
|
return
|
|
175
302
|
|
|
176
|
-
|
|
303
|
+
absent, recovering = _scaffold_inventory(cad, ctx, requested_stage)
|
|
304
|
+
if recovering and not absent:
|
|
177
305
|
raise SystemExit(
|
|
178
306
|
f"REFUSING: {ctx} already exists. .codearbiter/ is already scaffolded here. "
|
|
179
307
|
f"To populate it, run {_cc} or {_dc}; to repair, edit by hand.")
|
|
@@ -187,19 +315,16 @@ def main(argv=None):
|
|
|
187
315
|
|
|
188
316
|
os.makedirs(cad, exist_ok=True)
|
|
189
317
|
created = []
|
|
190
|
-
if
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
create_context=_cc, decompose=_dc))
|
|
318
|
+
if "CONTEXT.md" in absent:
|
|
319
|
+
_create_scaffold_file(ctx, CONTEXT.format(stage=args.stage, name=name,
|
|
320
|
+
create_context=_cc, decompose=_dc))
|
|
194
321
|
created.append("CONTEXT.md")
|
|
195
322
|
for fname, content in FILES.items():
|
|
196
|
-
|
|
197
|
-
if not os.path.exists(fp):
|
|
323
|
+
if fname in absent:
|
|
198
324
|
# Match taskwrite's LF output for this shared append-only template
|
|
199
325
|
# on Windows too; other scaffold files retain their existing EOLs.
|
|
200
326
|
newline = "\n" if fname == "done-tasks.md" else None
|
|
201
|
-
|
|
202
|
-
f.write(content)
|
|
327
|
+
_create_scaffold_file(os.path.join(cad, fname), content, newline=newline)
|
|
203
328
|
created.append(fname)
|
|
204
329
|
|
|
205
330
|
# #161: install the git-level enforcement backstop (pre-commit/pre-push) so
|
|
@@ -215,9 +340,12 @@ def main(argv=None):
|
|
|
215
340
|
except Exception as e: # noqa: BLE001
|
|
216
341
|
print(f"git hooks: install skipped ({e})", file=sys.stderr)
|
|
217
342
|
|
|
218
|
-
print(f"SCAFFOLDED .codearbiter/ at {cad}")
|
|
343
|
+
print(f"{'RECOVERED' if recovering else 'SCAFFOLDED'} .codearbiter/ at {cad}")
|
|
219
344
|
print("created: " + ", ".join(created))
|
|
220
|
-
|
|
345
|
+
if recovering and "CONTEXT.md" not in created:
|
|
346
|
+
print("arbiter: enabled; existing uninitialized CONTEXT.md and history retained.")
|
|
347
|
+
else:
|
|
348
|
+
print(f"arbiter: enabled (stage {args.stage}); CONTEXT.md is a stub (no <!--INITIALIZED--> sentinel).")
|
|
221
349
|
print(f"Next: run {_cc} (source exists) or {_dc} (greenfield) to populate.")
|
|
222
350
|
|
|
223
351
|
|
|
@@ -65,6 +65,7 @@ import sys
|
|
|
65
65
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
66
66
|
import hostapi # noqa: E402 — host seam (ADR-0011)
|
|
67
67
|
import _approvallib # noqa: E402 — host-owned artifact approval capture
|
|
68
|
+
import _artifactauthoritylib # noqa: E402 — exact context preview approval capture
|
|
68
69
|
import _hooklib # noqa: E402
|
|
69
70
|
import _modelib # noqa: E402 — mode plane core (T-06..T-16, Lane A)
|
|
70
71
|
import _prerequisitelib # noqa: E402 — host-owned prerequisite capture
|
|
@@ -485,6 +486,15 @@ def _handle_user_prompt_submit(payload, host):
|
|
|
485
486
|
)
|
|
486
487
|
if reconciliation:
|
|
487
488
|
contexts.append(reconciliation)
|
|
489
|
+
context_preview = _artifactauthoritylib.capture_context_preview_from_hook(
|
|
490
|
+
root=root,
|
|
491
|
+
plugin_root=host.plugin_root(),
|
|
492
|
+
prompt=prompt,
|
|
493
|
+
host=host.name,
|
|
494
|
+
session_id=session_id,
|
|
495
|
+
)
|
|
496
|
+
if context_preview:
|
|
497
|
+
contexts.append(context_preview)
|
|
488
498
|
approval_context = "\n".join(contexts)
|
|
489
499
|
if not active:
|
|
490
500
|
if not approval_context:
|
|
@@ -513,6 +523,10 @@ def _handle_precompact(payload, host):
|
|
|
513
523
|
if not _hooklib.arbiter_active(root):
|
|
514
524
|
return 0
|
|
515
525
|
_bump_compaction_generation(_hooklib.marker_root(payload), session_id)
|
|
526
|
+
# Read-time context pointers have their own worktree-local epoch. A
|
|
527
|
+
# surviving read marker cannot prove that a compacted actor still carries
|
|
528
|
+
# those pointers. Keep this separate from the persona generation above.
|
|
529
|
+
_readinjectlib.bump_context_generation(root, session_id)
|
|
516
530
|
return 0
|
|
517
531
|
|
|
518
532
|
|