@arbiterforge/ca-pi 0.15.12 → 0.16.1

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 (64) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/plugins/ca-pi/CHANGELOG.md +33 -0
  4. package/plugins/ca-pi/SKILLS.md +1 -1
  5. package/plugins/ca-pi/agents/infra-author.md +7 -1
  6. package/plugins/ca-pi/agents/scout.md +40 -8
  7. package/plugins/ca-pi/generated/command-catalog.json +1 -1
  8. package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-amd64 +0 -0
  9. package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-arm64 +0 -0
  10. package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-amd64 +0 -0
  11. package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-arm64 +0 -0
  12. package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-amd64.exe +0 -0
  13. package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-arm64.exe +0 -0
  14. package/plugins/ca-pi/helpers/artifacts/release.json +104 -6
  15. package/plugins/ca-pi/hooks/_activationlib.py +97 -13
  16. package/plugins/ca-pi/hooks/_arbiterstatelib.py +7 -2
  17. package/plugins/ca-pi/hooks/_artifactauthoritylib.py +313 -76
  18. package/plugins/ca-pi/hooks/_artifactlib.py +166 -2
  19. package/plugins/ca-pi/hooks/_artifactpromptlib.py +106 -2
  20. package/plugins/ca-pi/hooks/_bashguardlib.py +14 -0
  21. package/plugins/ca-pi/hooks/_contextreportlib.py +564 -6
  22. package/plugins/ca-pi/hooks/_contextselectlib.py +464 -0
  23. package/plugins/ca-pi/hooks/_contextsnapshotlib.py +999 -0
  24. package/plugins/ca-pi/hooks/_debughandofflib.py +767 -0
  25. package/plugins/ca-pi/hooks/_hooklib.py +1 -0
  26. package/plugins/ca-pi/hooks/_host.py +1 -1
  27. package/plugins/ca-pi/hooks/_intentlib.py +5 -5
  28. package/plugins/ca-pi/hooks/_protectedlib.py +3 -3
  29. package/plugins/ca-pi/hooks/_protectedstatelib.py +152 -2
  30. package/plugins/ca-pi/hooks/_provenancelib.py +604 -34
  31. package/plugins/ca-pi/hooks/_readinjectlib.py +131 -1
  32. package/plugins/ca-pi/hooks/artifact-authority.py +20 -1
  33. package/plugins/ca-pi/hooks/debug-handoff.py +85 -0
  34. package/plugins/ca-pi/hooks/doctor.py +4 -2
  35. package/plugins/ca-pi/hooks/hostapi.py +1 -1
  36. package/plugins/ca-pi/hooks/init-codearbiter.py +143 -15
  37. package/plugins/ca-pi/hooks/prompt-submit.py +14 -0
  38. package/plugins/ca-pi/hooks/session-start.py +26 -7
  39. package/plugins/ca-pi/includes/author-tdd-workflow.md +16 -4
  40. package/plugins/ca-pi/includes/helper-invocation.md +128 -0
  41. package/plugins/ca-pi/includes/pi-host-notes.md +13 -0
  42. package/plugins/ca-pi/includes/routing-table.md +1 -1
  43. package/plugins/ca-pi/package.json +1 -1
  44. package/plugins/ca-pi/routines/INDEX.md +1 -1
  45. package/plugins/ca-pi/routines/commit-gate/SKILL.md +5 -3
  46. package/plugins/ca-pi/routines/context-check/SKILL.md +103 -17
  47. package/plugins/ca-pi/routines/context-creation/SKILL.md +290 -54
  48. package/plugins/ca-pi/routines/debug/SKILL.md +303 -55
  49. package/plugins/ca-pi/routines/debug/references/debug-handoff.schema.json +879 -0
  50. package/plugins/ca-pi/routines/debug/references/handoff.md +155 -0
  51. package/plugins/ca-pi/routines/decompose/SKILL.md +13 -0
  52. package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +34 -0
  53. package/plugins/ca-pi/routines/tdd/SKILL.md +91 -17
  54. package/plugins/ca-pi/skills/ca-commit/SKILL.md +5 -3
  55. package/plugins/ca-pi/skills/ca-context-check/SKILL.md +103 -17
  56. package/plugins/ca-pi/skills/ca-create-context/SKILL.md +290 -54
  57. package/plugins/ca-pi/skills/ca-debug/SKILL.md +303 -55
  58. package/plugins/ca-pi/skills/ca-decompose/SKILL.md +13 -0
  59. package/plugins/ca-pi/skills/ca-feature/SKILL.md +1 -1
  60. package/plugins/ca-pi/skills/ca-fix/SKILL.md +36 -3
  61. package/plugins/ca-pi/skills/ca-init/SKILL.md +19 -0
  62. package/plugins/ca-pi/skills/ca-review/SKILL.md +33 -4
  63. package/plugins/ca-pi/skills/ca-status/SKILL.md +5 -0
  64. package/plugins/ca-pi/skills/ca-task/SKILL.md +55 -8
