@arbiterforge/ca-pi 0.18.3 → 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.
Files changed (74) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/plugins/ca-pi/CHANGELOG.md +60 -0
  4. package/plugins/ca-pi/COMMANDS.md +2 -2
  5. package/plugins/ca-pi/SKILLS.md +5 -5
  6. package/plugins/ca-pi/SPRINT.md +43 -230
  7. package/plugins/ca-pi/agents/backend-author.md +5 -5
  8. package/plugins/ca-pi/agents/checkpoint-aggregator.md +7 -0
  9. package/plugins/ca-pi/agents/frontend-author.md +5 -5
  10. package/plugins/ca-pi/agents/infra-author.md +6 -7
  11. package/plugins/ca-pi/agents/scout.md +25 -24
  12. package/plugins/ca-pi/arbiter.md +6 -4
  13. package/plugins/ca-pi/generated/command-catalog.json +5 -5
  14. package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-amd64 +0 -0
  15. package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-arm64 +0 -0
  16. package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-amd64 +0 -0
  17. package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-arm64 +0 -0
  18. package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-amd64.exe +0 -0
  19. package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-arm64.exe +0 -0
  20. package/plugins/ca-pi/helpers/artifacts/release.json +37 -37
  21. package/plugins/ca-pi/hooks/_approvallib.py +41 -0
  22. package/plugins/ca-pi/hooks/_artifactlib.py +61 -115
  23. package/plugins/ca-pi/hooks/_artifactpromptlib.py +12 -12
  24. package/plugins/ca-pi/hooks/_bashguardlib.py +66 -4
  25. package/plugins/ca-pi/hooks/_contextselectlib.py +38 -35
  26. package/plugins/ca-pi/hooks/_contextwriterlib.py +114 -0
  27. package/plugins/ca-pi/hooks/_host.py +1 -1
  28. package/plugins/ca-pi/hooks/_prerequisitelib.py +35 -0
  29. package/plugins/ca-pi/hooks/_provenancelib.py +107 -1
  30. package/plugins/ca-pi/hooks/_readinjectlib.py +21 -7
  31. package/plugins/ca-pi/hooks/_reconciliationlib.py +170 -3
  32. package/plugins/ca-pi/hooks/_releaselib.py +6 -6
  33. package/plugins/ca-pi/hooks/_sprintapprovallib.py +52 -2
  34. package/plugins/ca-pi/hooks/artifact-reconcile.py +13 -4
  35. package/plugins/ca-pi/hooks/hostapi.py +1 -1
  36. package/plugins/ca-pi/hooks/init-codearbiter.py +34 -5
  37. package/plugins/ca-pi/includes/artifacts-attestation.md +434 -0
  38. package/plugins/ca-pi/includes/artifacts.md +133 -492
  39. package/plugins/ca-pi/includes/author-tdd-workflow.md +32 -21
  40. package/plugins/ca-pi/includes/fresh-verification.md +14 -12
  41. package/plugins/ca-pi/includes/harvest.md +3 -3
  42. package/plugins/ca-pi/includes/reference-map.md +1 -1
  43. package/plugins/ca-pi/includes/routing-table.md +3 -3
  44. package/plugins/ca-pi/includes/sprint-authority.md +6 -1
  45. package/plugins/ca-pi/includes/verification-boundary.md +11 -2
  46. package/plugins/ca-pi/package.json +1 -1
  47. package/plugins/ca-pi/routines/INDEX.md +8 -8
  48. package/plugins/ca-pi/routines/brainstorming/SKILL.md +65 -202
  49. package/plugins/ca-pi/routines/commit-gate/SKILL.md +60 -25
  50. package/plugins/ca-pi/routines/context-check/SKILL.md +89 -53
  51. package/plugins/ca-pi/routines/context-creation/SKILL.md +114 -220
  52. package/plugins/ca-pi/routines/decompose/SKILL.md +139 -167
  53. package/plugins/ca-pi/routines/executing-plans/SKILL.md +38 -136
  54. package/plugins/ca-pi/routines/finishing-a-development-branch/SKILL.md +45 -55
  55. package/plugins/ca-pi/routines/refactor/SKILL.md +27 -12
  56. package/plugins/ca-pi/routines/release/SKILL.md +1 -1
  57. package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +80 -230
  58. package/plugins/ca-pi/routines/tdd/SKILL.md +115 -228
  59. package/plugins/ca-pi/routines/writing-plans/SKILL.md +104 -174
  60. package/plugins/ca-pi/skills/ca-audit/SKILL.md +2 -2
  61. package/plugins/ca-pi/skills/ca-checkpoint/SKILL.md +1 -1
  62. package/plugins/ca-pi/skills/ca-commit/SKILL.md +60 -25
  63. package/plugins/ca-pi/skills/ca-context-check/SKILL.md +89 -53
  64. package/plugins/ca-pi/skills/ca-create-context/SKILL.md +114 -220
  65. package/plugins/ca-pi/skills/ca-decompose/SKILL.md +139 -167
  66. package/plugins/ca-pi/skills/ca-feature/SKILL.md +21 -38
  67. package/plugins/ca-pi/skills/ca-fix/SKILL.md +15 -9
  68. package/plugins/ca-pi/skills/ca-init/SKILL.md +18 -40
  69. package/plugins/ca-pi/skills/ca-pr/SKILL.md +45 -55
  70. package/plugins/ca-pi/skills/ca-refactor/SKILL.md +27 -12
  71. package/plugins/ca-pi/skills/ca-release/SKILL.md +1 -1
  72. package/plugins/ca-pi/skills/ca-review/SKILL.md +5 -8
  73. package/plugins/ca-pi/skills/ca-sprint/SKILL.md +11 -25
  74. 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.1"
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) > MAX_RESPONSE:
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 only the finite preview or receipt request for the native workflow."""
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 receipt apply request")
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 native writer only after strict request validation."""
177
- client = ArtifactClient(root, helper_installation(__file__))
178
- client.require_context_workflow()
179
- return client
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
- """Verify the exact spec. Only initial sprint-pair preparation admits a ready draft.
560
+ """Resolve the exact source and safe target without an artifact approval.
570
561
 
