@arbiterforge/ca-pi 0.15.10 → 0.16.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 (69) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/plugins/ca-pi/CHANGELOG.md +45 -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 +519 -16
  18. package/plugins/ca-pi/hooks/_artifactlib.py +244 -7
  19. package/plugins/ca-pi/hooks/_artifactpromptlib.py +106 -2
  20. package/plugins/ca-pi/hooks/_bashguardlib.py +23 -8
  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 +2 -1
  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/_sensitivelib.py +28 -14
  33. package/plugins/ca-pi/hooks/artifact-authority-hook.py +1 -0
  34. package/plugins/ca-pi/hooks/artifact-authority.py +25 -1
  35. package/plugins/ca-pi/hooks/debug-handoff.py +85 -0
  36. package/plugins/ca-pi/hooks/doctor.py +4 -2
  37. package/plugins/ca-pi/hooks/git-enforce.py +7 -6
  38. package/plugins/ca-pi/hooks/hostapi.py +1 -1
  39. package/plugins/ca-pi/hooks/init-codearbiter.py +143 -15
  40. package/plugins/ca-pi/hooks/prompt-submit.py +14 -0
  41. package/plugins/ca-pi/hooks/session-start.py +26 -7
  42. package/plugins/ca-pi/includes/artifacts.md +41 -0
  43. package/plugins/ca-pi/includes/author-tdd-workflow.md +16 -4
  44. package/plugins/ca-pi/includes/helper-invocation.md +128 -0
  45. package/plugins/ca-pi/includes/pi-host-notes.md +13 -0
  46. package/plugins/ca-pi/includes/routing-table.md +1 -1
  47. package/plugins/ca-pi/includes/security-gate-record.md +5 -3
  48. package/plugins/ca-pi/package.json +1 -1
  49. package/plugins/ca-pi/routines/INDEX.md +1 -1
  50. package/plugins/ca-pi/routines/commit-gate/SKILL.md +5 -3
  51. package/plugins/ca-pi/routines/context-check/SKILL.md +103 -17
  52. package/plugins/ca-pi/routines/context-creation/SKILL.md +290 -54
  53. package/plugins/ca-pi/routines/debug/SKILL.md +303 -55
  54. package/plugins/ca-pi/routines/debug/references/debug-handoff.schema.json +879 -0
  55. package/plugins/ca-pi/routines/debug/references/handoff.md +155 -0
  56. package/plugins/ca-pi/routines/decompose/SKILL.md +13 -0
  57. package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +34 -0
  58. package/plugins/ca-pi/routines/tdd/SKILL.md +91 -17
  59. package/plugins/ca-pi/skills/ca-commit/SKILL.md +5 -3
  60. package/plugins/ca-pi/skills/ca-context-check/SKILL.md +103 -17
  61. package/plugins/ca-pi/skills/ca-create-context/SKILL.md +290 -54
  62. package/plugins/ca-pi/skills/ca-debug/SKILL.md +303 -55
  63. package/plugins/ca-pi/skills/ca-decompose/SKILL.md +13 -0
  64. package/plugins/ca-pi/skills/ca-feature/SKILL.md +1 -1
  65. package/plugins/ca-pi/skills/ca-fix/SKILL.md +36 -3
  66. package/plugins/ca-pi/skills/ca-init/SKILL.md +19 -0
  67. package/plugins/ca-pi/skills/ca-review/SKILL.md +33 -4
  68. package/plugins/ca-pi/skills/ca-status/SKILL.md +5 -0
  69. package/plugins/ca-pi/skills/ca-task/SKILL.md +55 -8
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env python3
2
2
  """Bounded stdlib bridge to an installation-pinned ca-artifact executable.
3
3
 
4
- No imports perform I/O. No PATH search, environment-selected executable, schema
5
- parser duplication, command execution from a document, or new host registration.
4
+ No imports perform I/O. No native-binary PATH search, environment-selected
5
+ engine, schema parser duplication, document command execution, or host registration.
6
6
  The caller supplies a trusted installation directory, never a repository setting.
7
7
  """
8
8
  from __future__ import annotations
@@ -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":
@@ -742,10 +842,83 @@ def _preflight_current_acceptance(
742
842
  }
743
843
 
744
844
 
