@arbiterforge/ca-pi 0.18.4 → 0.19.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 +54 -0
- package/plugins/ca-pi/COMMANDS.md +2 -2
- package/plugins/ca-pi/SKILLS.md +5 -5
- package/plugins/ca-pi/SPRINT.md +43 -230
- package/plugins/ca-pi/agents/backend-author.md +5 -5
- package/plugins/ca-pi/agents/checkpoint-aggregator.md +7 -0
- package/plugins/ca-pi/agents/frontend-author.md +5 -5
- package/plugins/ca-pi/agents/infra-author.md +6 -7
- package/plugins/ca-pi/agents/scout.md +25 -24
- package/plugins/ca-pi/arbiter.md +6 -4
- package/plugins/ca-pi/generated/command-catalog.json +5 -5
- 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 +37 -37
- package/plugins/ca-pi/hooks/_approvallib.py +41 -0
- package/plugins/ca-pi/hooks/_artifactlib.py +61 -115
- package/plugins/ca-pi/hooks/_artifactpromptlib.py +12 -12
- package/plugins/ca-pi/hooks/_bashguardlib.py +66 -4
- package/plugins/ca-pi/hooks/_contextselectlib.py +38 -35
- package/plugins/ca-pi/hooks/_contextwriterlib.py +114 -0
- package/plugins/ca-pi/hooks/_host.py +1 -1
- package/plugins/ca-pi/hooks/_prerequisitelib.py +35 -0
- package/plugins/ca-pi/hooks/_provenancelib.py +107 -1
- package/plugins/ca-pi/hooks/_readinjectlib.py +21 -7
- package/plugins/ca-pi/hooks/_reconciliationlib.py +167 -2
- package/plugins/ca-pi/hooks/_releaselib.py +6 -6
- package/plugins/ca-pi/hooks/_sprintapprovallib.py +52 -2
- package/plugins/ca-pi/hooks/artifact-reconcile.py +13 -4
- package/plugins/ca-pi/hooks/hostapi.py +1 -1
- package/plugins/ca-pi/hooks/init-codearbiter.py +34 -5
- package/plugins/ca-pi/includes/artifacts-attestation.md +434 -0
- package/plugins/ca-pi/includes/artifacts.md +133 -492
- package/plugins/ca-pi/includes/author-tdd-workflow.md +32 -21
- package/plugins/ca-pi/includes/fresh-verification.md +14 -12
- package/plugins/ca-pi/includes/harvest.md +3 -3
- package/plugins/ca-pi/includes/reference-map.md +1 -1
- package/plugins/ca-pi/includes/routing-table.md +3 -3
- package/plugins/ca-pi/includes/sprint-authority.md +6 -1
- package/plugins/ca-pi/includes/verification-boundary.md +11 -2
- package/plugins/ca-pi/package.json +1 -1
- package/plugins/ca-pi/routines/INDEX.md +8 -8
- package/plugins/ca-pi/routines/brainstorming/SKILL.md +65 -202
- package/plugins/ca-pi/routines/commit-gate/SKILL.md +60 -25
- package/plugins/ca-pi/routines/context-check/SKILL.md +89 -53
- package/plugins/ca-pi/routines/context-creation/SKILL.md +114 -220
- package/plugins/ca-pi/routines/decompose/SKILL.md +139 -167
- package/plugins/ca-pi/routines/executing-plans/SKILL.md +38 -136
- package/plugins/ca-pi/routines/finishing-a-development-branch/SKILL.md +45 -55
- package/plugins/ca-pi/routines/refactor/SKILL.md +27 -12
- package/plugins/ca-pi/routines/release/SKILL.md +1 -1
- package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +80 -230
- package/plugins/ca-pi/routines/tdd/SKILL.md +115 -228
- package/plugins/ca-pi/routines/writing-plans/SKILL.md +104 -174
- package/plugins/ca-pi/skills/ca-audit/SKILL.md +2 -2
- package/plugins/ca-pi/skills/ca-checkpoint/SKILL.md +1 -1
- package/plugins/ca-pi/skills/ca-commit/SKILL.md +60 -25
- package/plugins/ca-pi/skills/ca-context-check/SKILL.md +89 -53
- package/plugins/ca-pi/skills/ca-create-context/SKILL.md +114 -220
- package/plugins/ca-pi/skills/ca-decompose/SKILL.md +139 -167
- package/plugins/ca-pi/skills/ca-feature/SKILL.md +21 -38
- package/plugins/ca-pi/skills/ca-fix/SKILL.md +15 -9
- package/plugins/ca-pi/skills/ca-init/SKILL.md +18 -40
- package/plugins/ca-pi/skills/ca-pr/SKILL.md +45 -55
- package/plugins/ca-pi/skills/ca-refactor/SKILL.md +27 -12
- package/plugins/ca-pi/skills/ca-release/SKILL.md +1 -1
- package/plugins/ca-pi/skills/ca-review/SKILL.md +5 -8
- package/plugins/ca-pi/skills/ca-sprint/SKILL.md +11 -25
- package/plugins/ca-pi/skills/ca-status/SKILL.md +15 -6
|
@@ -20,10 +20,10 @@ import threading
|
|
|
20
20
|
from typing import Any
|
|
21
21
|
|
|
22
22
|
PROTOCOL = "codearbiter.artifact-api/0.1.0"
|
|
23
|
-
SCHEMA_VERSION = "0.3.
|
|
23
|
+
SCHEMA_VERSION = "0.3.2"
|
|
24
24
|
MAX_REQUEST = 8 << 20
|
|
25
25
|
MAX_RESPONSE = 65536
|
|
26
|
-
READ_OPERATIONS = frozenset({"capabilities", "schema", "read", "outline", "identity", "validate", "index", "snapshot", "diff", "repair-preview", "eligible", "farm-verify", "migration-preview"})
|
|
26
|
+
READ_OPERATIONS = frozenset({"capabilities", "schema", "read", "outline", "identity", "validate", "index", "snapshot", "diff", "repair-preview", "eligible", "task-context", "verification-status", "farm-verify", "migration-preview"})
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
class ArtifactError(RuntimeError):
|
|
@@ -41,8 +41,8 @@ def _object(pairs):
|
|
|
41
41
|
return value
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
def _decode(raw: bytes) -> Any:
|
|
45
|
-
if len(raw) >
|
|
44
|
+
def _decode(raw: bytes, *, max_bytes: int = MAX_RESPONSE) -> Any:
|
|
45
|
+
if len(raw) > max_bytes:
|
|
46
46
|
raise ArtifactError("INVALID_RESPONSE", "oversized JSON envelope")
|
|
47
47
|
# Bound nesting independently of the interpreter's recursion-limit setting.
|
|
48
48
|
depth = 0
|
|
@@ -97,7 +97,7 @@ def read_context_preview_request(root: str | Path, request_file: str | Path) ->
|
|
|
97
97
|
raw = os.read(fd, (1 << 20) + 1)
|
|
98
98
|
finally:
|
|
99
99
|
os.close(fd)
|
|
100
|
-
value = _decode(raw)
|
|
100
|
+
value = _decode(raw, max_bytes=1 << 20)
|
|
101
101
|
except (OSError, ValueError) as exc:
|
|
102
102
|
raise ArtifactError("INVALID_CONTEXT_REQUEST", "preview request is unavailable or outside the repository") from exc
|
|
103
103
|
if not isinstance(value, dict):
|
|
@@ -140,7 +140,7 @@ def context_writer_qualified() -> bool:
|
|
|
140
140
|
|
|
141
141
|
|
|
142
142
|
def validate_context_writer_request(request: dict[str, Any]) -> tuple[str, dict]:
|
|
143
|
-
"""Parse
|
|
143
|
+
"""Parse an inert preview or its exact apply reference; never infer approval."""
|
|
144
144
|
if not isinstance(request, dict):
|
|
145
145
|
raise ArtifactError("INVALID_CONTEXT_REQUEST", "context request must be an object")
|
|
146
146
|
operation = request.get("operation")
|
|
@@ -161,22 +161,33 @@ def validate_context_writer_request(request: dict[str, Any]) -> tuple[str, dict]
|
|
|
161
161
|
for value in preview["expected"].values()))
|
|
162
162
|
if not (regular or final):
|
|
163
163
|
raise ArtifactError("INVALID_CONTEXT_REQUEST", "preview must name one bounded context target")
|
|
164
|
+
elif operation == "apply" and set(request) == {"operation", "preview_ref", "preview_sha256"}:
|
|
165
|
+
preview_ref = request["preview_ref"]
|
|
166
|
+
preview_sha256 = request["preview_sha256"]
|
|
167
|
+
if (not isinstance(preview_ref, str) or re.fullmatch(
|
|
168
|
+
r"\.codearbiter/\.artifacts/context-previews/[0-9a-f]{64}\.json", preview_ref) is None
|
|
169
|
+
or not isinstance(preview_sha256, str)
|
|
170
|
+
or re.fullmatch(r"[0-9a-f]{64}", preview_sha256) is None):
|
|
171
|
+
raise ArtifactError("INVALID_CONTEXT_REQUEST", "apply requires an exact retained preview reference and digest")
|
|
172
|
+
return "apply", {"preview_ref": preview_ref, "preview_sha256": preview_sha256}
|
|
164
173
|
elif operation == "apply" and set(request) == {"operation", "receipt"}:
|
|
165
174
|
receipt = request["receipt"]
|
|
166
175
|
if (not isinstance(receipt, str) or re.fullmatch(
|
|
167
176
|
r"\.codearbiter/\.artifacts/receipts/[0-9a-f]{64}\.json", receipt) is None):
|
|
168
177
|
raise ArtifactError("INVALID_CONTEXT_REQUEST", "apply requires one receipt reference")
|
|
169
178
|
else:
|
|
170
|
-
raise ArtifactError("INVALID_CONTEXT_REQUEST", "select a closed preview or
|
|
179
|
+
raise ArtifactError("INVALID_CONTEXT_REQUEST", "select a closed preview or exact apply request")
|
|
171
180
|
|
|
172
181
|
return ("preview", preview) if operation == "preview" else ("apply", {"receipt": receipt})
|
|
173
182
|
|
|
174
183
|
|
|
175
184
|
def context_writer_client(root: str | Path) -> "ArtifactClient":
|
|
176
|
-
"""Construct the installed
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
185
|
+
"""Construct the installed writer; each context call checks its capability.
|
|
186
|
+
|
|
187
|
+
Avoid a duplicate native probe here. Request validation precedes construction
|
|
188
|
+
and ArtifactClient.call admits the actual mutation through the pinned binary.
|
|
189
|
+
"""
|
|
190
|
+
return ArtifactClient(root, helper_installation(__file__))
|
|
180
191
|
|
|
181
192
|
|
|
182
193
|
def _open_pinned_regular(path: Path) -> int:
|
|
@@ -463,20 +474,6 @@ def _installed_workflow_preflight(installation: Path) -> dict[str, object]:
|
|
|
463
474
|
return result
|
|
464
475
|
|
|
465
476
|
|
|
466
|
-
def _require_host_workflow(client: "ArtifactClient") -> None:
|
|
467
|
-
readiness = client.workflow_preflight()
|
|
468
|
-
if readiness["resources_available"] is not True:
|
|
469
|
-
raise ArtifactError(
|
|
470
|
-
"HOST_WORKFLOW_UNAVAILABLE",
|
|
471
|
-
("installed HTML workflow prerequisites are unavailable: "
|
|
472
|
-
+ ", ".join(readiness["missing"]))[:400]
|
|
473
|
-
+ "; repair this package or use a supported host; no fallback artifacts",
|
|
474
|
-
)
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
# The native engine ships only through the release distribution channels, never
|
|
478
|
-
# through a main-branch checkout, so the repair is always "install from the
|
|
479
|
-
# channel". Named concretely so the user is not left to guess (ADR-0040).
|
|
480
477
|
PAYLOAD_REPAIR_HINT = (
|
|
481
478
|
"Claude Code: /plugin marketplace update codearbiter, then /plugin update ca@codearbiter; "
|
|
482
479
|
"Codex (registered without --ref): codex plugin remove ca-codex@codearbiter, "
|
|
@@ -549,12 +546,6 @@ def _select_authoring_route(
|
|
|
549
546
|
_require_authoring_capability(client)
|
|
550
547
|
if _resolve_workflow_pair(root, slug) != selected:
|
|
551
548
|
raise ArtifactError("STALE_ROUTE", "artifact namespace changed during the capability probe")
|
|
552
|
-
_require_host_workflow(client)
|
|
553
|
-
if _resolve_workflow_pair(root, slug) != selected:
|
|
554
|
-
raise ArtifactError(
|
|
555
|
-
"STALE_ROUTE",
|
|
556
|
-
"artifact namespace changed during the installed host preflight",
|
|
557
|
-
)
|
|
558
549
|
return {"workflow": workflow, "lane": lane, "mode": "artifact", **selected}
|
|
559
550
|
|
|
560
551
|
|
|
@@ -566,15 +557,14 @@ def _preflight_plan_authoring(
|
|
|
566
557
|
spec_normative_sha256: str,
|
|
567
558
|
draft_for_pair: bool = False,
|
|
568
559
|
) -> Path:
|
|
569
|
-
"""
|
|
560
|
+
"""Resolve the exact source and safe target without an artifact approval.
|
|
570
561
|
|
|
571
|
-
|
|
572
|
-
|
|
562
|
+
Drafts may be incomplete. This checks structural identity, not ready-state
|
|
563
|
+
completeness, user consent, command support, or future reviewer availability.
|
|
564
|
+
draft_for_pair remains a typed compatibility keyword, not an execution mode.
|
|
573
565
|
"""
|
|
574
566
|
if type(draft_for_pair) is not bool:
|
|
575
567
|
raise ArtifactError("INVALID_ROUTE", "draft_for_pair must be an explicit boolean")
|
|
576
|
-
if draft_for_pair and (not isinstance(selected, dict) or selected.get("workflow") != "sprint" or selected.get("lane") != "full"):
|
|
577
|
-
raise ArtifactError("INVALID_ROUTE", "draft preview is limited to full-lane initial sprint pairs")
|
|
578
568
|
if not isinstance(selected, dict):
|
|
579
569
|
raise ArtifactError("INVALID_ROUTE", "plan authoring requires a selected route")
|
|
580
570
|
slug = selected.get("slug")
|
|
@@ -616,7 +606,7 @@ def _preflight_plan_authoring(
|
|
|
616
606
|
):
|
|
617
607
|
raise ArtifactError("AUTHORITY_MISMATCH", "spec normative digest is invalid")
|
|
618
608
|
|
|
619
|
-
|
|
609
|
+
_require_authoring_capability(client)
|
|
620
610
|
identity = client.call("identity", {"artifact_id": spec_artifact_id})
|
|
621
611
|
expected_path = f".codearbiter/specs/{slug}.html"
|
|
622
612
|
if (
|
|
@@ -629,24 +619,19 @@ def _preflight_plan_authoring(
|
|
|
629
619
|
"AUTHORITY_MISMATCH",
|
|
630
620
|
"caller spec identity does not match the selected current HTML spec",
|
|
631
621
|
)
|
|
632
|
-
|
|
633
|
-
"validate",
|
|
634
|
-
{"artifact_id": spec_artifact_id, "gate": "ready" if draft_for_pair else "approved"},
|
|
622
|
+
checked = client.call(
|
|
623
|
+
"validate", {"artifact_id": spec_artifact_id, "gate": "structural"},
|
|
635
624
|
permit_invalid=True,
|
|
636
625
|
)
|
|
637
|
-
authority = approved.get("authority")
|
|
638
626
|
if (
|
|
639
|
-
|
|
640
|
-
or
|
|
641
|
-
or
|
|
642
|
-
or
|
|
643
|
-
or approved.get("model_sha256") != identity.get("model_sha256")
|
|
644
|
-
or authority.get("state") != ("draft" if draft_for_pair else "approved")
|
|
645
|
-
or authority.get("authority_verified") is not (not draft_for_pair)
|
|
627
|
+
checked.get("valid") is not True
|
|
628
|
+
or checked.get("artifact_id") != spec_artifact_id
|
|
629
|
+
or checked.get("normative_sha256") != spec_normative_sha256
|
|
630
|
+
or checked.get("model_sha256") != identity.get("model_sha256")
|
|
646
631
|
):
|
|
647
632
|
raise ArtifactError(
|
|
648
|
-
"
|
|
649
|
-
"plan authoring requires the
|
|
633
|
+
"SOURCE_INVALID",
|
|
634
|
+
"plan authoring requires the selected structurally valid, unchanged spec",
|
|
650
635
|
)
|
|
651
636
|
if _resolve_workflow_pair(root, slug) != current:
|
|
652
637
|
raise ArtifactError(
|
|
@@ -754,7 +739,7 @@ def _preflight_current_acceptance(
|
|
|
754
739
|
)
|
|
755
740
|
identities[kind] = identity
|
|
756
741
|
|
|
757
|
-
eligible = client.call("
|
|
742
|
+
eligible = client.call("verification-status", {"artifact_id": plan_artifact_id})
|
|
758
743
|
if (
|
|
759
744
|
eligible.get("artifact_id") != plan_artifact_id
|
|
760
745
|
or eligible.get("model_sha256") != identities["plan"].get("model_sha256")
|
|
@@ -821,7 +806,7 @@ def _preflight_current_acceptance(
|
|
|
821
806
|
|
|
822
807
|
final_spec = client.call("identity", {"artifact_id": spec_artifact_id})
|
|
823
808
|
final_plan = client.call("identity", {"artifact_id": plan_artifact_id})
|
|
824
|
-
final_eligible = client.call("
|
|
809
|
+
final_eligible = client.call("verification-status", {"artifact_id": plan_artifact_id})
|
|
825
810
|
if (
|
|
826
811
|
final_spec.get("model_sha256") != identities["spec"].get("model_sha256")
|
|
827
812
|
or final_plan.get("model_sha256") != identities["plan"].get("model_sha256")
|
|
@@ -967,51 +952,6 @@ def _bounded_child(argv: list[str], request: bytes, fd: int | None, timeout: flo
|
|
|
967
952
|
return process.returncode, bytes(buffers[0]), bytes(buffers[1])
|
|
968
953
|
|
|
969
954
|
|
|
970
|
-
_CONTEXT_NATIVE_CASES = [
|
|
971
|
-
"TestContextBoundedRender", "TestContextDocumentContract",
|
|
972
|
-
"TestContextMarkdownPreservation", "TestContextMutationAdmission",
|
|
973
|
-
"TestContextRecoveryAndPaths", "TestContextRepresentationBoundary",
|
|
974
|
-
]
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
def _context_release_qualified(installation: Path) -> bool:
|
|
978
|
-
"""Require exact context evidence added by the native receipt assembler."""
|
|
979
|
-
system = {"Linux": "linux", "Darwin": "darwin", "Windows": "windows"}.get(platform.system())
|
|
980
|
-
arch = {"x86_64": "amd64", "amd64": "amd64", "aarch64": "arm64", "arm64": "arm64"}.get(platform.machine().lower())
|
|
981
|
-
if system is None or arch is None:
|
|
982
|
-
return False
|
|
983
|
-
try:
|
|
984
|
-
fd = _open_pinned_regular(installation / "release.json")
|
|
985
|
-
try:
|
|
986
|
-
if os.fstat(fd).st_size > 65_536:
|
|
987
|
-
return False
|
|
988
|
-
manifest = _decode(os.read(fd, 65_537))
|
|
989
|
-
finally:
|
|
990
|
-
os.close(fd)
|
|
991
|
-
entry = manifest["binaries"][f"{system}/{arch}"]
|
|
992
|
-
record = manifest["context_qualification"][f"{system}/{arch}"]
|
|
993
|
-
return (
|
|
994
|
-
isinstance(record, dict)
|
|
995
|
-
and set(record) == {"source_commit", "workflow", "run_id", "binary_sha256",
|
|
996
|
-
"observation_sha256", "module_sha256", "native_cases"}
|
|
997
|
-
and isinstance(record["source_commit"], str)
|
|
998
|
-
and re.fullmatch(r"[0-9a-f]{40}", record["source_commit"]) is not None
|
|
999
|
-
and record["workflow"] == ".github/workflows/ci.yml"
|
|
1000
|
-
and isinstance(record["run_id"], str)
|
|
1001
|
-
and re.fullmatch(r"[1-9][0-9]*", record["run_id"]) is not None
|
|
1002
|
-
and record["binary_sha256"] == entry["sha256"]
|
|
1003
|
-
and all(isinstance(record[field], str)
|
|
1004
|
-
and re.fullmatch(r"[0-9a-f]{64}", record[field]) is not None
|
|
1005
|
-
for field in ("observation_sha256", "module_sha256"))
|
|
1006
|
-
and record["native_cases"] == _CONTEXT_NATIVE_CASES
|
|
1007
|
-
)
|
|
1008
|
-
except (ArtifactError, OSError, KeyError, TypeError, ValueError):
|
|
1009
|
-
return False
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
# A whole-tree input snapshot on a large repository can take ~30s on Windows,
|
|
1013
|
-
# so the old 30s bound timed out every publish, arm and verify. The bound is a
|
|
1014
|
-
# wait limit, never a gate: raising it relaxes no check.
|
|
1015
955
|
DEFAULT_ENGINE_TIMEOUT_SECONDS = 120
|
|
1016
956
|
ENGINE_TIMEOUT_ENV = "CODEARBITER_ENGINE_TIMEOUT"
|
|
1017
957
|
_MAX_ENGINE_TIMEOUT_SECONDS = 3600
|
|
@@ -1047,22 +987,26 @@ class ArtifactClient:
|
|
|
1047
987
|
return _installed_workflow_preflight(self.installation)
|
|
1048
988
|
|
|
1049
989
|
def require_context_workflow(self) -> dict[str, object]:
|
|
1050
|
-
"""
|
|
1051
|
-
|
|
990
|
+
"""Check the installed writer's API, without a second host approval gate.
|
|
991
|
+
|
|
992
|
+
The capabilities call verifies the installation-pinned, native-tested
|
|
993
|
+
binary through the normal bridge. Package/host observations remain
|
|
994
|
+
evidence, not permission to update already-authorized context.
|
|
995
|
+
"""
|
|
1052
996
|
capability = self.call("capabilities")
|
|
1053
997
|
context = capability.get("repository_context")
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
or
|
|
1058
|
-
or context
|
|
1059
|
-
or context
|
|
1060
|
-
or context
|
|
1061
|
-
or context
|
|
1062
|
-
or
|
|
1063
|
-
or not
|
|
1064
|
-
raise ArtifactError("CONTEXT_WORKFLOW_UNAVAILABLE", "installed repository-context
|
|
1065
|
-
return
|
|
998
|
+
required = {"context-preview", "context-finalize-preview", "context-apply"}
|
|
999
|
+
operations = capability.get("operations")
|
|
1000
|
+
if (not isinstance(context, dict)
|
|
1001
|
+
or context.get("source_present") is not True
|
|
1002
|
+
or context.get("kind_registered") is not True
|
|
1003
|
+
or context.get("host_default_enabled") is not True
|
|
1004
|
+
or context.get("permission_model") != "host-owned"
|
|
1005
|
+
or context.get("mutation_available") is not True
|
|
1006
|
+
or not isinstance(operations, list)
|
|
1007
|
+
or any(name not in operations for name in required)):
|
|
1008
|
+
raise ArtifactError("CONTEXT_WORKFLOW_UNAVAILABLE", "installed binary lacks the assistive repository-context writer")
|
|
1009
|
+
return dict(context)
|
|
1066
1010
|
|
|
1067
1011
|
def _open_binary(self) -> int:
|
|
1068
1012
|
system = {"Linux":"linux", "Darwin":"darwin", "Windows":"windows"}.get(platform.system())
|
|
@@ -1132,7 +1076,7 @@ class ArtifactClient:
|
|
|
1132
1076
|
def call(self, operation: str, request: dict[str, Any] | None = None, *, permit_invalid: bool = False) -> dict:
|
|
1133
1077
|
if not re.fullmatch(r"[a-z][a-z-]{0,39}", operation):
|
|
1134
1078
|
raise ArtifactError("UNKNOWN_OPERATION", "invalid internal operation name")
|
|
1135
|
-
if operation in {"context-
|
|
1079
|
+
if operation in {"context-preview", "context-finalize-preview", "context-apply"}:
|
|
1136
1080
|
self.require_context_workflow()
|
|
1137
1081
|
request = dict(request or {})
|
|
1138
1082
|
if "protocol" in request and request["protocol"] != PROTOCOL:
|
|
@@ -1258,12 +1202,14 @@ def has_html(root: str | Path) -> bool:
|
|
|
1258
1202
|
|
|
1259
1203
|
|
|
1260
1204
|
def checked_spec_index(root, installation) -> list[dict]:
|
|
1261
|
-
"""Fresh
|
|
1205
|
+
"""Fresh validated advisory pointers, including receipt-free draft specs."""
|
|
1262
1206
|
if not has_html(root):
|
|
1263
1207
|
return []
|
|
1264
1208
|
client = ArtifactClient(root, installation)
|
|
1265
|
-
return [{"spec": x["artifact_id"], "path": x["path"], "globs": x
|
|
1266
|
-
|
|
1209
|
+
return [{"spec": x["artifact_id"], "path": x["path"], "globs": x["governs"],
|
|
1210
|
+
"artifact_html": True, "governance_state": x["authority"]["state"]}
|
|
1211
|
+
for x in client.index("spec")
|
|
1212
|
+
if x["authority"].get("state") in ("draft", "ready", "approved") and x.get("governs")]
|
|
1267
1213
|
|
|
1268
1214
|
|
|
1269
1215
|
def resolve_spec_file(filename, installation) -> tuple[ArtifactClient, dict]:
|
|
@@ -39,7 +39,8 @@ def compose_feature_actor_input(*, actor: str, brief: str,
|
|
|
39
39
|
code_map, provenance, commands, constraints,
|
|
40
40
|
effective_instructions, evidence_ids,
|
|
41
41
|
command_bindings=None, observations=None,
|
|
42
|
-
confirmed_mini_spec: str | None = None
|
|
42
|
+
confirmed_mini_spec: str | None = None,
|
|
43
|
+
required_refs=()):
|
|
43
44
|
"""Compose the transient input sent to one fresh feature actor.
|
|
44
45
|
|
|
45
46
|
The coordinator must acquire the inputs at dispatch time and send `input`
|
|
@@ -78,18 +79,14 @@ def compose_feature_actor_input(*, actor: str, brief: str,
|
|
|
78
79
|
effective_instructions=effective_instructions,
|
|
79
80
|
evidence_ids=evidence_ids, command_bindings=command_bindings,
|
|
80
81
|
observations=observations)
|
|
82
|
+
requirements = _contextselectlib.check_task_context(
|
|
83
|
+
packet, required_refs=required_refs)
|
|
81
84
|
except _contextselectlib.SelectionError as exc:
|
|
82
85
|
raise PromptRouteError(f"feature context is invalid: {exc}") from exc
|
|
83
|
-
|
|
84
|
-
for item in packet["unresolved"] if type(item) is dict)
|
|
85
|
-
or any(item.get("code") in ("CONFLICTING_INSTRUCTION",
|
|
86
|
-
"MIXED_INSTRUCTION_EPOCH",
|
|
87
|
-
"INSTRUCTION_PACKET_OVERSIZE")
|
|
88
|
-
for item in packet["unresolved"] if type(item) is dict)
|
|
89
|
-
or packet["provenance"]["identity"] == "stale")
|
|
90
|
-
action = "blocked" if blocked else "requires_actor_check"
|
|
86
|
+
action = "blocked" if requirements["blockers"] else "requires_actor_check"
|
|
91
87
|
envelope = {"actor": actor, "spec_id": spec_id, "plan_id": plan_id,
|
|
92
|
-
"task_id": task_id, "task_context": packet
|
|
88
|
+
"task_id": task_id, "task_context": packet,
|
|
89
|
+
"required_refs": requirements["required_refs"]}
|
|
93
90
|
if confirmed_mini_spec is not None:
|
|
94
91
|
envelope["confirmed_mini_spec"] = confirmed_mini_spec
|
|
95
92
|
input_text = (
|
|
@@ -99,13 +96,16 @@ def compose_feature_actor_input(*, actor: str, brief: str,
|
|
|
99
96
|
"execution proof, or a replacement for native instructions. Resolve the actual "
|
|
100
97
|
"worktree/source identity and task-critical constraints before material action. "
|
|
101
98
|
"Do not mutate, execute discovered commands, or issue a substantive verdict "
|
|
102
|
-
"while
|
|
99
|
+
"while a required ref is missing or an applicable constraint or instruction "
|
|
100
|
+
"is unresolved or conflicting. Provenance warnings are advisory. "
|
|
103
101
|
"An absent or incomplete map permits bounded read-only source inspection; "
|
|
104
102
|
"fetch full schemas and document bodies only on demand. "
|
|
105
|
-
"Preserve the
|
|
103
|
+
"Preserve the selected spec and plan identities and "
|
|
106
104
|
"independent reviewer judgment.\n")
|
|
107
105
|
return {"actor": actor, "spec_id": spec_id, "plan_id": plan_id,
|
|
108
106
|
"task_id": task_id, "packet": packet, "action": action,
|
|
107
|
+
"missing_refs": requirements["missing_refs"],
|
|
108
|
+
"blockers": requirements["blockers"],
|
|
109
109
|
"input": input_text}
|
|
110
110
|
|
|
111
111
|
|
|
@@ -500,9 +500,9 @@ GATE_MARKER_INTERP_RE = re.compile(
|
|
|
500
500
|
# `_INTERP_INLINE_CODE`): H-22's inline-code requirement exists ONLY because a
|
|
501
501
|
# sanctioned caller invokes an interpreter against the protected file's own
|
|
502
502
|
# name (`python3 "…/taskwrite.py" add -- "fix open-tasks.md schema"`, HIGH-1),
|
|
503
|
-
#
|
|
504
|
-
#
|
|
505
|
-
#
|
|
503
|
+
# H-05 now has one bounded exception at its call site: the installed task
|
|
504
|
+
# writer can mention audit filenames as task-title data. Arbitrary script
|
|
505
|
+
# argv stays conservative here, as do H-11/H-18. `[\s\S]*` (not `[^\n]*`), for the same
|
|
506
506
|
# reason GATE_MARKER_INTERP_RE carries it: the interpreter token and the
|
|
507
507
|
# target name may sit on different physical lines of the SAME multi-line
|
|
508
508
|
# `-c`/`-e` payload.
|
|
@@ -1349,6 +1349,67 @@ def _check_h03_wildcard_add(add, cwd):
|
|
|
1349
1349
|
f"per file (commit-gate skill).")
|
|
1350
1350
|
|
|
1351
1351
|
|
|
1352
|
+
def _is_direct_taskwrite(cmd):
|
|
1353
|
+
"""Recognize literal argv to this package's task writer, not shell code.
|
|
1354
|
+
|
|
1355
|
+
This only distinguishes H-05's interpreter match from task-title data.
|
|
1356
|
+
Other H-05 legs and all other guards still inspect the original command.
|
|
1357
|
+
Unknown syntax, wrappers, substitutions and options retain the old refusal.
|
|
1358
|
+
"""
|
|
1359
|
+
if re.search(r"[\r\n;&|<>`$%!^*?{}\[\]()]", cmd):
|
|
1360
|
+
return False
|
|
1361
|
+
# A deliberately small literal-token grammar accepts quoted Windows paths
|
|
1362
|
+
# and adjacent single-quoted fragments (PowerShell's doubled apostrophe).
|
|
1363
|
+
# Gaps must be whitespace; an unmatched quote cannot disappear in parsing.
|
|
1364
|
+
tokens, end = [], 0
|
|
1365
|
+
for match in re.finditer(r'''(?:[^\s'"\x00]|'[^']*'|"[^"]*")+''', cmd):
|
|
1366
|
+
if cmd[end:match.start()].strip():
|
|
1367
|
+
return False
|
|
1368
|
+
token = match.group()
|
|
1369
|
+
if len(token) >= 2 and token[0] == token[-1] and token[0] in "\"'":
|
|
1370
|
+
token = token[1:-1]
|
|
1371
|
+
tokens.append(token)
|
|
1372
|
+
end = match.end()
|
|
1373
|
+
if cmd[end:].strip() or len(tokens) < 4:
|
|
1374
|
+
return False
|
|
1375
|
+
executable = tokens.pop(0).replace("\\", "/").rsplit("/", 1)[-1]
|
|
1376
|
+
if not re.fullmatch(r"(?:python(?:[23](?:\.\d+)*)?|py)(?:\.exe)?", executable, re.I):
|
|
1377
|
+
return False
|
|
1378
|
+
if tokens[0] == "-B":
|
|
1379
|
+
tokens.pop(0)
|
|
1380
|
+
helper = tokens.pop(0)
|
|
1381
|
+
known = os.path.join(os.path.dirname(os.path.abspath(__file__)), "taskwrite.py")
|
|
1382
|
+
if not os.path.isabs(helper) or os.path.normcase(os.path.normpath(helper)) != os.path.normcase(known):
|
|
1383
|
+
return False
|
|
1384
|
+
value_options = {
|
|
1385
|
+
"add": {"--from", "--id", "--boundaries", "--desc", "--section"},
|
|
1386
|
+
"start": {"--as", "--date"}, "done": {"--date"}, "archive": set(),
|
|
1387
|
+
}
|
|
1388
|
+
verb = tokens.pop(0) if tokens else ""
|
|
1389
|
+
if verb not in value_options:
|
|
1390
|
+
return False
|
|
1391
|
+
positional, seen, index = 0, set(), 0
|
|
1392
|
+
while index < len(tokens):
|
|
1393
|
+
token = tokens[index]
|
|
1394
|
+
index += 1
|
|
1395
|
+
if token == "--":
|
|
1396
|
+
return positional == 0 and len(tokens) - index == 1 and bool(tokens[index])
|
|
1397
|
+
if token.startswith("-"):
|
|
1398
|
+
if token in seen:
|
|
1399
|
+
return False
|
|
1400
|
+
seen.add(token)
|
|
1401
|
+
if verb == "archive" and token == "--allow-undated":
|
|
1402
|
+
continue
|
|
1403
|
+
if token not in value_options[verb] or index == len(tokens):
|
|
1404
|
+
return False
|
|
1405
|
+
if not tokens[index] or tokens[index].startswith("-"):
|
|
1406
|
+
return False
|
|
1407
|
+
index += 1
|
|
1408
|
+
else:
|
|
1409
|
+
positional += 1
|
|
1410
|
+
return positional == 1
|
|
1411
|
+
|
|
1412
|
+
|
|
1352
1413
|
def _check_h05_audit_log(cmd):
|
|
1353
1414
|
"""H-05: the audit trail is append-only — block truncation/removal of the
|
|
1354
1415
|
audit logs via shell verbs (Write/Edit are guarded separately). The
|
|
@@ -1368,7 +1429,8 @@ def _check_h05_audit_log(cmd):
|
|
|
1368
1429
|
ADR_LIFECYCLE_LOG_BASENAME.casefold() in folded)
|
|
1369
1430
|
if enrolled_name and (
|
|
1370
1431
|
LOG_TRUNC_RE.search(cmd) or LOG_DESTROY_RE.search(cmd)
|
|
1371
|
-
or LOG_GIT_RESTORE_RE.search(cmd)
|
|
1432
|
+
or LOG_GIT_RESTORE_RE.search(cmd)
|
|
1433
|
+
or LOG_INTERP_RE.search(cmd) and not _is_direct_taskwrite(cmd)):
|
|
1372
1434
|
block("H-05", "The .codearbiter audit logs (overrides.log, triage.log, sprint-log.md, "
|
|
1373
1435
|
"gate-events.log, decisions/decision-log.md) are append-only "
|
|
1374
1436
|
"(ORCHESTRATOR §7). Truncating, overwriting, or deleting the audit "
|
|
@@ -246,7 +246,7 @@ def prepare_actor_delivery(packet, *, actor, task_id, worktree, source_epoch,
|
|
|
246
246
|
index. A host receipt is a caller-reported observation; admission to work
|
|
247
247
|
remains with the caller.
|
|
248
248
|
Unknown epochs cannot make a saved receipt current and allow at most two
|
|
249
|
-
counted deliveries of the same binding before an
|
|
249
|
+
counted deliveries of the same binding before an advisory gap is returned.
|
|
250
250
|
"""
|
|
251
251
|
if (type(packet) is not dict or packet.get('admission') != 'requires_actor_check'
|
|
252
252
|
or type(packet.get('task_paths')) is not list
|
|
@@ -312,7 +312,7 @@ def prepare_actor_delivery(packet, *, actor, task_id, worktree, source_epoch,
|
|
|
312
312
|
'observation': 'reported', 'attempts_used': count,
|
|
313
313
|
'delivery_text': None}
|
|
314
314
|
if count >= 2:
|
|
315
|
-
return {'action': '
|
|
315
|
+
return {'action': 'advisory', 'binding': binding, 'attempt': None,
|
|
316
316
|
'observation': 'unknown', 'attempts_used': count,
|
|
317
317
|
'gap': 'ACTOR_DELIVERY_UNOBSERVED', 'delivery_text': None}
|
|
318
318
|
delivery_text = (
|
|
@@ -331,7 +331,8 @@ def compose_fix_or_test_actor_input(*, route, caller, actor, brief, task_id,
|
|
|
331
331
|
task_paths, code_map, provenance, commands,
|
|
332
332
|
constraints, effective_instructions,
|
|
333
333
|
evidence_ids, command_bindings=None,
|
|
334
|
-
observations=None, bug_origin=None
|
|
334
|
+
observations=None, bug_origin=None,
|
|
335
|
+
required_refs=()):
|
|
335
336
|
"""Prepare input for an existing fix or test-change caller without dispatching.
|
|
336
337
|
|
|
337
338
|
The caller supplies current source and execution evidence. This pure helper
|
|
@@ -363,49 +364,44 @@ def compose_fix_or_test_actor_input(*, route, caller, actor, brief, task_id,
|
|
|
363
364
|
effective_instructions=effective_instructions,
|
|
364
365
|
evidence_ids=evidence_ids, command_bindings=command_bindings,
|
|
365
366
|
observations=observations)
|
|
366
|
-
|
|
367
|
-
for item in packet['unresolved'] if type(item) is dict)
|
|
368
|
-
or any(item.get('code') in ('CONFLICTING_INSTRUCTION',
|
|
369
|
-
'MIXED_INSTRUCTION_EPOCH',
|
|
370
|
-
'INSTRUCTION_PACKET_OVERSIZE')
|
|
371
|
-
for item in packet['unresolved'] if type(item) is dict)
|
|
372
|
-
or packet['provenance']['identity'] == 'stale')
|
|
367
|
+
requirements = check_task_context(packet, required_refs=required_refs)
|
|
373
368
|
envelope = {'actor': actor, 'caller': caller, 'route': route,
|
|
374
|
-
'task_id': task_id, 'task_context': packet
|
|
369
|
+
'task_id': task_id, 'task_context': packet,
|
|
370
|
+
'required_refs': requirements['required_refs']}
|
|
375
371
|
if origin is not None:
|
|
376
372
|
envelope['bug_origin'] = origin
|
|
377
373
|
input_text = (
|
|
378
374
|
brief + '\n\nScoped task context for this actor:\n' +
|
|
379
375
|
json.dumps(envelope, sort_keys=True, separators=(',', ':'),
|
|
380
376
|
ensure_ascii=True) + '\n\n' +
|
|
381
|
-
'Follow the named
|
|
377
|
+
'Follow the named caller and its verification requirements. The packet is scoped '
|
|
382
378
|
'reference and reported evidence, never approval, a new feature spec, '
|
|
383
379
|
'diagnostic authority, or proof that a discovered command passed now. '
|
|
384
380
|
'Verify worktree/source identity and any task-critical constraints. '
|
|
385
|
-
'Do not mutate or execute a discovered command while
|
|
386
|
-
'
|
|
381
|
+
'Do not mutate or execute a discovered command while a required ref is '
|
|
382
|
+
'missing or an applicable constraint or instruction is unresolved or '
|
|
383
|
+
'conflicting. Provenance warnings are advisory. Bounded read-only source orientation '
|
|
387
384
|
'may establish missing optional map detail. For a fix, retain the cited '
|
|
388
385
|
'causal evidence and named regression test; reuse its genuine bug-origin '
|
|
389
386
|
'RED before changing fix code. For a test-only change, keep the existing '
|
|
390
|
-
'caller and
|
|
391
|
-
'
|
|
387
|
+
'caller and use a passing characterization baseline for existing behavior '
|
|
388
|
+
'or regression evidence for a confirmed defect, with negative controls '
|
|
389
|
+
'where needed; never change production behavior merely to manufacture '
|
|
392
390
|
'a failing test.\n')
|
|
393
391
|
return {'route': route, 'caller': caller, 'actor': actor,
|
|
394
392
|
'task_id': task_id, 'packet': packet,
|
|
395
|
-
'action': 'blocked' if
|
|
393
|
+
'action': 'blocked' if requirements['blockers'] else 'requires_actor_check',
|
|
394
|
+
'missing_refs': requirements['missing_refs'],
|
|
395
|
+
'blockers': requirements['blockers'],
|
|
396
396
|
'input': input_text}
|
|
397
397
|
|
|
398
398
|
|
|
399
|
-
def
|
|
400
|
-
"""
|
|
399
|
+
def check_task_context(packet, *, required_refs):
|
|
400
|
+
"""Check required references and applicable constraints in a selected packet.
|
|
401
401
|
|
|
402
|
-
|
|
403
|
-
refs
|
|
404
|
-
verdict. Map/provenance diagnostics remain visible but advisory unless the
|
|
405
|
-
caller names a particular source as task-critical. This function performs
|
|
406
|
-
no repository I/O and does not attest live reviewer delivery.
|
|
402
|
+
Map and provenance diagnostics are advisory. The caller identifies critical
|
|
403
|
+
refs from the task after bounded read-only orientation.
|
|
407
404
|
"""
|
|
408
|
-
_text(reviewer, 'reviewer', 128)
|
|
409
405
|
if (type(packet) is not dict or packet.get('admission') != 'requires_actor_check'
|
|
410
406
|
or type(packet.get('task_paths')) is not list
|
|
411
407
|
or type(packet.get('map')) is not list
|
|
@@ -415,21 +411,20 @@ def prepare_review_input(packet, *, reviewer, required_refs):
|
|
|
415
411
|
or type(packet.get('unresolved')) is not list
|
|
416
412
|
or type(required_refs) not in (list, tuple, set, frozenset)
|
|
417
413
|
or len(required_refs) > 64):
|
|
418
|
-
raise SelectionError('
|
|
414
|
+
raise SelectionError('INVALID_TASK_CONTEXT')
|
|
419
415
|
required = list(required_refs)
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
raise SelectionError('INVALID_REQUIRED_REF')
|
|
416
|
+
for ref in required:
|
|
417
|
+
_text(ref, 'required_ref')
|
|
423
418
|
available = set()
|
|
424
419
|
blockers = []
|
|
425
420
|
for row in packet['map']:
|
|
426
421
|
if type(row) is not dict:
|
|
427
|
-
raise SelectionError('
|
|
422
|
+
raise SelectionError('INVALID_TASK_CONTEXT')
|
|
428
423
|
if type(row.get('path')) is str:
|
|
429
424
|
available.add('map:' + row['path'])
|
|
430
425
|
for constraint in packet['constraints']:
|
|
431
426
|
if type(constraint) is not dict:
|
|
432
|
-
raise SelectionError('
|
|
427
|
+
raise SelectionError('INVALID_TASK_CONTEXT')
|
|
433
428
|
source = constraint.get('source_ref')
|
|
434
429
|
if type(source) is str:
|
|
435
430
|
available.add('constraint:' + source)
|
|
@@ -437,13 +432,13 @@ def prepare_review_input(packet, *, reviewer, required_refs):
|
|
|
437
432
|
blockers.append('UNRESOLVED_CONSTRAINT')
|
|
438
433
|
for instruction in packet['instructions']:
|
|
439
434
|
if type(instruction) is not dict:
|
|
440
|
-
raise SelectionError('
|
|
435
|
+
raise SelectionError('INVALID_TASK_CONTEXT')
|
|
441
436
|
path = instruction.get('path')
|
|
442
437
|
if type(path) is str and instruction.get('text') is not None:
|
|
443
438
|
available.add('instruction:' + path)
|
|
444
439
|
for command in packet['commands']:
|
|
445
440
|
if type(command) is not dict:
|
|
446
|
-
raise SelectionError('
|
|
441
|
+
raise SelectionError('INVALID_TASK_CONTEXT')
|
|
447
442
|
command_id, cwd = command.get('id'), command.get('cwd')
|
|
448
443
|
if type(command_id) is str and type(cwd) is str:
|
|
449
444
|
available.add('command:' + cwd + ':' + command_id)
|
|
@@ -456,9 +451,17 @@ def prepare_review_input(packet, *, reviewer, required_refs):
|
|
|
456
451
|
if missing:
|
|
457
452
|
blockers.append('MISSING_CRITICAL_CONTEXT')
|
|
458
453
|
blockers = list(dict.fromkeys(blockers))
|
|
454
|
+
return {'required_refs': required, 'missing_refs': missing, 'blockers': blockers}
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def prepare_review_input(packet, *, reviewer, required_refs):
|
|
458
|
+
"""Prepare bounded input without repository I/O or delivery attestation."""
|
|
459
|
+
_text(reviewer, 'reviewer', 128)
|
|
460
|
+
requirements = check_task_context(packet, required_refs=required_refs)
|
|
461
|
+
blockers = requirements['blockers']
|
|
459
462
|
return {'orientation': 'read_only',
|
|
460
463
|
'verdict_admission': 'blocked' if blockers else 'ready',
|
|
461
|
-
'missing_refs':
|
|
464
|
+
'missing_refs': requirements['missing_refs'], 'blockers': blockers,
|
|
462
465
|
'reviewer_input': None if blockers else {
|
|
463
466
|
'reviewer': reviewer, 'context': copy.deepcopy(packet),
|
|
464
|
-
'required_refs':
|
|
467
|
+
'required_refs': requirements['required_refs']}}
|