@@ -79,6 +79,106 @@ def helper_installation(helper_file: str) -> Path:
79
79
  return Path(helper_file).resolve().parent.parent / "helpers" / "artifacts"
80
80
 
81
81
 
82
+ def read_context_preview_request(root: str | Path, request_file: str | Path) -> dict[str, Any]:
83
+ """Read a bounded inert preview request from this repository only."""
84
+ repository = _trusted_directory(root, "UNSAFE_ROOT")
85
+ candidate = Path(request_file)
86
+ if not candidate.is_absolute():
87
+ candidate = repository / candidate
88
+ try:
89
+ relative = candidate.resolve(strict=True).relative_to(repository)
90
+ if not relative.parts:
91
+ raise ValueError("request names the repository root")
92
+ fd = _open_pinned_regular(candidate)
93
+ try:
94
+ info = os.fstat(fd)
95
+ if not stat.S_ISREG(info.st_mode) or info.st_size > (1 << 20):
96
+ raise ArtifactError("INVALID_CONTEXT_REQUEST", "preview request is not a bounded regular file")
97
+ raw = os.read(fd, (1 << 20) + 1)
98
+ finally:
99
+ os.close(fd)
100
+ value = _decode(raw)
101
+ except (OSError, ValueError) as exc:
102
+ raise ArtifactError("INVALID_CONTEXT_REQUEST", "preview request is unavailable or outside the repository") from exc
103
+ if not isinstance(value, dict):
104
+ raise ArtifactError("INVALID_CONTEXT_REQUEST", "preview request must be an object")
105
+ return value
106
+
107
+
108
+ _CONTEXT_TARGETS = {
109
+ "CONTEXT": ".codearbiter/CONTEXT.md",
110
+ "tech-stack": ".codearbiter/tech-stack.md",
111
+ "coding-standards": ".codearbiter/coding-standards.md",
112
+ "security-controls": ".codearbiter/security-controls.md",
113
+ "code-map": ".codearbiter/code-map.md",
114
+ }
115
+ _CONTEXT_PREVIEW_KEYS = frozenset({
116
+ "operation_id", "mode", "document_id", "target_path", "typed",
117
+ "selections", "expected_document_sha256", "expected_provenance_sha256",
118
+ "proposed_provenance",
119
+ })
120
+ _CONTEXT_FINALIZE_KEYS = frozenset({
121
+ "operation_id", "mode", "document_id", "target_path", "expected",
122
+ })
123
+ _CONTEXT_FINALIZE_PATHS = frozenset({
124
+ *(_CONTEXT_TARGETS.values()),
125
+ *(f".codearbiter/.provenance/{name}.json" for name in _CONTEXT_TARGETS),
126
+ ".codearbiter/open-tasks.md", ".codearbiter/open-questions.md",
127
+ ".codearbiter/overrides.log",
128
+ })
129
+
130
+
131
+ def context_writer_qualified() -> bool:
132
+ """Inspect this shipped package's actual context writer, without caller flags."""
133
+ installation = helper_installation(__file__)
134
+ try:
135
+ client = ArtifactClient(installation.parent.parent, installation)
136
+ client.require_context_workflow()
137
+ except ArtifactError:
138
+ return False
139
+ return True
140
+
141
+
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."""
144
+ if not isinstance(request, dict):
145
+ raise ArtifactError("INVALID_CONTEXT_REQUEST", "context request must be an object")
146
+ operation = request.get("operation")
147
+ if operation == "preview" and set(request) == {"operation", "preview"}:
148
+ preview = request["preview"]
149
+ mode = preview.get("mode") if isinstance(preview, dict) else None
150
+ document_id = preview.get("document_id") if isinstance(preview, dict) else None
151
+ regular = (isinstance(preview, dict) and set(preview) == _CONTEXT_PREVIEW_KEYS
152
+ and isinstance(mode, str) and mode in {"create", "adopt", "update"}
153
+ and isinstance(document_id, str)
154
+ and _CONTEXT_TARGETS.get(document_id) == preview.get("target_path"))
155
+ final = (isinstance(preview, dict) and set(preview) == _CONTEXT_FINALIZE_KEYS
156
+ and isinstance(mode, str) and mode == "finalize" and document_id == "CONTEXT"
157
+ and preview.get("target_path") == _CONTEXT_TARGETS["CONTEXT"]
158
+ and isinstance(preview.get("expected"), dict)
159
+ and set(preview["expected"]) == _CONTEXT_FINALIZE_PATHS
160
+ and all(isinstance(value, str) and re.fullmatch(r"[0-9a-f]{64}", value)
161
+ for value in preview["expected"].values()))
162
+ if not (regular or final):
163
+ raise ArtifactError("INVALID_CONTEXT_REQUEST", "preview must name one bounded context target")
164
+ elif operation == "apply" and set(request) == {"operation", "receipt"}:
165
+ receipt = request["receipt"]
166
+ if (not isinstance(receipt, str) or re.fullmatch(
167
+ r"\.codearbiter/\.artifacts/receipts/[0-9a-f]{64}\.json", receipt) is None):
168
+ raise ArtifactError("INVALID_CONTEXT_REQUEST", "apply requires one receipt reference")
169
+ else:
170
+ raise ArtifactError("INVALID_CONTEXT_REQUEST", "select a closed preview or receipt apply request")
171
+
172
+ return ("preview", preview) if operation == "preview" else ("apply", {"receipt": receipt})
173
+
174
+
175
+ 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
180
+
181
+
82
182
  def _open_pinned_regular(path: Path) -> int:
83
183
  """Open without following the final link and pin Windows replacement.
