@arbiterforge/ca-pi 0.15.10 → 0.16.0
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 +45 -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 +519 -16
- package/plugins/ca-pi/hooks/_artifactlib.py +244 -7
- package/plugins/ca-pi/hooks/_artifactpromptlib.py +106 -2
- package/plugins/ca-pi/hooks/_bashguardlib.py +23 -8
- 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 +2 -1
- 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/_sensitivelib.py +28 -14
- package/plugins/ca-pi/hooks/artifact-authority-hook.py +1 -0
- package/plugins/ca-pi/hooks/artifact-authority.py +25 -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/git-enforce.py +7 -6
- 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/artifacts.md +41 -0
- 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/includes/security-gate-record.md +5 -3
- 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
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
# own checkout — the root gate MARKERS
|
|
47
47
|
# (.codearbiter/.markers/) live under (#604)
|
|
48
48
|
# repo_rel(fpath, root) -> str repo-relative POSIX path, or "" if outside root
|
|
49
|
-
# line_digest(line) -> str
|
|
49
|
+
# line_digest(line, path=None) -> str path-bound sha256 of one diff line (H-09b/H-10b gate)
|
|
50
50
|
# content_digest(text) -> str sha256 hex of a whole file's content (H-14 gate)
|
|
51
51
|
# migration_globs(root) -> tuple[list, list] (includes, excludes) for migration detection
|
|
52
52
|
# scope_globs(root, defaults, decl_re) -> tuple[list, list] generic glob scope resolver
|
|
@@ -144,6 +144,7 @@ from _activationlib import ( # noqa: F401
|
|
|
144
144
|
arbiter_active,
|
|
145
145
|
frontmatter_enabled,
|
|
146
146
|
frontmatter_enabled_text,
|
|
147
|
+
initialized_body_text,
|
|
147
148
|
get_host,
|
|
148
149
|
marker_root,
|
|
149
150
|
project_root,
|
|
@@ -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
|
+
adapter_version = "0.16.0"
|
|
15
15
|
update_target = "ca-pi"
|
|
16
16
|
update_tag_prefix = "ca-pi-v"
|
|
17
17
|
update_command = "pi update npm:@arbiterforge/ca-pi"
|
|
@@ -239,18 +239,18 @@ def main(argv):
|
|
|
239
239
|
from _artifactlib import resolve_spec_file, helper_installation
|
|
240
240
|
client, entry = resolve_spec_file(positional[0], helper_installation(__file__))
|
|
241
241
|
result = client.call("validate", {"artifact_id": entry["artifact_id"], "gate": "ready"}, permit_invalid=True)
|
|
242
|
-
# Native success uses null for an empty diagnostic slice. Normalize
|
|
243
|
-
# only that documented absence, never arbitrary falsey malformed data.
|
|
244
242
|
if (result.get("artifact_id") != entry["artifact_id"]
|
|
245
243
|
or result.get("gate") != "ready"
|
|
246
244
|
or type(result.get("valid")) is not bool
|
|
247
|
-
or not
|
|
245
|
+
or not isinstance(result.get("model_sha256"), str)
|
|
246
|
+
or not re.fullmatch(r"[0-9a-f]{64}", result["model_sha256"])
|
|
247
|
+
or "diagnostics" not in result):
|
|
248
248
|
raise RuntimeError("malformed HTML validation result")
|
|
249
249
|
diagnostics = result.get("diagnostics")
|
|
250
250
|
if diagnostics is None:
|
|
251
251
|
diagnostics = []
|
|
252
252
|
if not isinstance(diagnostics, list):
|
|
253
|
-
raise
|
|
253
|
+
raise ValueError("HTML validation diagnostics must be a list or null")
|
|
254
254
|
findings = []
|
|
255
255
|
for item in diagnostics:
|
|
256
256
|
if (not isinstance(item, dict)
|
|
@@ -272,7 +272,7 @@ def main(argv):
|
|
|
272
272
|
findings.append("[UNCOVERED-CHECKBOX] " + box)
|
|
273
273
|
fresh = client.call("identity", {"artifact_id": entry["artifact_id"]})
|
|
274
274
|
if (fresh.get("artifact_id") != entry["artifact_id"]
|
|
275
|
-
or fresh
|
|
275
|
+
or fresh.get("model_sha256") != result["model_sha256"]):
|
|
276
276
|
raise RuntimeError("HTML spec changed during intent verification; retry a fresh read")
|
|
277
277
|
for finding in findings:
|
|
278
278
|
print(finding)
|
|
@@ -218,7 +218,7 @@ def is_marker_path(rel):
|
|
|
218
218
|
return bool(MARKERS_RE.search(norm_path(rel)))
|
|
219
219
|
|
|
220
220
|
|
|
221
|
-
def is_registered_state_path(rel):
|
|
221
|
+
def is_registered_state_path(rel, root=None):
|
|
222
222
|
"""True iff `rel` carries a policy in the protected-state registry
|
|
223
223
|
(`_protectedstatelib.lookup_policy`, B1/#564) — the fifth protected class
|
|
224
224
|
`classify_protected` recognizes (T-05a), evaluated strictly AFTER the
|
|
@@ -253,7 +253,7 @@ def is_registered_state_path(rel):
|
|
|
253
253
|
It is a local import, not file I/O or a git call, so it does not violate
|
|
254
254
|
this module's zero-side-effects-at-import invariant."""
|
|
255
255
|
import _protectedstatelib
|
|
256
|
-
return _protectedstatelib.lookup_policy(rel) is not None
|
|
256
|
+
return _protectedstatelib.lookup_policy(rel, root=root) is not None
|
|
257
257
|
|
|
258
258
|
|
|
259
259
|
def classify_protected(fpath, root):
|
|
@@ -315,7 +315,7 @@ def classify_protected(fpath, root):
|
|
|
315
315
|
for p in (raw_repo_rel(fpath, root), repo_rel(fpath, root)):
|
|
316
316
|
if not p:
|
|
317
317
|
continue
|
|
318
|
-
if is_registered_state_path(p):
|
|
318
|
+
if is_registered_state_path(p, root):
|
|
319
319
|
hits.add("state")
|
|
320
320
|
break
|
|
321
321
|
return hits
|
|
@@ -120,6 +120,9 @@
|
|
|
120
120
|
from __future__ import annotations
|
|
121
121
|
|
|
122
122
|
import os
|
|
123
|
+
import posixpath
|
|
124
|
+
import re
|
|
125
|
+
import stat
|
|
123
126
|
from enum import Enum
|
|
124
127
|
|
|
125
128
|
from _hooklib import MARKER_FRESHNESS_MINUTES, marker_fresh
|
|
@@ -211,6 +214,150 @@ REGISTRY: dict[str, ProtectedPolicy] = {
|
|
|
211
214
|
}
|
|
212
215
|
|
|
213
216
|
|
|
217
|
+
def context_writer_protection() -> dict[str, ProtectedPolicy]:
|
|
218
|
+
"""Return context candidates only after a fresh shipped-writer check.
|
|
219
|
+
|
|
220
|
+
Called only while resolving a context target, never at module import.
|
|
221
|
+
"""
|
|
222
|
+
import _artifactlib
|
|
223
|
+
|
|
224
|
+
if not _artifactlib.context_writer_qualified():
|
|
225
|
+
return {}
|
|
226
|
+
return {path: ProtectedPolicy.HELPER_ONLY
|
|
227
|
+
for path in _artifactlib._CONTEXT_TARGETS.values()}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _committed_context_journal(root, document_id, target):
|
|
231
|
+
"""Find the native writer's durable two-target commit, with bounded reads.
|
|
232
|
+
|
|
233
|
+
None means the journal inventory is uncertain: protect context paths until
|
|
234
|
+
it is reconciled. An empty or absent directory is not enrollment.
|
|
235
|
+
"""
|
|
236
|
+
import _artifactlib
|
|
237
|
+
|
|
238
|
+
directory = os.path.join(root, ".codearbiter", ".artifacts", "transactions")
|
|
239
|
+
for parent in (os.path.join(root, ".codearbiter"),
|
|
240
|
+
os.path.join(root, ".codearbiter", ".artifacts"), directory):
|
|
241
|
+
if os.path.islink(parent):
|
|
242
|
+
return None
|
|
243
|
+
if not os.path.lexists(directory):
|
|
244
|
+
return False
|
|
245
|
+
if not os.path.isdir(directory):
|
|
246
|
+
return None
|
|
247
|
+
try:
|
|
248
|
+
entries = []
|
|
249
|
+
with os.scandir(directory) as scan:
|
|
250
|
+
for entry in scan:
|
|
251
|
+
entries.append(entry)
|
|
252
|
+
if len(entries) > 1024:
|
|
253
|
+
return None
|
|
254
|
+
total_bytes = 0
|
|
255
|
+
for entry in entries:
|
|
256
|
+
if entry.name.endswith(".next"):
|
|
257
|
+
continue
|
|
258
|
+
if (not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]{7,79}\.json", entry.name)
|
|
259
|
+
or not entry.is_file(follow_symlinks=False)):
|
|
260
|
+
return None
|
|
261
|
+
descriptor = _artifactlib._open_pinned_regular(entry.path)
|
|
262
|
+
try:
|
|
263
|
+
info = os.fstat(descriptor)
|
|
264
|
+
total_bytes += info.st_size
|
|
265
|
+
if (info.st_size > 65536 or info.st_size <= 0 or total_bytes > 8 << 20
|
|
266
|
+
or not stat.S_ISREG(info.st_mode)
|
|
267
|
+
or getattr(info, "st_file_attributes", 0) &
|
|
268
|
+
getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0)):
|
|
269
|
+
return None
|
|
270
|
+
raw = os.read(descriptor, 65537)
|
|
271
|
+
finally:
|
|
272
|
+
os.close(descriptor)
|
|
273
|
+
journal = _artifactlib._decode(raw)
|
|
274
|
+
if not isinstance(journal, dict):
|
|
275
|
+
return None
|
|
276
|
+
historical = journal.get("format") == "codearbiter.transaction/0.1.0"
|
|
277
|
+
if (journal.get("format") not in {"codearbiter.transaction/0.1.0",
|
|
278
|
+
"codearbiter.transaction/0.2.0"}
|
|
279
|
+
or set(journal) != ({"format", "operation_id", "request_sha256",
|
|
280
|
+
"state", "entries"} if historical else
|
|
281
|
+
{"format", "operation_id", "request_sha256",
|
|
282
|
+
"state", "entries", "result"})
|
|
283
|
+
or journal.get("operation_id") != entry.name[:-5]
|
|
284
|
+
or not isinstance(journal.get("request_sha256"), str)
|
|
285
|
+
or re.fullmatch(r"[0-9a-f]{64}", journal["request_sha256"]) is None
|
|
286
|
+
or journal.get("state") not in {"prepared", "committed", "rolled_back"}):
|
|
287
|
+
return None
|
|
288
|
+
rows = journal.get("entries")
|
|
289
|
+
if (not isinstance(rows, list) or not 1 <= len(rows) <= 128
|
|
290
|
+
or any(not isinstance(row, dict)
|
|
291
|
+
or set(row) != {"path", "stage", "backup", "before_sha256",
|
|
292
|
+
"after_sha256", "size"}
|
|
293
|
+
or not isinstance(row["path"], str)
|
|
294
|
+
or not row["path"] or row["path"].startswith("/")
|
|
295
|
+
or posixpath.normpath(row["path"]) != row["path"]
|
|
296
|
+
or any(part in {"", ".", ".."} or part.endswith((".", " "))
|
|
297
|
+
or re.fullmatch(
|
|
298
|
+
r"(?i)(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\..*)?",
|
|
299
|
+
part) is not None
|
|
300
|
+
for part in row["path"].split("/"))
|
|
301
|
+
or any(char in row["path"] for char in '\\:*?<>|"')
|
|
302
|
+
or any(ord(char) < 32 or ord(char) == 127 for char in row["path"])
|
|
303
|
+
or not isinstance(row["after_sha256"], str)
|
|
304
|
+
or re.fullmatch(r"[0-9a-f]{64}", row["after_sha256"]) is None
|
|
305
|
+
or (row["before_sha256"] is not None and
|
|
306
|
+
(not isinstance(row["before_sha256"], str) or
|
|
307
|
+
re.fullmatch(r"[0-9a-f]{64}", row["before_sha256"]) is None))
|
|
308
|
+
or type(row["size"]) is not int or not 0 <= row["size"] <= 8 << 20
|
|
309
|
+
or row["stage"] !=
|
|
310
|
+
f"{os.path.dirname(row['path'])}/.ca-artifact-{journal['operation_id']}-{i}.new"
|
|
311
|
+
or row["backup"] !=
|
|
312
|
+
f".codearbiter/.artifacts/history/{journal['operation_id']}-{i}.before"
|
|
313
|
+
for i, row in enumerate(rows))):
|
|
314
|
+
return None
|
|
315
|
+
paths = [row["path"] for row in rows]
|
|
316
|
+
if historical:
|
|
317
|
+
if target in paths:
|
|
318
|
+
return None
|
|
319
|
+
continue
|
|
320
|
+
result = journal.get("result")
|
|
321
|
+
if not isinstance(result, dict):
|
|
322
|
+
if target in paths:
|
|
323
|
+
return None
|
|
324
|
+
continue
|
|
325
|
+
if result.get("document_id") != document_id or result.get("target_path") != target:
|
|
326
|
+
if target in paths:
|
|
327
|
+
return None
|
|
328
|
+
continue
|
|
329
|
+
if (set(result) != {"document_id", "target_path", "document_sha256",
|
|
330
|
+
"provenance_sha256", "mode"}
|
|
331
|
+
or result["mode"] not in {"create", "adopt", "update"}
|
|
332
|
+
or len(rows) != 2
|
|
333
|
+
or paths !=
|
|
334
|
+
[target, f".codearbiter/.provenance/{document_id}.json"]
|
|
335
|
+
or rows[0].get("after_sha256") != result["document_sha256"]
|
|
336
|
+
or rows[1].get("after_sha256") != result["provenance_sha256"]):
|
|
337
|
+
return None
|
|
338
|
+
if journal.get("state") == "committed":
|
|
339
|
+
return True
|
|
340
|
+
if journal.get("state") == "prepared":
|
|
341
|
+
return None
|
|
342
|
+
return False
|
|
343
|
+
except (OSError, ValueError, _artifactlib.ArtifactError):
|
|
344
|
+
return None
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _context_policy(rel_path, root):
|
|
348
|
+
import _artifactlib
|
|
349
|
+
|
|
350
|
+
document_id = next((doc for doc, target in _artifactlib._CONTEXT_TARGETS.items()
|
|
351
|
+
if _canon(target) == _canon(rel_path)), None)
|
|
352
|
+
if document_id is None:
|
|
353
|
+
return None
|
|
354
|
+
target = _artifactlib._CONTEXT_TARGETS[document_id]
|
|
355
|
+
enrolled = _committed_context_journal(root, document_id, target)
|
|
356
|
+
if enrolled is not False:
|
|
357
|
+
return ProtectedPolicy.HELPER_ONLY
|
|
358
|
+
return context_writer_protection().get(target)
|
|
359
|
+
|
|
360
|
+
|
|
214
361
|
def _canon(rel_path):
|
|
215
362
|
"""Canonical comparison form of a repo-relative path: separator-
|
|
216
363
|
normalized (`norm_path`), then whitespace-stripped, `./`-prefix-
|
|
@@ -263,7 +410,7 @@ def _canon(rel_path):
|
|
|
263
410
|
return p.lower()
|
|
264
411
|
|
|
265
412
|
|
|
266
|
-
def lookup_policy(rel_path, registry=None):
|
|
413
|
+
def lookup_policy(rel_path, registry=None, *, root=None):
|
|
267
414
|
"""The ProtectedPolicy registered for `rel_path`, or None if it carries
|
|
268
415
|
no policy. Both `rel_path` and every registry key are canonicalized
|
|
269
416
|
(`_canon`, above) before comparison — separator-normalized, `./`/
|
|
@@ -284,12 +431,15 @@ def lookup_policy(rel_path, registry=None):
|
|
|
284
431
|
and realpath-resolved forms of a path - a flank that calls
|
|
285
432
|
`lookup_policy` directly with its own ad hoc dict re-opens the symlink
|
|
286
433
|
alias this module does not itself guard against."""
|
|
434
|
+
dynamic = registry is None and root is not None
|
|
287
435
|
if registry is None:
|
|
288
436
|
registry = REGISTRY
|
|
289
437
|
normalized = _canon(rel_path)
|
|
290
438
|
for key, policy in registry.items():
|
|
291
439
|
if _canon(key) == normalized:
|
|
292
440
|
return policy
|
|
441
|
+
if dynamic:
|
|
442
|
+
return _context_policy(rel_path, root)
|
|
293
443
|
return None
|
|
294
444
|
|
|
295
445
|
|
|
@@ -341,7 +491,7 @@ def resolve_registered_path(fpath, root, registry=None):
|
|
|
341
491
|
for p in (raw_repo_rel(fpath, root), repo_rel(fpath, root)):
|
|
342
492
|
if not p:
|
|
343
493
|
continue
|
|
344
|
-
policy = lookup_policy(p, registry)
|
|
494
|
+
policy = lookup_policy(p, registry, root=root)
|
|
345
495
|
if policy is not None:
|
|
346
496
|
return p, policy
|
|
347
497
|
return None, None
|