@arbiterforge/ca-pi 0.10.13 → 0.14.7
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 +2 -1
- package/plugins/ca-pi/CHANGELOG.md +167 -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/_prerequisitelib.py +971 -0
- 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 +47 -10
- package/plugins/ca-pi/hooks/releasehash.py +46 -32
- package/plugins/ca-pi/includes/artifacts.md +210 -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.7"
|
|
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:
|