745
- def _bounded_child(argv: list[str], request: bytes, fd: int, timeout: float) -> tuple[int, bytes, bytes]:
845
+ def _native_git_excludes(git: Path, root: Path) -> dict[str, str]:
846
+ """Resolve only the governed root's ignore policy with the producer's Git.
847
+
848
+ Git expands the explicit setting or its documented XDG/home default. No
849
+ general configuration or host environment crosses the native boundary.
850
+ Mapped roots with different policies still must pass exact closure equality.
851
+ """
852
+ from _gitexec import root_bound_git_env
853
+
854
+ default = os.environ.get("XDG_CONFIG_HOME")
855
+ default = str(Path(default) / "git" / "ignore") if default else "~/.config/git/ignore"
856
+ environment = root_bound_git_env()
857
+ environment.pop("GIT_CONFIG", None) # git-config-only override; inventory commands ignore it.
858
+ try:
859
+ code, raw, _ = _bounded_child(
860
+ [str(git), "--no-lazy-fetch", "--no-optional-locks", "-C", str(root),
861
+ "-c", "core.fsmonitor=false", "-c", "protocol.allow=never", "config",
862
+ "--null", "--path", "--default", default, "--get", "core.excludesFile"],
863
+ b"", None, 5, environment=environment, cwd=root)
864
+ if code != 0 or not raw.endswith(b"\0") or b"\0" in raw[:-1] or len(raw) > 4096:
865
+ return {}
866
+ value = raw[:-1].decode("utf-8", "strict")
867
+ if not value:
868
+ return {}
869
+ path = Path(value)
870
+ path = (path if path.is_absolute() else root / path).resolve()
871
+ except (ArtifactError, OSError, RuntimeError, UnicodeError, ValueError):
872
+ return {}
873
+ return {"GIT_CONFIG_COUNT": "1", "GIT_CONFIG_KEY_0": "core.excludesFile",
874
+ "GIT_CONFIG_VALUE_0": str(path)}
875
+
876
+
877
+ def _native_child_environment(argv: list[str]) -> dict[str, str]:
878
+ """Transport one host Git identity for fixed native workspace probes only.
879
+
880
+ CODEARBITER_GIT_EXECUTABLE is the shared _gitexec host override. Resolve the
881
+ ordinary host PATH in this bridge, excluding CWD/relative entries and the
882
+ governed checkout; the native engine receives only that identity and the
883
+ resolved core.excludesFile setting, never PATH or general ambient state.
884
+ Cold ordinary operations remain usable when no safe Git is available.
885
+ """
886
+ try:
887
+ root = Path(argv[argv.index("--root") + 1]).resolve(strict=True)
888
+ boundaries = [root, *(parent for parent in root.parents if (parent / ".git").exists())]
889
+ except (ValueError, IndexError, OSError):
890
+ return {}
891
+ configured = os.environ.get("CODEARBITER_GIT_EXECUTABLE")
892
+ if configured:
893
+ candidates = [Path(configured)]
894
+ else:
895
+ name = "git.exe" if os.name == "nt" else "git"
896
+ candidates = [Path(directory) / name for directory in os.environ.get("PATH", "").split(os.pathsep)
897
+ if directory and Path(directory).is_absolute()]
898
+ for candidate in candidates:
899
+ try:
900
+ if not candidate.is_absolute() or any(candidate.is_relative_to(boundary) for boundary in boundaries):
901
+ continue
902
+ resolved = candidate.resolve(strict=True)
903
+ if (any(resolved.is_relative_to(boundary) for boundary in boundaries)
904
+ or not stat.S_ISREG(resolved.stat().st_mode) or not os.access(resolved, os.X_OK)):
905
+ continue
906
+ except (OSError, RuntimeError):
907
+ continue
908
+ return {"CODEARBITER_GIT_EXECUTABLE": str(resolved),
909
+ **_native_git_excludes(resolved, root)}
910
+ return {}
911
+
912
+
913
+ def _bounded_child(argv: list[str], request: bytes, fd: int | None, timeout: float,
914
+ *, environment: dict[str, str] | None = None,
915
+ cwd: Path | None = None) -> tuple[int, bytes, bytes]:
746
916
  options = dict(stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
747
- close_fds=True, start_new_session=True, env={})
748
- if os.name != "nt":
917
+ close_fds=True, start_new_session=True,
918
+ env=_native_child_environment(argv) if environment is None else environment)
919
+ if cwd is not None:
920
+ options["cwd"] = cwd
921
+ if os.name != "nt" and fd is not None:
749
922
  options["pass_fds"] = (fd,)
750
923
  process = subprocess.Popen(argv, **options)
751
924
  buffers = [bytearray(), bytearray()]
@@ -793,6 +966,48 @@ def _bounded_child(argv: list[str], request: bytes, fd: int, timeout: float) ->
793
966
  return process.returncode, bytes(buffers[0]), bytes(buffers[1])
794
967
 
795
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
+
796
1011
  class ArtifactClient:
797
1012
  def __init__(self, root: str | Path, installation: str | Path, *, timeout: float = 30):
798
1013
  self.root = _trusted_directory(root, "UNSAFE_ROOT")
@@ -803,6 +1018,24 @@ class ArtifactClient:
803
1018
  """Read fresh installed prerequisites; never attest live host authority."""
