@arbiterforge/ca-pi 0.10.12 → 0.14.6
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 +6 -5
- package/package.json +2 -1
- package/plugins/ca-pi/CHANGELOG.md +165 -0
- package/plugins/ca-pi/SKILLS.md +1 -1
- package/plugins/ca-pi/SPRINT.md +40 -10
- package/plugins/ca-pi/agents/backend-author.md +1 -1
- package/plugins/ca-pi/agents/frontend-author.md +1 -1
- 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 +38 -0
- package/plugins/ca-pi/hooks/_approvallib.py +355 -0
- package/plugins/ca-pi/hooks/_artifactlib.py +855 -0
- package/plugins/ca-pi/hooks/_cleanuplib.py +1553 -0
- package/plugins/ca-pi/hooks/_hooklib.py +37 -13
- package/plugins/ca-pi/hooks/_host.py +1 -1
- package/plugins/ca-pi/hooks/_intentlib.py +31 -0
- package/plugins/ca-pi/hooks/_planfilelib.py +34 -1
- package/plugins/ca-pi/hooks/_protectedstatelib.py +11 -1
- package/plugins/ca-pi/hooks/_readinjectlib.py +120 -0
- package/plugins/ca-pi/hooks/_releaselib.py +933 -85
- package/plugins/ca-pi/hooks/_standuplib.py +38 -10
- package/plugins/ca-pi/hooks/doctor.py +103 -0
- package/plugins/ca-pi/hooks/hostapi.py +1 -1
- package/plugins/ca-pi/hooks/pi-bridge.py +5 -1
- package/plugins/ca-pi/hooks/prompt-submit.py +33 -10
- package/plugins/ca-pi/hooks/releasehash.py +46 -32
- package/plugins/ca-pi/hooks/session-start.py +5 -4
- package/plugins/ca-pi/includes/artifacts.md +166 -0
- package/plugins/ca-pi/includes/author-tdd-workflow.md +2 -1
- package/plugins/ca-pi/includes/farm.md +27 -4
- package/plugins/ca-pi/includes/routing-table.md +2 -2
- package/plugins/ca-pi/includes/verification-boundary.md +27 -0
- package/plugins/ca-pi/package.json +18 -0
- package/plugins/ca-pi/routines/INDEX.md +4 -4
- package/plugins/ca-pi/routines/brainstorming/SKILL.md +23 -4
- package/plugins/ca-pi/routines/commit-gate/SKILL.md +30 -5
- package/plugins/ca-pi/routines/executing-plans/SKILL.md +52 -7
- package/plugins/ca-pi/routines/finishing-a-development-branch/SKILL.md +25 -3
- package/plugins/ca-pi/routines/refactor/SKILL.md +3 -2
- package/plugins/ca-pi/routines/release/SKILL.md +150 -44
- package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +53 -9
- package/plugins/ca-pi/routines/subagent-driven-development/references/farm-dispatch.md +20 -3
- package/plugins/ca-pi/routines/tdd/SKILL.md +27 -5
- package/plugins/ca-pi/routines/using-git-worktrees/SKILL.md +11 -2
- package/plugins/ca-pi/routines/writing-plans/SKILL.md +39 -10
- package/plugins/ca-pi/routines/writing-plans/references/farm-plan.md +34 -13
- package/plugins/ca-pi/skills/ca-chore/SKILL.md +7 -3
- package/plugins/ca-pi/skills/ca-commit/SKILL.md +3 -2
- package/plugins/ca-pi/skills/ca-doctor/SKILL.md +14 -0
- package/plugins/ca-pi/skills/ca-feature/SKILL.md +42 -8
- package/plugins/ca-pi/skills/ca-refactor/SKILL.md +4 -2
- package/plugins/ca-pi/skills/ca-release/SKILL.md +7 -6
- package/plugins/ca-pi/skills/ca-standup/SKILL.md +36 -8
|
@@ -92,7 +92,8 @@
|
|
|
92
92
|
# restating the literal.
|
|
93
93
|
# marker_fresh(path, minutes) -> bool True iff marker file exists and is recent
|
|
94
94
|
# write_text_atomic(path, text) -> None crash-safe write (temp + os.replace)
|
|
95
|
-
# acquire_lock(path) -> handle|None
|
|
95
|
+
# acquire_lock(path, wait_seconds=None) -> handle|None
|
|
96
|
+
# OS-owned cross-process file lock (#271 C-2);
|
|
96
97
|
# non-blocking + bounded LOCK_WAIT retry spin,
|
|
97
98
|
# fail-soft None on contention/timeout/OSError
|
|
98
99
|
# release_lock(handle) -> None release + close; None handle is a no-op
|
|
@@ -222,6 +223,7 @@ from _sensitivelib import ( # noqa: F401
|
|
|
222
223
|
# POSIX flock and Windows byte-range locks are process-scoped, so sibling
|
|
223
224
|
# threads can otherwise burn the shared wait budget contending with each other.
|
|
224
225
|
_GATE_EVENTS_PROCESS_LOCK = threading.Lock()
|
|
226
|
+
GATE_EVENT_LOCK_WAIT_SECONDS = 5.0
|
|
225
227
|
_WINDOWS_LOCK_TIMEOUT_SECONDS = 5.0
|
|
226
228
|
_WINDOWS_LOCK_RETRY_SECONDS = 0.01
|
|
227
229
|
_WINDOWS_CRT_EDEADLK = 36
|
|
@@ -345,7 +347,7 @@ def write_text_atomic(path, text, newline=None):
|
|
|
345
347
|
raise
|
|
346
348
|
|
|
347
349
|
|
|
348
|
-
def acquire_lock(path):
|
|
350
|
+
def acquire_lock(path, wait_seconds=None):
|
|
349
351
|
"""Acquire an OS-owned cross-process file lock keyed on `path`; process
|
|
350
352
|
death releases it automatically (#271 C-2 — hoisted from the
|
|
351
353
|
statusline-ledger-only `_ledgerlib._acquire_lock`, now shared with
|
|
@@ -355,7 +357,8 @@ def acquire_lock(path):
|
|
|
355
357
|
with one byte so the OS byte-range lock has a byte to lock (an empty file
|
|
356
358
|
has no range to range-lock). Non-blocking (`msvcrt.locking(..., LK_NBLCK,
|
|
357
359
|
1)` on Windows, `fcntl.flock(..., LOCK_EX | LOCK_NB)` elsewhere) with a
|
|
358
|
-
bounded `LOCK_WAIT`-second retry spin
|
|
360
|
+
bounded `LOCK_WAIT`-second retry spin (or an explicit non-negative
|
|
361
|
+
`wait_seconds` budget); any `OSError` opening the lock file,
|
|
359
362
|
or exhausting the deadline still contended, is FAIL-SOFT: returns `None`
|
|
360
363
|
rather than raising or blocking indefinitely. Callers decide what
|
|
361
364
|
"fail-soft" means for them — `_ledgerlib.ledger_update`/`persist_sess_start`
|
|
@@ -373,7 +376,8 @@ def acquire_lock(path):
|
|
|
373
376
|
handle.flush()
|
|
374
377
|
except OSError:
|
|
375
378
|
return None
|
|
376
|
-
|
|
379
|
+
wait_seconds = LOCK_WAIT if wait_seconds is None else max(0.0, wait_seconds)
|
|
380
|
+
deadline = time.monotonic() + wait_seconds
|
|
377
381
|
while True:
|
|
378
382
|
try:
|
|
379
383
|
handle.seek(0)
|
|
@@ -493,9 +497,22 @@ def marker_fresh(path, minutes):
|
|
|
493
497
|
def _log_gate_event(kind, tag, msg):
|
|
494
498
|
"""Best-effort durable append of one gate decision to
|
|
495
499
|
.codearbiter/gate-events.log (observability-001, issue #186) — the durable
|
|
496
|
-
sink block()
|
|
497
|
-
|
|
498
|
-
|
|
500
|
+
sink block() funnels every BLOCK through, so a decision is no longer
|
|
501
|
+
visible ONLY in the ephemeral per-turn stderr transcript.
|
|
502
|
+
|
|
503
|
+
remind()/warn() do NOT call this (2026-09-17 scope-down): measured on a
|
|
504
|
+
live repo, REMIND/WARN accounted for 76% of the file's lines and 60% were
|
|
505
|
+
one repeated boilerplate reminder carrying no information past its first
|
|
506
|
+
occurrence, while the file's git-tracked + H-05 append-only status gave
|
|
507
|
+
that noise the full cost of a protected audit artifact — two branches
|
|
508
|
+
that both merely touched it, independently, produced a real merge
|
|
509
|
+
conflict; _releaselib.py's clean-tree probe carries its own comment about
|
|
510
|
+
working around a mid-window REMIND/WARN append poisoning a release gate.
|
|
511
|
+
BLOCK is the one kind with real forensic value (evidence for diagnosing a
|
|
512
|
+
hook that misfired); REMIND/WARN remain full-strength on stderr, just not
|
|
513
|
+
persisted. Not ADR-governed — issue #186 shipped this as an implementation
|
|
514
|
+
detail of `_hooklib`, not an architectural decision, so scoping it back
|
|
515
|
+
down is the same kind of call.
|
|
499
516
|
|
|
500
517
|
One line per event: `[ISO-8601Z] KIND [tag] host=<host> hook=<script> | msg`.
|
|
501
518
|
`tag` may be None (warn() carries no tag) — the bracket is simply omitted
|
|
@@ -538,7 +555,10 @@ def _log_gate_event(kind, tag, msg):
|
|
|
538
555
|
try:
|
|
539
556
|
_GATE_EVENTS_PROCESS_LOCK.acquire()
|
|
540
557
|
process_lock_acquired = True
|
|
541
|
-
audit_lock = acquire_lock(
|
|
558
|
+
audit_lock = acquire_lock(
|
|
559
|
+
audit_lock_key(root, log_path),
|
|
560
|
+
wait_seconds=GATE_EVENT_LOCK_WAIT_SECONDS,
|
|
561
|
+
)
|
|
542
562
|
except Exception:
|
|
543
563
|
if process_lock_acquired:
|
|
544
564
|
_GATE_EVENTS_PROCESS_LOCK.release()
|
|
@@ -551,7 +571,10 @@ def _log_gate_event(kind, tag, msg):
|
|
|
551
571
|
os_lock_acquired = False
|
|
552
572
|
try:
|
|
553
573
|
fd = os.open(log_path, flags, 0o600)
|
|
554
|
-
|
|
574
|
+
# The trusted sidecar already serializes current writers. Keep the
|
|
575
|
+
# legacy descriptor lock for compatibility once the log has data,
|
|
576
|
+
# but Windows cannot reliably lock byte zero of a new empty file.
|
|
577
|
+
if os.name == "nt" and os.fstat(fd).st_size:
|
|
555
578
|
import msvcrt
|
|
556
579
|
os.lseek(fd, 0, os.SEEK_SET)
|
|
557
580
|
lock_mode = getattr(msvcrt, "LK_NBLCK", msvcrt.LK_LOCK)
|
|
@@ -596,14 +619,15 @@ def block(tag, msg):
|
|
|
596
619
|
|
|
597
620
|
|
|
598
621
|
def remind(tag, msg):
|
|
599
|
-
"""Non-blocking nudge to stderr.
|
|
600
|
-
|
|
622
|
+
"""Non-blocking nudge to stderr. Deliberately NOT persisted to
|
|
623
|
+
gate-events.log (see _log_gate_event's 2026-09-17 scope-down note)."""
|
|
601
624
|
print(f"REMINDER [{tag}]: {msg}", file=sys.stderr)
|
|
602
625
|
|
|
603
626
|
|
|
604
627
|
def warn(msg):
|
|
605
|
-
"""Loud degradation/diagnostic breadcrumb — never silent.
|
|
606
|
-
|
|
628
|
+
"""Loud degradation/diagnostic breadcrumb — never silent. Deliberately
|
|
629
|
+
NOT persisted to gate-events.log (see _log_gate_event's 2026-09-17
|
|
630
|
+
scope-down note)."""
|
|
607
631
|
print(f"codeArbiter hook: {msg}", file=sys.stderr)
|
|
608
632
|
|
|
609
633
|
|
|
@@ -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.14.6"
|
|
15
15
|
update_target = "ca-pi"
|
|
16
16
|
update_tag_prefix = "ca-pi-v"
|
|
17
17
|
update_command = "pi update npm:@arbiterforge/ca-pi"
|
|
@@ -168,6 +168,8 @@ def uncovered_intent(spec_text, issue_body=None):
|
|
|
168
168
|
docstring for what it cannot catch."""
|
|
169
169
|
if not isinstance(spec_text, str):
|
|
170
170
|
spec_text = ""
|
|
171
|
+
if spec_text.lstrip().lower().startswith(("<!doctype html", "<html")):
|
|
172
|
+
return ["[HTML-REQUIRES-VALIDATION] Use the path-based artifact validator; HTML is not empty Markdown."]
|
|
171
173
|
criteria_tokens = set()
|
|
172
174
|
for criterion in _criteria_texts(spec_text):
|
|
173
175
|
criteria_tokens.update(_tokens(criterion))
|
|
@@ -232,6 +234,35 @@ def main(argv):
|
|
|
232
234
|
sys.stderr.write(f"_intentlib.py: cannot read {issue_body_path}: {exc}\n")
|
|
233
235
|
return 2
|
|
234
236
|
|
|
237
|
+
if positional[0].lower().endswith(".html"):
|
|
238
|
+
try:
|
|
239
|
+
from _artifactlib import resolve_spec_file, helper_installation
|
|
240
|
+
client, entry = resolve_spec_file(positional[0], helper_installation(__file__))
|
|
241
|
+
result = client.call("validate", {"artifact_id": entry["artifact_id"], "gate": "ready"}, permit_invalid=True)
|
|
242
|
+
findings = ["[INVALID-HTML-SPEC] " + item["code"] + ": " + item["message"]
|
|
243
|
+
for item in result.get("diagnostics", [])]
|
|
244
|
+
# Explicit source->criterion mappings replace fuzzy scope matching.
|
|
245
|
+
# The legacy issue-checkbox heuristic is retained ONLY for external
|
|
246
|
+
# issue text until issue-source records receive their own schema.
|
|
247
|
+
if issue_body:
|
|
248
|
+
criteria_tokens = set()
|
|
249
|
+
for record in client.outline(entry["artifact_id"]):
|
|
250
|
+
if record["kind"] == "criteria" and not record["retired"]:
|
|
251
|
+
data = client.call("read", {"artifact_id": entry["artifact_id"], "symbol": record["id"], "mode": "exact"})
|
|
252
|
+
criteria_tokens.update(_tokens(data["record"].get("statement", "")))
|
|
253
|
+
for box in _checkboxes(issue_body):
|
|
254
|
+
if not _cited(box, criteria_tokens):
|
|
255
|
+
findings.append("[UNCOVERED-CHECKBOX] " + box)
|
|
256
|
+
fresh = client.call("identity", {"artifact_id": entry["artifact_id"]})
|
|
257
|
+
if fresh["model_sha256"] != result["model_sha256"]:
|
|
258
|
+
raise RuntimeError("HTML spec changed during intent verification; retry a fresh read")
|
|
259
|
+
for finding in findings:
|
|
260
|
+
print(finding)
|
|
261
|
+
return 1 if findings or not result["valid"] else 0
|
|
262
|
+
except Exception as error:
|
|
263
|
+
sys.stderr.write("_intentlib.py: HTML validation failed: " + str(error)[:512] + "\n")
|
|
264
|
+
return 2
|
|
265
|
+
|
|
235
266
|
findings = uncovered_intent(spec_text, issue_body)
|
|
236
267
|
for finding in findings:
|
|
237
268
|
print(finding)
|
|
@@ -70,6 +70,30 @@ def _request_path(root, slug, kind):
|
|
|
70
70
|
return os.path.join(root, ".codearbiter", KINDS[kind], f"{slug}.md")
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
def _require_legacy_pair(root, slug):
|
|
74
|
+
"""Refuse Pi's Markdown-only plan surface unless one exact legacy pair owns the slug."""
|
|
75
|
+
_request_path(root, slug, "spec")
|
|
76
|
+
root_info = os.lstat(root)
|
|
77
|
+
if not _directory(root_info):
|
|
78
|
+
raise PlanFileError("root")
|
|
79
|
+
state_path, _ = _exact_absolute_child(root, ".codearbiter")
|
|
80
|
+
for kind in KINDS:
|
|
81
|
+
parent_path, _ = _exact_absolute_child(state_path, KINDS[kind])
|
|
82
|
+
html_leaf = f"{slug}.html"
|
|
83
|
+
if _aliases(parent_path, html_leaf):
|
|
84
|
+
raise PlanFileError("html_authority")
|
|
85
|
+
markdown_leaf = f"{slug}.md"
|
|
86
|
+
aliases = _aliases(parent_path, markdown_leaf)
|
|
87
|
+
if not aliases:
|
|
88
|
+
raise PlanFileError("legacy_pair_required")
|
|
89
|
+
if aliases != [markdown_leaf]:
|
|
90
|
+
raise PlanFileError("path_alias")
|
|
91
|
+
path = os.path.join(parent_path, markdown_leaf)
|
|
92
|
+
info = os.lstat(path)
|
|
93
|
+
if not _regular_single(info) or os.path.realpath(path) != os.path.abspath(path):
|
|
94
|
+
raise PlanFileError("path_type")
|
|
95
|
+
|
|
96
|
+
|
|
73
97
|
def _windows_handle_identity(handle):
|
|
74
98
|
from ctypes import wintypes
|
|
75
99
|
|
|
@@ -580,7 +604,8 @@ def _committed_result(path, data, content, fault=None, initial_diagnostic=None):
|
|
|
580
604
|
return result
|
|
581
605
|
|
|
582
606
|
|
|
583
|
-
def plan_file_operation(root, request, *, lock_root=None, fault=None
|
|
607
|
+
def plan_file_operation(root, request, *, lock_root=None, fault=None,
|
|
608
|
+
require_legacy_pair=False):
|
|
584
609
|
"""Run a bounded CAS. The lock serializes cooperating codeArbiter Pi writers only."""
|
|
585
610
|
try:
|
|
586
611
|
if not isinstance(request, dict) or set(request) - {"slug", "kind", "action", "expectedHash", "content"}:
|
|
@@ -593,7 +618,13 @@ def plan_file_operation(root, request, *, lock_root=None, fault=None):
|
|
|
593
618
|
if action not in ("read", "replace"):
|
|
594
619
|
raise PlanFileError("request")
|
|
595
620
|
target = _request_path(root, request.get("slug"), request.get("kind"))
|
|
621
|
+
if require_legacy_pair is not False:
|
|
622
|
+
if require_legacy_pair is not True:
|
|
623
|
+
raise PlanFileError("request")
|
|
624
|
+
_require_legacy_pair(root, request["slug"])
|
|
596
625
|
with _lock_for(target, lock_root), _held_path(root, request["slug"], request["kind"], fault) as path:
|
|
626
|
+
if require_legacy_pair:
|
|
627
|
+
_require_legacy_pair(root, request["slug"])
|
|
597
628
|
chosen = _read_target(path, fault)
|
|
598
629
|
if action == "read":
|
|
599
630
|
if chosen is None:
|
|
@@ -625,6 +656,8 @@ def plan_file_operation(root, request, *, lock_root=None, fault=None):
|
|
|
625
656
|
for phase in ("before_publish", "publish"):
|
|
626
657
|
if fault:
|
|
627
658
|
fault(phase, path)
|
|
659
|
+
if require_legacy_pair:
|
|
660
|
+
_require_legacy_pair(root, request["slug"])
|
|
628
661
|
try:
|
|
629
662
|
latest = _read_target(path)
|
|
630
663
|
except Exception:
|
|
@@ -381,6 +381,16 @@ def marker_name_for(rel_path):
|
|
|
381
381
|
(non-empty) rel_path can ever produce, since a real segment always
|
|
382
382
|
contributes a non-empty stem.
|
|
383
383
|
|
|
384
|
+
Every segment is case-folded (`.lower()`) before encoding (#624), matching
|
|
385
|
+
`_canon`/`lookup_policy`'s own case-insensitive registry comparison: a
|
|
386
|
+
marker name computed for a differently-cased Write/Edit target must equal
|
|
387
|
+
the one minted under the registry's canonical casing, or a legitimate
|
|
388
|
+
authoring write is wrongly blocked despite a fresh, correctly-minted
|
|
389
|
+
marker. This widens what two paths can collide into one marker (a
|
|
390
|
+
same-name directory differing only by case now shares a marker with its
|
|
391
|
+
canonical sibling) - the same accepted "widens admission, never narrows
|
|
392
|
+
protection" tradeoff `lookup_policy`'s own case fold already makes.
|
|
393
|
+
|
|
384
394
|
Residual (accepted): the escaping above closes hyphen/segment-boundary
|
|
385
395
|
ambiguity but is not a fully bijective encoding against adversarially
|
|
386
396
|
crafted underscore runs (e.g. a directory literally named "a_" holding
|
|
@@ -388,7 +398,7 @@ def marker_name_for(rel_path):
|
|
|
388
398
|
small, curated, human-authored set (spec B1/B2), not attacker-chosen
|
|
389
399
|
directory names, so this is judged out of proportion to close fully
|
|
390
400
|
here."""
|
|
391
|
-
parts = [p for p in norm_path(rel_path).split("/") if p not in ("", ".", "..")]
|
|
401
|
+
parts = [p for p in norm_path(rel_path).lower().split("/") if p not in ("", ".", "..")]
|
|
392
402
|
if not parts:
|
|
393
403
|
return "-authoring"
|
|
394
404
|
if parts[0] == ".codearbiter" and len(parts) > 1:
|
|
@@ -1084,3 +1084,123 @@ def compute_injection(root, session_id, rel, runner=None):
|
|
|
1084
1084
|
return ""
|
|
1085
1085
|
except Exception: # noqa: BLE001
|
|
1086
1086
|
return ""
|
|
1087
|
+
|
|
1088
|
+
# Typed artifact pilot bridge. Keep legacy matching unchanged; never cache HTML
|
|
1089
|
+
# authority by mtime. Importing this module performs no subprocess or file I/O.
|
|
1090
|
+
_artifact_legacy_build_index = build_index
|
|
1091
|
+
_artifact_legacy_spec_pointers = spec_pointers
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
def build_index(root):
|
|
1095
|
+
result = _artifact_legacy_build_index(root)
|
|
1096
|
+
if not isinstance(result, dict):
|
|
1097
|
+
return result
|
|
1098
|
+
result = dict(result)
|
|
1099
|
+
try:
|
|
1100
|
+
from _artifactlib import ArtifactClient, helper_installation, has_html
|
|
1101
|
+
if not has_html(root):
|
|
1102
|
+
return result
|
|
1103
|
+
# Preserve full engine identities in the advisory snapshot. Pointer
|
|
1104
|
+
# matching uses only spec/path/globs, while dedup below hashes the same
|
|
1105
|
+
# entries including revision/model/normative identity. Do not reread the
|
|
1106
|
+
# artifact after this validated index stream.
|
|
1107
|
+
extra = []
|
|
1108
|
+
client = ArtifactClient(root, helper_installation(__file__))
|
|
1109
|
+
for entry in client.index("spec"):
|
|
1110
|
+
authority = entry["authority"]
|
|
1111
|
+
if authority["authority_verified"] and entry.get("governs"):
|
|
1112
|
+
extra.append({
|
|
1113
|
+
"spec": entry["artifact_id"],
|
|
1114
|
+
"path": entry["path"],
|
|
1115
|
+
"globs": entry["governs"],
|
|
1116
|
+
"artifact_html": True,
|
|
1117
|
+
"revision": entry.get("revision"),
|
|
1118
|
+
"model_sha256": entry.get("model_sha256"),
|
|
1119
|
+
"normative_sha256": entry.get("normative_sha256"),
|
|
1120
|
+
})
|
|
1121
|
+
except Exception as error:
|
|
1122
|
+
# Advisory read injection remains fail-open. This warning is NOT an
|
|
1123
|
+
# approved-spec pointer and MUST NOT authorize a planning/execution gate.
|
|
1124
|
+
code = getattr(error, "code", "CAPABILITY_MISSING")
|
|
1125
|
+
code = str(code)[:80]
|
|
1126
|
+
capability_codes = {
|
|
1127
|
+
"CAPABILITY_MISSING", "INVALID_INSTALLATION", "PACKAGE_INTEGRITY",
|
|
1128
|
+
"UNSUPPORTED_PLATFORM", "UNSUPPORTED_VERSION", "UNVERIFIED_PLATFORM",
|
|
1129
|
+
"INVALID_RESPONSE", "RESPONSE_TOO_LARGE", "SUBPROCESS_FAILED", "TIMEOUT",
|
|
1130
|
+
}
|
|
1131
|
+
extra = [{
|
|
1132
|
+
"artifact_diagnostic": code,
|
|
1133
|
+
"artifact_diagnostic_kind": (
|
|
1134
|
+
"capability" if code in capability_codes else "invalid-artifact"
|
|
1135
|
+
),
|
|
1136
|
+
"artifact_html": True,
|
|
1137
|
+
}]
|
|
1138
|
+
result["spec"] = [item for item in result.get("spec", [])
|
|
1139
|
+
if not isinstance(item, dict) or not item.get("artifact_html")] + extra
|
|
1140
|
+
return result
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
def spec_pointers(rel, index):
|
|
1144
|
+
if not isinstance(index, list):
|
|
1145
|
+
return _artifact_legacy_spec_pointers(rel, index)
|
|
1146
|
+
warnings = [item for item in index if isinstance(item, dict) and item.get("artifact_diagnostic")]
|
|
1147
|
+
normal = [item for item in index if not isinstance(item, dict) or not item.get("artifact_diagnostic")]
|
|
1148
|
+
result = _artifact_legacy_spec_pointers(rel, normal)
|
|
1149
|
+
if warnings:
|
|
1150
|
+
warning = warnings[0]
|
|
1151
|
+
code = warning["artifact_diagnostic"]
|
|
1152
|
+
if warning.get("artifact_diagnostic_kind") == "capability":
|
|
1153
|
+
text = (
|
|
1154
|
+
"HTML artifact capability unavailable (" + code + "). Repair or reinstall "
|
|
1155
|
+
"the pinned artifact payload. Source inspection and unrelated legacy Markdown "
|
|
1156
|
+
"may continue; HTML planning/execution must stop until resolved."
|
|
1157
|
+
)
|
|
1158
|
+
else:
|
|
1159
|
+
text = (
|
|
1160
|
+
"HTML specification is invalid or unreadable (" + code + "). Inspect the "
|
|
1161
|
+
"engine diagnostic and repair-preview before any repair. Source inspection may "
|
|
1162
|
+
"continue; HTML planning/execution must stop until resolved."
|
|
1163
|
+
)
|
|
1164
|
+
result.append({"tier": "artifact-diagnostic", "text": text})
|
|
1165
|
+
return result
|
|
1166
|
+
|
|
1167
|
+
_artifact_legacy_compute_injection = compute_injection
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
def compute_injection(root, session_id, rel, runner=None):
|
|
1171
|
+
# Bind the injected pointers and dedup epoch to ONE validated HTML snapshot.
|
|
1172
|
+
# A second artifact read here permits B(epoch) -> A(content) and records A
|
|
1173
|
+
# under B's marker, suppressing a later stable B read. HTML therefore builds
|
|
1174
|
+
# once before dedup; the same entries drive both context and epoch. This is
|
|
1175
|
+
# advisory identity only, never approval or execution authority.
|
|
1176
|
+
if not isinstance(rel, str) or rel.replace("\\", "/").split("/", 1)[0] == ".codearbiter":
|
|
1177
|
+
return _artifact_legacy_compute_injection(root, session_id, rel, runner)
|
|
1178
|
+
try:
|
|
1179
|
+
from _artifactlib import has_html
|
|
1180
|
+
if not has_html(root):
|
|
1181
|
+
return _artifact_legacy_compute_injection(root, session_id, rel, runner)
|
|
1182
|
+
index = build_index(root)
|
|
1183
|
+
if not isinstance(index, dict):
|
|
1184
|
+
return ""
|
|
1185
|
+
artifact_entries = [
|
|
1186
|
+
item for item in index.get("spec", [])
|
|
1187
|
+
if isinstance(item, dict) and item.get("artifact_html") is True
|
|
1188
|
+
]
|
|
1189
|
+
epoch = hashlib.sha256(
|
|
1190
|
+
json.dumps(
|
|
1191
|
+
artifact_entries, sort_keys=True, separators=(",", ":")
|
|
1192
|
+
).encode()
|
|
1193
|
+
).hexdigest()
|
|
1194
|
+
epoch_session = str(session_id) + "|html:" + epoch
|
|
1195
|
+
if already_injected(root, epoch_session, rel):
|
|
1196
|
+
return ""
|
|
1197
|
+
pointers = governing_docs(rel, index, runner)
|
|
1198
|
+
if not pointers:
|
|
1199
|
+
return ""
|
|
1200
|
+
context = assemble_context(pointers, budget=150)
|
|
1201
|
+
if context:
|
|
1202
|
+
record_injection(root, epoch_session, rel)
|
|
1203
|
+
return context
|
|
1204
|
+
return ""
|
|
1205
|
+
except Exception: # noqa: BLE001 - advisory injection always fails open
|
|
1206
|
+
return ""
|