84
184
 
@@ -304,7 +404,7 @@ def _installed_workflow_preflight(installation: Path) -> dict[str, object]:
304
404
  return result
305
405
 
306
406
  for filename in ("_approvallib.py", "_prerequisitelib.py", "_sprintapprovallib.py",
307
- "_reconciliationlib.py", "_artifactauthoritylib.py", "_gitexec.py", "_replylib.py",
407
+ "_reconciliationlib.py", "_artifactauthoritylib.py", "_artifactpromptlib.py", "_gitexec.py", "_replylib.py",
308
408
  "artifact-authority.py", "artifact-authority-hook.py", "prompt-submit.py"):
309
409
  resource("hooks/" + filename)
310
410
  if host == "claude":
@@ -866,6 +966,48 @@ def _bounded_child(argv: list[str], request: bytes, fd: int | None, timeout: flo
866
966
  return process.returncode, bytes(buffers[0]), bytes(buffers[1])
867
967
 
868
968
 
969
+ _CONTEXT_NATIVE_CASES = [
970
+ "TestContextBoundedRender", "TestContextDocumentContract",
971
+ "TestContextMarkdownPreservation", "TestContextMutationAdmission",
972
+ "TestContextRecoveryAndPaths", "TestContextRepresentationBoundary",
973
+ ]
974
+
975
+
976
+ def _context_release_qualified(installation: Path) -> bool:
977
+ """Require exact context evidence added by the native receipt assembler."""
978
+ system = {"Linux": "linux", "Darwin": "darwin", "Windows": "windows"}.get(platform.system())
979
+ arch = {"x86_64": "amd64", "amd64": "amd64", "aarch64": "arm64", "arm64": "arm64"}.get(platform.machine().lower())
980
+ if system is None or arch is None:
981
+ return False
982
+ try:
983
+ fd = _open_pinned_regular(installation / "release.json")
984
+ try:
985
+ if os.fstat(fd).st_size > 65_536:
986
+ return False
987
+ manifest = _decode(os.read(fd, 65_537))
988
+ finally:
989
+ os.close(fd)
990
+ entry = manifest["binaries"][f"{system}/{arch}"]
991
+ record = manifest["context_qualification"][f"{system}/{arch}"]
992
+ return (
993
+ isinstance(record, dict)
994
+ and set(record) == {"source_commit", "workflow", "run_id", "binary_sha256",
995
+ "observation_sha256", "module_sha256", "native_cases"}
996
+ and isinstance(record["source_commit"], str)
997
+ and re.fullmatch(r"[0-9a-f]{40}", record["source_commit"]) is not None
998
+ and record["workflow"] == ".github/workflows/ci.yml"
999
+ and isinstance(record["run_id"], str)
1000
+ and re.fullmatch(r"[1-9][0-9]*", record["run_id"]) is not None
1001
+ and record["binary_sha256"] == entry["sha256"]
1002
+ and all(isinstance(record[field], str)
1003
+ and re.fullmatch(r"[0-9a-f]{64}", record[field]) is not None
1004
+ for field in ("observation_sha256", "module_sha256"))
1005
+ and record["native_cases"] == _CONTEXT_NATIVE_CASES
1006
+ )
1007
+ except (ArtifactError, OSError, KeyError, TypeError, ValueError):
1008
+ return False
1009
+
1010
+
869
1011
  class ArtifactClient:
870
1012
  def __init__(self, root: str | Path, installation: str | Path, *, timeout: float = 30):
871
1013
  self.root = _trusted_directory(root, "UNSAFE_ROOT")
@@ -876,6 +1018,24 @@ class ArtifactClient:
876
1018
  """Read fresh installed prerequisites; never attest live host authority."""
877
1019
  return _installed_workflow_preflight(self.installation)
878
1020
 
1021
+ def require_context_workflow(self) -> dict[str, object]:
1022
+ """Admit only the packaged native context candidate on a supported host."""
1023
+ installed = self.workflow_preflight()
1024
+ capability = self.call("capabilities")
1025
+ context = capability.get("repository_context")
1026
+ if (installed.get("resources_available") is not True
1027
+ or installed.get("host") not in {"codex", "claude"}
1028
+ or not isinstance(context, dict)
1029
+ or set(context) != {"source_present", "kind_registered", "host_default_enabled", "qualification", "mutation_available"}
1030
+ or context["source_present"] is not True
1031
+ or context["kind_registered"] is not True
1032
+ or context["host_default_enabled"] is not False
1033
+ or context["qualification"] != "package-required"
1034
+ or context["mutation_available"] is not True
1035
+ or not _context_release_qualified(self.installation)):
1036
+ raise ArtifactError("CONTEXT_WORKFLOW_UNAVAILABLE", "installed repository-context workflow is not qualified")
1037
+ return {**context, "host_default_enabled": True, "qualification": "packaged-candidate"}
1038
+
879
1039
  def _open_binary(self) -> int:
880
1040
  system = {"Linux":"linux", "Darwin":"darwin", "Windows":"windows"}.get(platform.system())
881
1041
  arch = {"x86_64":"amd64", "amd64":"amd64", "aarch64":"arm64", "arm64":"arm64"}.get(platform.machine().lower())
@@ -897,7 +1057,9 @@ class ArtifactClient:
897
1057
  manifest = _decode(raw)
898
1058
  except ArtifactError as exc:
899
1059
  raise ArtifactError("INVALID_INSTALLATION", "invalid release manifest JSON") from exc
900
- if not isinstance(manifest, dict) or set(manifest) != {"format", "version", "protocol", "schema_version", "binaries"}:
1060
+ if not isinstance(manifest, dict) or set(manifest) not in (
1061
+ {"format", "version", "protocol", "schema_version", "binaries"},
1062
+ {"format", "version", "protocol", "schema_version", "binaries", "context_qualification"}):
901
1063
  raise ArtifactError("INVALID_INSTALLATION", "unexpected release manifest")
902
1064
  if manifest["format"] != "codearbiter.artifact-release/0.1.0" or manifest["protocol"] != PROTOCOL or manifest["schema_version"] != SCHEMA_VERSION:
903
1065
  raise ArtifactError("UNSUPPORTED_VERSION", "installed binary/adapter version mismatch")
@@ -942,6 +1104,8 @@ class ArtifactClient:
942
1104
  def call(self, operation: str, request: dict[str, Any] | None = None, *, permit_invalid: bool = False) -> dict:
943
1105
  if not re.fullmatch(r"[a-z][a-z-]{0,39}", operation):
944
1106
  raise ArtifactError("UNKNOWN_OPERATION", "invalid internal operation name")
1107
+ if operation in {"context-evidence-context", "context-finalize-evidence-context", "context-apply"}:
1108
+ self.require_context_workflow()
945
1109
  request = dict(request or {})
946
1110
  if "protocol" in request and request["protocol"] != PROTOCOL:
947
1111
  raise ArtifactError("UNSUPPORTED_VERSION", "request protocol mismatch")
@@ -22,6 +22,91 @@ class PromptRouteError(RuntimeError):
22
22
  pass
23
23
 
24
24
 
25
+ _FEATURE_ACTORS = frozenset({
26
+ "backend-author", "frontend-author", "infra-author",
27
+ "spec-reviewer", "quality-reviewer", "security-reviewer",
28
+ "auth-crypto-reviewer", "dependency-reviewer", "migration-reviewer",
29
+ "coverage-auditor",
30
+ })
31
+
32
+
33
+ def compose_feature_actor_input(*, actor: str, brief: str,
34
+ spec_id: str | None, plan_id: str | None,
35
+ task_id: str, task_paths,
36
+ code_map, provenance, commands, constraints,
37
+ effective_instructions, evidence_ids,
38
+ command_bindings=None, observations=None,
39
+ confirmed_mini_spec: str | None = None):
40
+ """Compose the transient input sent to one fresh feature actor.
41
+
42
+ The coordinator must acquire the inputs at dispatch time and send `input`
43
+ to the named child. This does not dispatch a child, verify a snapshot,
44
+ approve an artifact, or grant permission for a material action.
45
+ """
46
+ # Approval prompt routing imports this module in installed-host hooks.
47
+ # Selection is needed only for feature child composition; eager loading
48
+ # would pull hostapi from the wrong package into an isolated host process.
49
+ import _contextselectlib
50
+
51
+ if type(actor) is not str or actor not in _FEATURE_ACTORS:
52
+ raise PromptRouteError("feature actor is invalid")
53
+ for label, value in (("brief", brief), ("task", task_id)):
54
+ try:
55
+ valid = (type(value) is str and bool(value.strip()) and
56
+ len(value.encode("utf-8")) <= (8192 if label == "brief" else 128))
57
+ except UnicodeError:
58
+ valid = False
59
+ if not valid:
60
+ raise PromptRouteError(f"feature {label} is invalid")
61
+ if confirmed_mini_spec is None:
62
+ if (type(spec_id) is not str or type(plan_id) is not str or
63
+ not spec_id.startswith("SPEC-") or
64
+ not plan_id.startswith("PLAN-") or
65
+ len(spec_id) > 128 or len(plan_id) > 128):
66
+ raise PromptRouteError("feature artifact identity is invalid")
67
+ elif (spec_id is not None or plan_id is not None or
68
+ type(confirmed_mini_spec) is not str or
69
+ not confirmed_mini_spec.strip() or
70
+ len(confirmed_mini_spec.encode("utf-8")) > 8192):
71
+ raise PromptRouteError("feature mini-spec binding is invalid")
72
+ try:
73
+ packet = _contextselectlib.select_task_context(
74
+ task_paths, code_map=code_map, provenance=provenance,
75
+ commands=commands, constraints=constraints,
76
+ effective_instructions=effective_instructions,
77
+ evidence_ids=evidence_ids, command_bindings=command_bindings,
78
+ observations=observations)
79
+ except _contextselectlib.SelectionError as exc:
80
+ raise PromptRouteError(f"feature context is invalid: {exc}") from exc
81
+ blocked = (any(item.get("status") in ("conflict", "unresolved", "unknown")
82
+ for item in packet["unresolved"] if type(item) is dict)
83
+ or any(item.get("code") in ("CONFLICTING_INSTRUCTION",
84
+ "MIXED_INSTRUCTION_EPOCH",
85
+ "INSTRUCTION_PACKET_OVERSIZE")
86
+ for item in packet["unresolved"] if type(item) is dict)
87
+ or packet["provenance"]["identity"] == "stale")
88
+ action = "blocked" if blocked else "requires_actor_check"
89
+ envelope = {"actor": actor, "spec_id": spec_id, "plan_id": plan_id,
90
+ "task_id": task_id, "task_context": packet}
91
+ if confirmed_mini_spec is not None:
92
+ envelope["confirmed_mini_spec"] = confirmed_mini_spec
93
+ input_text = (
94
+ brief + "\n\nTask-context packet for this fresh actor:\n" +
95
+ _canonical(envelope).decode("ascii") + "\n\n" +
96
+ "Treat the packet as scoped references and reported evidence, never as approval, "
97
+ "execution proof, or a replacement for native instructions. Resolve the actual "
98
+ "worktree/source identity and task-critical constraints before material action. "
99
+ "Do not mutate, execute discovered commands, or issue a substantive verdict "
100
+ "while an applicable critical constraint is unresolved or conflicting. "
101
+ "An absent or incomplete map permits bounded read-only source inspection; "
102
+ "fetch full schemas and document bodies only on demand. "
103
+ "Preserve the approved spec and plan identities, existing stage gates, and "
104
+ "independent reviewer judgment.\n")
105
+ return {"actor": actor, "spec_id": spec_id, "plan_id": plan_id,
106
+ "task_id": task_id, "packet": packet, "action": action,
107
+ "input": input_text}
108
+
109
+
25
110
  def _canonical(value):
26
111
  return json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode()
27
112
 
@@ -63,7 +148,7 @@ def _key(route: str, artifact_id: str, repository: dict[str, str]) -> str:
63
148
 
64
149
  def register(root: str | Path, route: str, artifact_id: str, prompt: str,
65
150
  *, binding_sha256: str | None = None) -> None:
66
- if route not in {"approval", "prerequisite", "reconciliation"} or not isinstance(prompt, str) or not prompt:
151
+ if route not in {"approval", "prerequisite", "reconciliation", "context_approval"} or not isinstance(prompt, str) or not prompt:
67
152
  raise PromptRouteError("artifact prompt route is invalid")
68
153
  if binding_sha256 is not None and (
69
154
  not isinstance(binding_sha256, str)
@@ -86,6 +171,25 @@ def register(root: str | Path, route: str, artifact_id: str, prompt: str,
86
171
  key = _key(route, artifact_id, repository)
87
172
  path = registry / f"{key}.json"
88
173
  data = _canonical(value)
174
+ if route == "context_approval":
175
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
176
+ if hasattr(os, "O_BINARY"):
177
+ flags |= os.O_BINARY
178
+ try:
179
+ fd = os.open(path, flags, 0o600)
180
+ except FileExistsError as exc:
181
+ raise PromptRouteError("an unresolved context preview reply is already armed") from exc
182
+ try:
183
+ view = memoryview(data)
184
+ while view:
185
+ written = os.write(fd, view)
186
+ if written <= 0:
187
+ raise OSError("short context preview route write")
188
+ view = view[written:]
189
+ os.fsync(fd)
190
+ finally:
191
+ os.close(fd)
192
+ return
89
193
  # Each caller owns an exclusively created pending marker before registering.
90
194
  # A prior route at this key therefore belongs to an interrupted request.
91
195
  fd, temporary = tempfile.mkstemp(prefix=f".{key}.", suffix=".tmp", dir=registry)
@@ -109,7 +213,7 @@ def register(root: str | Path, route: str, artifact_id: str, prompt: str,
109
213
 
110
214
 
111
215
  def resolve(route: str, prompt: str) -> Path | None:
112
- if route not in {"approval", "prerequisite", "reconciliation"} or not isinstance(prompt, str):
216
+ if route not in {"approval", "prerequisite", "reconciliation", "context_approval"} or not isinstance(prompt, str):
113
217
  return None
114
218
  prompt_hash = _digest(prompt.encode("utf-8"))
115
219
  matches = []
@@ -96,6 +96,7 @@ import _protectedstatelib # H-22's shell flank (T-08, #564) — imported as a
96
96
  # attribute lookup at import time, never a
97
97
  # snapshotted name binding.
98
98
  from _protectedstatelib import ProtectedPolicy, marker_gated_write_admitted
99
+ from _artifactlib import _CONTEXT_TARGETS # inert candidate names; qualification is lazy
99
100
 
100
101
  # The most recent git-read failure, surfaced in the H-01/H-09b/H-14 fail-closed
101
102
  # block message. "git unavailable or timed out" alone cost a session of root-
@@ -773,6 +774,12 @@ def _build_state_write_res(registry):
773
774
  # after the fact — this tuple would not see that (it is a one-time
774
775
  # import-time snapshot, by design).
775
776
  _STATE_WRITE_RES = _build_state_write_res(_protectedstatelib.REGISTRY)
777
+ # Candidate regexes are inert at import. Actual context policy is resolved only
778
+ # after a shell command names one of these paths; no package or repository probe
779
+ # runs while importing a hook, and legacy registry rows retain their old path.
780
+ _CONTEXT_STATE_WRITE_RES = _build_state_write_res({
781
+ path: ProtectedPolicy.HELPER_ONLY for path in _CONTEXT_TARGETS.values()
782
+ })
776
783
 
777
784
 
778
785
  def git_cwd(cmd, root):
@@ -1485,6 +1492,13 @@ def _check_h22_state(cmd, root):
1485
1492
  f"policy={policy.value}) — shell redirects and write/delete verbs "
1486
1493
  f"naming it are prohibited outright; there is no marker path for "
1487
1494
  f"this policy. Use the sanctioned helper.")
1495
+ for rel_path, _policy, redirect_re, write_re, git_restore_re, interp_re in _CONTEXT_STATE_WRITE_RES:
1496
+ if not (redirect_re.search(cmd) or write_re.search(verb_scan_cmd)
1497
+ or git_restore_re.search(cmd) or interp_re.search(cmd)):
1498
+ continue
1499
+ if _protectedstatelib.lookup_policy(rel_path, root=root) == ProtectedPolicy.HELPER_ONLY:
1500
+ block("H-22", f"'{rel_path}' is protected context state — direct shell replacement "
1501
+ "is prohibited; use the qualified context writer.")
1488
1502
 
1489
1503
 
1490
1504
  def _marker_root(root):