804
1019
  return _installed_workflow_preflight(self.installation)
805
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
+
806
1039
  def _open_binary(self) -> int:
807
1040
  system = {"Linux":"linux", "Darwin":"darwin", "Windows":"windows"}.get(platform.system())
808
1041
  arch = {"x86_64":"amd64", "amd64":"amd64", "aarch64":"arm64", "arm64":"arm64"}.get(platform.machine().lower())
@@ -824,7 +1057,9 @@ class ArtifactClient:
824
1057
  manifest = _decode(raw)
825
1058
  except ArtifactError as exc:
826
1059
  raise ArtifactError("INVALID_INSTALLATION", "invalid release manifest JSON") from exc
827
- 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"}):
828
1063
  raise ArtifactError("INVALID_INSTALLATION", "unexpected release manifest")
829
1064
  if manifest["format"] != "codearbiter.artifact-release/0.1.0" or manifest["protocol"] != PROTOCOL or manifest["schema_version"] != SCHEMA_VERSION:
830
1065
  raise ArtifactError("UNSUPPORTED_VERSION", "installed binary/adapter version mismatch")
@@ -869,6 +1104,8 @@ class ArtifactClient:
869
1104
  def call(self, operation: str, request: dict[str, Any] | None = None, *, permit_invalid: bool = False) -> dict:
870
1105
  if not re.fullmatch(r"[a-z][a-z-]{0,39}", operation):
871
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()
872
1109
  request = dict(request or {})
873
1110
  if "protocol" in request and request["protocol"] != PROTOCOL:
874
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 = []
@@ -83,8 +83,8 @@ import sys
83
83
  from _hooklib import (
84
84
  ADR_LIFECYCLE_LOG_BASENAME, AUDIT_LOG_BASENAMES, AUDIT_LOG_FLAT_BASENAMES,
85
85
  AUDIT_LOG_NAMES, CRYPTO_RE, DECISION_AUDIT_LOG_NAMES, DECISION_LOG_BASENAME, DECISIONS_DIR_RE,
86
- GATE_MARKER_NAMES, MARKER_FRESHNESS_MINUTES, SECRET_RE, SECURITY_DIFF_GIT_ARGS, block,
87
- content_digest, is_migration_path, line_digest, marker_fresh, security_scan_diff,
86
+ GATE_MARKER_NAMES, SECRET_RE, SECURITY_DIFF_GIT_ARGS, block,
87
+ content_digest, is_migration_path, line_digest, security_scan_diff,
88
88
  )
89
89
  from _gitexec import git_executable
90
90
  import _gitlib # reused for its spawn-free, worktree-aware (.git-as-a-FILE /
@@ -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):
@@ -1508,11 +1522,12 @@ def _check_h09b_h10b_crypto_secret(commit, add, cwd, root):
1508
1522
  """H-09b / H-10b: BLOCK a commit that introduces crypto/secret changes without
1509
1523
  a recorded security-gate pass. The crypto-compliance / secret-handling skills
1510
1524
  record the pass via hooks/security-pass.py — a marker holding the digest of
1511
- every sensitive line the gate approved. Two checks, both required:
1512
- freshness (< 30 min) AND coverage (every sensitive line being committed is
1513
- in the approved set). Coverage is what closes the TOCTOU window: a pass
1514
- minted for one diff can no longer launder a different diff committed inside
1515
- the freshness window. Scans the staged diff, plus the worktree diff when
1525
+ every sensitive line the gate approved. Two checks, both required: a pass
1526
+ is recorded AND it covers every sensitive line being committed. Coverage is
1527
+ by line digest with no time window (as H-14 binds migrations by content): a
1528
+ pass stays valid for exactly the lines it reviewed, and any new or changed
1529
+ sensitive line is uncovered and blocks, so a pass for one diff can never
1530
+ launder a different one. Scans the staged diff, plus the worktree diff when
1516
1531
  the commit uses -a/--all or the same command stages files."""
1517
1532
  if not commit:
1518
1533
  return
@@ -1541,7 +1556,7 @@ def _check_h09b_h10b_crypto_secret(commit, add, cwd, root):
1541
1556
  tag = "H-09b" if touches_crypto else "H-10b"
1542
1557
  skill = "crypto-compliance" if touches_crypto else "secret-handling"
1543
1558
  marker = os.path.join(_marker_root(root), ".codearbiter", ".markers", "security-gate-passed")
1544
- if not marker_fresh(marker, MARKER_FRESHNESS_MINUTES):
1559
+ if not os.path.isfile(marker):
1545
1560
  block(tag, f"This commit introduces {kind} changes, but no security-gate pass is "
1546
1561
  f"recorded (.codearbiter/.markers/security-gate-passed). Run the "
1547
1562
  f"{skill} gate (it records the pass), then commit. To bypass a "