571
- Draft preparation never approves the spec or plan and never permits execution.
572
- The ordinary feature/sequential path still requires an approved spec.
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
- _require_host_workflow(client)
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
- approved = client.call(
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
- approved.get("valid") is not True
640
- or approved.get("artifact_id") != spec_artifact_id
641
- or approved.get("normative_sha256") != spec_normative_sha256
642
- or not isinstance(authority, dict)
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
- "AUTHORITY_UNVERIFIED",
649
- "plan authoring requires the exact current ready and approved spec",
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("eligible", {"artifact_id": plan_artifact_id})
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("eligible", {"artifact_id": plan_artifact_id})
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
- """Admit only the packaged native context candidate on a supported host."""
1051
- installed = self.workflow_preflight()
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
- if (installed.get("resources_available") is not True
1055
- or installed.get("host") not in {"codex", "claude"}
1056
- or not isinstance(context, dict)
1057
- or set(context) != {"source_present", "kind_registered", "host_default_enabled", "qualification", "mutation_available"}
1058
- or context["source_present"] is not True
1059
- or context["kind_registered"] is not True
1060
- or context["host_default_enabled"] is not False
1061
- or context["qualification"] != "package-required"
1062
- or context["mutation_available"] is not True
1063
- or not _context_release_qualified(self.installation)):
1064
- raise ArtifactError("CONTEXT_WORKFLOW_UNAVAILABLE", "installed repository-context workflow is not qualified")
1065
- return {**context, "host_default_enabled": True, "qualification": "packaged-candidate"}
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-evidence-context", "context-finalize-evidence-context", "context-apply"}:
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 per-call authority validation, deliberately not an mtime cache."""
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.get("governs") or [], "artifact_html": True}
1266
- for x in client.index("spec") if x["authority"]["authority_verified"] and x.get("governs")]
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
- blocked = (any(item.get("status") in ("conflict", "unresolved", "unknown")
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 an applicable critical constraint is unresolved or conflicting. "
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 approved spec and plan identities, existing stage gates, and "
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
- # and H-05/H-11/H-18 have no equivalent sanctioned interpreter caller to
504
- # spare — narrowing them the same way would reopen exactly the gap this
505
- # closes for no offsetting benefit. `[\s\S]*` (not `[^\n]*`), for the same
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) or LOG_INTERP_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 explicit gap is returned.
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': 'blocked', 'binding': binding, 'attempt': None,
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
- blocked = (any(item.get('status') in ('conflict', 'unresolved', 'unknown')
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 existing caller and its gates. The packet is scoped '
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 applicable context '
386
- 'is unresolved or conflicting. Bounded read-only source orientation '
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 validate the test against the actual defect or an honest '
391
- 'fault fixture; never change production behavior merely to manufacture '
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 blocked else 'requires_actor_check',
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 prepare_review_input(packet, *, reviewer, required_refs):
400
- """Prepare bounded reviewer input; leave admission to the actual review actor.
399
+ def check_task_context(packet, *, required_refs):
400
+ """Check required references and applicable constraints in a selected packet.
401
401
 
402
- The caller identifies task-critical refs after read-only orientation. Missing
403
- refs and applicable constraint/instruction conflicts block a substantive
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('INVALID_REVIEW_CONTEXT')
414
+ raise SelectionError('INVALID_TASK_CONTEXT')
419
415
  required = list(required_refs)
420
- if any(type(ref) is not str or len(ref.encode('utf-8')) > 512
421
- or not ref.strip() for ref in required):
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('INVALID_REVIEW_CONTEXT')
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('INVALID_REVIEW_CONTEXT')
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('INVALID_REVIEW_CONTEXT')
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('INVALID_REVIEW_CONTEXT')
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': missing, 'blockers': blockers,
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': required}}
467
+ 'required_refs': requirements['required_refs']}}