@arbiterforge/ca-pi 0.21.3 → 0.21.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@ and durable project context. You decide. codeArbiter enforces.
10
10
  <img alt="Claude Code plugin" src="https://img.shields.io/badge/Claude_Code-plugin-d97757">
11
11
  <img alt="Codex plugin" src="https://img.shields.io/badge/OpenAI_Codex-plugin-10a37f">
12
12
  <img alt="Pi Feature Forge preview" src="https://img.shields.io/badge/ca--pi-Feature_Forge_preview-d97757">
13
- <img alt="version 3.2.3" src="https://img.shields.io/badge/version-3.2.3-2b7489">
13
+ <img alt="version 3.2.5" src="https://img.shields.io/badge/version-3.2.5-2b7489">
14
14
  <img alt="core lanes" src="https://img.shields.io/badge/core_lanes-18-555">
15
15
  <img alt="skills" src="https://img.shields.io/badge/skills-22-555">
16
16
  <img alt="agents" src="https://img.shields.io/badge/agents-20-555">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arbiterforge/ca-pi",
3
- "version": "0.21.3",
3
+ "version": "0.21.5",
4
4
  "license": "AGPL-3.0-only",
5
5
  "repository": {
6
6
  "type": "git",
@@ -4,6 +4,18 @@ All notable changes to `ca-pi` are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.21.5] - 2026-10-19
8
+
9
+ ### Fixed
10
+
11
+ - The H-05 (audit logs), H-11 (ADRs) and H-18 (`CONTEXT.md`) shell guards blocked any interpreter command that merely named a protected path, so a read-only `python -c` read, an issue body citing the path, or an `ArtifactClient` request whose record text named an audit log was refused as a write. Their interpreter legs now block only when the command shows write intent after the interpreter (a write, delete, copy, move or in-place edit call, a non-read `open` mode, PowerShell content cmdlets, or dynamic execution such as `exec`, `eval` or `subprocess`), or when the interpreter is handed a script or module argument. H-18 also lets a `sed` without `-i` or a `w` command read `CONTEXT.md` (#997, #911).
12
+
13
+ ## [0.21.4] - 2026-10-19
14
+
15
+ ### Fixed
16
+
17
+ - On Windows the artifact bridge read its binary's architecture through `platform.machine()`, which asks WMI and then the `PROCESSOR_*` environment variables. A read-only sandbox can block WMI and strip that environment, so reads failed with `UNSUPPORTED_PLATFORM: unsupported architecture`. The bridge now asks the kernel (`IsWow64Process2`) for the native machine, consults no environment, and still fails closed when the OS gives no answer (#986).
18
+
7
19
  ## [0.21.3] - 2026-10-19
8
20
 
9
21
  ### Fixed
@@ -37,12 +37,12 @@
37
37
  },
38
38
  "context_qualification": {
39
39
  "darwin/amd64": {
40
- "source_commit": "406435438171f442305147b23ad8714ffb2de886",
40
+ "source_commit": "0fc246614c8b0a85188ffcee9284f1942b4b3567",
41
41
  "workflow": ".github/workflows/ci.yml",
42
- "run_id": "39261627593",
42
+ "run_id": "39337812494",
43
43
  "binary_sha256": "e501ca2b817c882feec23bc3560223f20d88ca372400f950679bfe01ba47cebd",
44
- "observation_sha256": "92ffe83810fd75c64241d18bb10ebf1f961bdcf2ad85a4523e59499a99dacdf7",
45
- "module_sha256": "896150db84c968920a4397b61b5be1d4d0d3775de7816d172f0bf4885e9e7133",
44
+ "observation_sha256": "c832f3aff1d1c2b553173e16bb4fcd5b7ddf1cbf45c2a57e32840f58232c5a7a",
45
+ "module_sha256": "537452f67f1105572c4fa3829cec4af7354260a96e1614b66daa8fec7eefc256",
46
46
  "native_cases": [
47
47
  "TestContextBoundedRender",
48
48
  "TestContextDocumentContract",
@@ -53,12 +53,12 @@
53
53
  ]
54
54
  },
55
55
  "darwin/arm64": {
56
- "source_commit": "406435438171f442305147b23ad8714ffb2de886",
56
+ "source_commit": "0fc246614c8b0a85188ffcee9284f1942b4b3567",
57
57
  "workflow": ".github/workflows/ci.yml",
58
- "run_id": "39261627593",
58
+ "run_id": "39337812494",
59
59
  "binary_sha256": "0fe9c4672486a28db1340c77d9eebf37bc33774fc34b5c055728f135afe4d7e8",
60
- "observation_sha256": "f32b281e641f4bb8edccb7a058051c28f52bc5fef093bc0c393242217bfdc2bd",
61
- "module_sha256": "896150db84c968920a4397b61b5be1d4d0d3775de7816d172f0bf4885e9e7133",
60
+ "observation_sha256": "8007bb64505d6f4163df70b58d0a52d950f718caf284846b3a79b9936c759dee",
61
+ "module_sha256": "537452f67f1105572c4fa3829cec4af7354260a96e1614b66daa8fec7eefc256",
62
62
  "native_cases": [
63
63
  "TestContextBoundedRender",
64
64
  "TestContextDocumentContract",
@@ -69,12 +69,12 @@
69
69
  ]
70
70
  },
71
71
  "linux/amd64": {
72
- "source_commit": "406435438171f442305147b23ad8714ffb2de886",
72
+ "source_commit": "0fc246614c8b0a85188ffcee9284f1942b4b3567",
73
73
  "workflow": ".github/workflows/ci.yml",
74
- "run_id": "39261627593",
74
+ "run_id": "39337812494",
75
75
  "binary_sha256": "f98d2647144640b226d0fa3fae6369eefbcee477e9d2bb818f767711e6a3e506",
76
- "observation_sha256": "c44e13ff2daaffb5721cc2926d2c92c62b040fd04a7375b188699e67071d7201",
77
- "module_sha256": "896150db84c968920a4397b61b5be1d4d0d3775de7816d172f0bf4885e9e7133",
76
+ "observation_sha256": "d7c40f95cd3189dee10f26db25d63fbc2891181280efc2403dda058912f80236",
77
+ "module_sha256": "537452f67f1105572c4fa3829cec4af7354260a96e1614b66daa8fec7eefc256",
78
78
  "native_cases": [
79
79
  "TestContextBoundedRender",
80
80
  "TestContextDocumentContract",
@@ -85,12 +85,12 @@
85
85
  ]
86
86
  },
87
87
  "linux/arm64": {
88
- "source_commit": "406435438171f442305147b23ad8714ffb2de886",
88
+ "source_commit": "0fc246614c8b0a85188ffcee9284f1942b4b3567",
89
89
  "workflow": ".github/workflows/ci.yml",
90
- "run_id": "39261627593",
90
+ "run_id": "39337812494",
91
91
  "binary_sha256": "7abc7f188851bde8c082c2b812b74378e9e1848541aeb9029ee8551f4b68fdaa",
92
- "observation_sha256": "57342715d43327d295410d61c0f9df7e11ef6f076ac4bda9067d44988548fcda",
93
- "module_sha256": "896150db84c968920a4397b61b5be1d4d0d3775de7816d172f0bf4885e9e7133",
92
+ "observation_sha256": "1bd885c3ecb6efacb789404d0d97db91596943de630c1cecbcee25482f2bf26f",
93
+ "module_sha256": "537452f67f1105572c4fa3829cec4af7354260a96e1614b66daa8fec7eefc256",
94
94
  "native_cases": [
95
95
  "TestContextBoundedRender",
96
96
  "TestContextDocumentContract",
@@ -101,12 +101,12 @@
101
101
  ]
102
102
  },
103
103
  "windows/amd64": {
104
- "source_commit": "406435438171f442305147b23ad8714ffb2de886",
104
+ "source_commit": "0fc246614c8b0a85188ffcee9284f1942b4b3567",
105
105
  "workflow": ".github/workflows/ci.yml",
106
- "run_id": "39261627593",
106
+ "run_id": "39337812494",
107
107
  "binary_sha256": "3fe0a4b867fbcc8a278248f92697b649f4b9b208d51c5168885ba92fcbb73961",
108
- "observation_sha256": "85c96ca98d349c9ecb29a7f00a926899a8da5e39597ac42a9bb052d551d1785e",
109
- "module_sha256": "896150db84c968920a4397b61b5be1d4d0d3775de7816d172f0bf4885e9e7133",
108
+ "observation_sha256": "8439eeadb753215c8e5e983091ff61e1920ac12480d9406e9cc6a837faa7725f",
109
+ "module_sha256": "537452f67f1105572c4fa3829cec4af7354260a96e1614b66daa8fec7eefc256",
110
110
  "native_cases": [
111
111
  "TestContextBoundedRender",
112
112
  "TestContextDocumentContract",
@@ -117,12 +117,12 @@
117
117
  ]
118
118
  },
119
119
  "windows/arm64": {
120
- "source_commit": "406435438171f442305147b23ad8714ffb2de886",
120
+ "source_commit": "0fc246614c8b0a85188ffcee9284f1942b4b3567",
121
121
  "workflow": ".github/workflows/ci.yml",
122
- "run_id": "39261627593",
122
+ "run_id": "39337812494",
123
123
  "binary_sha256": "339210645436d11bca4129f74d4a7885ad12a9489f449669b5f46a6f9cd1056d",
124
- "observation_sha256": "ecd9301e92933159f92991f72be499bc89afa30136545560fdce37d8dd3b0813",
125
- "module_sha256": "896150db84c968920a4397b61b5be1d4d0d3775de7816d172f0bf4885e9e7133",
124
+ "observation_sha256": "062e23804043c352163576ced022881e88e849f1d9b202b14c08958ec9c63ae8",
125
+ "module_sha256": "537452f67f1105572c4fa3829cec4af7354260a96e1614b66daa8fec7eefc256",
126
126
  "native_cases": [
127
127
  "TestContextBoundedRender",
128
128
  "TestContextDocumentContract",
@@ -976,6 +976,45 @@ def _engine_timeout() -> float:
976
976
  return value
977
977
 
978
978
 
979
+ # IMAGE_FILE_MACHINE values IsWow64Process2 reports for the native machine.
980
+ _WINDOWS_NATIVE_MACHINES = {0x8664: "amd64", 0xAA64: "arm64"}
981
+
982
+
983
+ def _windows_native_architecture() -> str | None:
984
+ """Ask Windows itself for the native machine, through IsWow64Process2.
985
+
986
+ platform.machine() asks WMI first and then falls back to PROCESSOR_*
987
+ environment variables. A read-only sandbox can block WMI and strip that
988
+ environment, which left the architecture empty (#986). The kernel call
989
+ needs neither, and the environment is never consulted. None means the OS
990
+ gave no answer, and the caller fails closed."""
991
+ try:
992
+ import ctypes
993
+ query = ctypes.WinDLL("kernel32", use_last_error=True).IsWow64Process2
994
+ except (ImportError, OSError, AttributeError):
995
+ return None
996
+ # BOOL IsWow64Process2(HANDLE, USHORT *process, USHORT *native); the
997
+ # current-process pseudo-handle is -1. One-element arrays pass as pointers.
998
+ query.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
999
+ query.restype = ctypes.c_uint32
1000
+ process, native = (ctypes.c_uint16 * 1)(), (ctypes.c_uint16 * 1)()
1001
+ if not query(ctypes.c_void_p(-1), process, native):
1002
+ return None
1003
+ return _WINDOWS_NATIVE_MACHINES.get(native[0])
1004
+
1005
+
1006
+ def _host_platform() -> tuple[str, str]:
1007
+ """The installed binary's platform cell, or UNSUPPORTED_PLATFORM."""
1008
+ system = {"Linux":"linux", "Darwin":"darwin", "Windows":"windows"}.get(platform.system())
1009
+ if system == "windows":
1010
+ arch = _windows_native_architecture()
1011
+ else:
1012
+ arch = {"x86_64":"amd64", "amd64":"amd64", "aarch64":"arm64", "arm64":"arm64"}.get(platform.machine().lower())
1013
+ if system is None or arch is None:
1014
+ raise ArtifactError("UNSUPPORTED_PLATFORM", "unsupported architecture")
1015
+ return system, arch
1016
+
1017
+
979
1018
  class ArtifactClient:
980
1019
  def __init__(self, root: str | Path, installation: str | Path, *, timeout: float | None = None,
981
1020
  initialize_store: bool = True):
@@ -1013,10 +1052,7 @@ class ArtifactClient:
1013
1052
  return dict(context)
1014
1053
 
1015
1054
  def _open_binary(self) -> int:
1016
- system = {"Linux":"linux", "Darwin":"darwin", "Windows":"windows"}.get(platform.system())
1017
- arch = {"x86_64":"amd64", "amd64":"amd64", "aarch64":"arm64", "arm64":"arm64"}.get(platform.machine().lower())
1018
- if system is None or arch is None:
1019
- raise ArtifactError("UNSUPPORTED_PLATFORM", "unsupported architecture")
1055
+ system, arch = _host_platform()
1020
1056
  manifest_path = self.installation / "release.json"
1021
1057
  try:
1022
1058
  mf = _open_pinned_regular(manifest_path)
@@ -1162,8 +1198,7 @@ class ArtifactClient:
1162
1198
 
1163
1199
  @staticmethod
1164
1200
  def _binary_name() -> str:
1165
- system = {"Linux":"linux", "Darwin":"darwin", "Windows":"windows"}[platform.system()]
1166
- arch = {"x86_64":"amd64", "amd64":"amd64", "aarch64":"arm64", "arm64":"arm64"}[platform.machine().lower()]
1201
+ system, arch = _host_platform()
1167
1202
  return f"ca-artifact-{system}-{arch}" + (".exe" if system == "windows" else "")
1168
1203
 
1169
1204
  @staticmethod
@@ -516,6 +516,147 @@ CONTEXT_INTERP_RE = re.compile(
516
516
  _INTERP_EXECUTABLE + r"[\s\S]*" + CONTEXT_MD, re.I,
517
517
  )
518
518
 
519
+ # #997/#911: the three legs above match an interpreter token followed ANYWHERE
520
+ # by a protected name, so a read-only `python -c "print(open(...).read())"` or
521
+ # an issue body that merely cites the path blocked like a write. They are now a
522
+ # pre-filter: `_interp_protected_write` blocks only when the command shows
523
+ # write intent after the interpreter, or hands the interpreter a script or
524
+ # module argument (script argv stays conservative: its writes are invisible
525
+ # here). Write intent is lexical and deliberately broad; dynamic execution
526
+ # (`exec`, `eval`, `system`, `subprocess`) counts, because it hides the write.
527
+ # The guard stays defense in depth behind the Write/Edit tool guards; a path
528
+ # assembled at runtime (`'.codearbiter/' + 'CONTEXT.md'`) was never seen.
529
+ _PS_WRITE_ALIASES = (r"sc|ac|ni|ri|mi|cpi|rni|ren|clc|del|erase|rd|rmdir|rm|mv|cp"
530
+ r"|move|copy|tee")
531
+ INTERP_WRITE_INTENT_RE = re.compile(
532
+ # Calls whose name carries a write/destroy verb: write_text, writeFileSync,
533
+ # WriteAllText, fwrite, appendFile, os.remove, unlink, shutil.move/copy,
534
+ # rmtree, Path.touch/chmod, os.truncate, rename/replace, File.Delete.
535
+ r"[\w$]*(?:write|append|unlink|remove|rename|replace|truncate|rmtree|rmdir"
536
+ r"|delete|move|copy|symlink|touch|chmod|chown|mkfifo|put_contents|fputs)"
537
+ r"[\w$]*\s*\("
538
+ r"|(?:\b|\.)(?:rm|cp|link)(?:Sync)?\s*\("
539
+ r"|\w*Writer\b|\w*WriteStream\b|\bFileStream\b|\bFileUtils\b"
540
+ r"|::\s*(?:Create\w*|Open(?:Write)?)\s*\("
541
+ # open() with a second positional argument that is not a literal read
542
+ # mode (a write mode, a flag, or a variable), or a non-read mode= keyword.
543
+ r"|\b\w*open(?:Sync)?\s*\(\s*[^,()]+(?:\([^()]*\))?[^,()]*,"
544
+ r"(?!\s*['\"](?:[rbtU]*|<)['\"]\s*[,)]|\s*[A-Za-z_]+\s*=)"
545
+ r"|\bmode\s*=\s*(?!['\"][rbtU]*['\"])"
546
+ r"|\b(?:Set-Content|Add-Content|Out-File|Clear-Content|Remove-Item|Move-Item"
547
+ r"|Copy-Item|Rename-Item|New-Item|Tee-Object|Invoke-Expression|Start-Process"
548
+ r"|Invoke-Command)\b"
549
+ r"|(?:^|[;&|({\n\"'`])\s*(?:" + _PS_WRITE_ALIASES + r")\s"
550
+ r"|\b(?:exec\w*|eval|system|popen|spawn\w*|check_call|check_output|getattr"
551
+ r"|setattr|__import__|compile)\s*\("
552
+ r"|\bsubprocess\b|\bmethodcaller\b|\bDeno\.(?:run|Command)\b|\.Invoke\w*\s*\("
553
+ # Reflection hides the called name: dunders (`os.__dict__['remove']`),
554
+ # vars()/globals(), and a computed member call (`fs['write' + x](...)`).
555
+ r"|__\w+__|\b(?:vars|globals|locals)\s*\(|\]\s*\("
556
+ # A write-mode literal passed or assigned anywhere: `o = open; o(p, 'w')`.
557
+ r"|(?<=[,(=\s])['\"][rbtU]*[wax+][rbtUwax+]*['\"](?=\s*[,);\]])",
558
+ re.I,
559
+ )
560
+ _INTERP_ARG_RE = re.compile(r"\s+([^\s|;&]+)")
561
+ # Words that precede a command without ending a shell word list.
562
+ _COMMAND_PREFIXES = frozenset({"env", "sudo", "call", "exec", "nohup", "time",
563
+ "command", "xargs", "then", "do", "else", "&"})
564
+
565
+
566
+ def _interp_operand(cmd, end):
567
+ """Classify what an interpreter token ending at `end` is handed:
568
+ 'inline' (an inline-code switch or a heredoc), 'stdin' (`-`, code that
569
+ may arrive from a pipe), 'script' (a positional argument or `<` input
570
+ shaped like a path), 'word' (any other positional argument, such as a
571
+ `-m` module or a dotless script name) or 'bare' (nothing before the
572
+ command ends)."""
573
+ rest = cmd[end:]
574
+ suffix = re.match(r"[\w.-]*[\"'`]?", rest)
575
+ pos = suffix.end()
576
+ while True:
577
+ m = _INTERP_ARG_RE.match(rest, pos)
578
+ if not m:
579
+ return "bare"
580
+ token = m.group(1).rstrip("\"'`")
581
+ inline = re.match(_INTERP_INLINE_CODE, token, re.I)
582
+ if inline and inline.end() == len(token):
583
+ return "inline"
584
+ if token.startswith("<<"):
585
+ return "inline"
586
+ if token == "-":
587
+ return "stdin"
588
+ if not token.startswith("-"):
589
+ return "script" if re.search(r"[./\\]", token) else "word"
590
+ pos = m.end()
591
+
592
+
593
+ def _at_command_position(cmd, start):
594
+ """True when the interpreter token at `start` sits where a command starts,
595
+ rather than inside prose such as "each node in the graph"."""
596
+ before = re.sub(r"[^\s;&|()`\"'{}]*$", "", cmd[:start]).rstrip()
597
+ if not before or before[-1] in ";&|({\n`\"'" or before.endswith("$("):
598
+ return True
599
+ word = before.split()[-1]
600
+ return word.lower() in _COMMAND_PREFIXES or "=" in word
601
+
602
+
603
+ def _interp_protected_write(cmd, interp_re, name_re):
604
+ """True when an interpreter invocation can write the protected `name_re`
605
+ target: `interp_re` (token then name) matched, and after some interpreter
606
+ token that precedes the name either write intent appears, or the code it
607
+ runs is not visible on the command line (a script, module or file, piped
608
+ stdin, or a dotless argument in command position)."""
609
+ if not interp_re.search(cmd):
610
+ return False
611
+ names = [m.start() for m in name_re.finditer(cmd)]
612
+ for m in re.finditer(_INTERP_EXECUTABLE, cmd, re.I):
613
+ if not any(n >= m.end() for n in names):
614
+ continue
615
+ # Perl and Ruby calls need no parentheses (`open F, ">", p`,
616
+ # `File.write p, ""`), which the lexical intent scan cannot see, so
617
+ # they keep the conservative pre-#997 behavior.
618
+ if m.group(1).lower() in ("perl", "ruby"):
619
+ return True
620
+ if INTERP_WRITE_INTENT_RE.search(cmd, m.start()):
621
+ return True
622
+ operand = _interp_operand(cmd, m.end())
623
+ if operand == "script":
624
+ return True
625
+ if operand == "stdin" and "<<" not in cmd[m.end():]:
626
+ return True
627
+ if operand in ("word", "bare") and _at_command_position(cmd, m.start()):
628
+ return True
629
+ return False
630
+
631
+
632
+ LOG_NAME_RE = re.compile(LOG_NAMES, re.I)
633
+ DECISIONS_NAME_RE = re.compile(DECISIONS, re.I)
634
+ CONTEXT_NAME_RE = re.compile(CONTEXT_MD, re.I)
635
+
636
+ # #997: `sed` writes a file only in place (`-i`, `--in-place`) or through a
637
+ # `w`/`W` script command; `sed -n 1,20p .codearbiter/CONTEXT.md` is a read.
638
+ _SED_WRITES_RE = re.compile(
639
+ r"(?<![\w-])-[a-zA-Z]*i|--in-place|(?:^|[\s'\";{}/\d$,])[wW]\s+\S")
640
+
641
+
642
+ def _context_shell_write(cmd):
643
+ """CONTEXT_WRITE_RE, except a `sed` invocation that cannot write."""
644
+ text = cmd
645
+ while True:
646
+ m = CONTEXT_WRITE_RE.search(text)
647
+ if not m:
648
+ return False
649
+ if m.group(1).lower() != "sed":
650
+ return True
651
+ # GNU sed accepts options after operands (`sed s/a/b/ FILE -i`), so
652
+ # scan the whole command segment, not only the text before the path.
653
+ segment = re.match(r"[^|;&]*", text[m.end(1):]).group(0)
654
+ if _SED_WRITES_RE.search(segment):
655
+ return True
656
+ # Blank this read-only `sed` and rescan, so another verb later in the
657
+ # same segment is still seen.
658
+ text = text[:m.start(1)] + " " + text[m.end(1):]
659
+
519
660
  # H-22's shell flank: the protected-state registry (B1/#564) — Write/Edit are
520
661
  # guarded via classify_protected's "state" class (pre-write.py/pre-edit.py,
521
662
  # T-05a/T-06/T-07); this closes the shell flank the SAME way the four
@@ -1430,7 +1571,8 @@ def _check_h05_audit_log(cmd):
1430
1571
  if enrolled_name and (
1431
1572
  LOG_TRUNC_RE.search(cmd) or LOG_DESTROY_RE.search(cmd)
1432
1573
  or LOG_GIT_RESTORE_RE.search(cmd)
1433
- or LOG_INTERP_RE.search(cmd) and not _is_direct_taskwrite(cmd)):
1574
+ or _interp_protected_write(cmd, LOG_INTERP_RE, LOG_NAME_RE)
1575
+ and not _is_direct_taskwrite(cmd)):
1434
1576
  block("H-05", "The .codearbiter audit logs (overrides.log, triage.log, sprint-log.md, "
1435
1577
  "gate-events.log, decisions/decision-log.md) are append-only "
1436
1578
  "(ORCHESTRATOR §7). Truncating, overwriting, or deleting the audit "
@@ -1457,7 +1599,7 @@ def _check_h11_decisions(cmd):
1457
1599
  cmd = DECISION_LOG_SHELL_RE.sub(" ", cmd)
1458
1600
  cmd = ADR_LIFECYCLE_LOG_SHELL_RE.sub(" ", cmd)
1459
1601
  if (DECISIONS_REDIRECT_RE.search(cmd) or DECISIONS_WRITE_RE.search(cmd)
1460
- or DECISIONS_INTERP_RE.search(cmd)):
1602
+ or _interp_protected_write(cmd, DECISIONS_INTERP_RE, DECISIONS_NAME_RE)):
1461
1603
  block("H-11", "ADR files under .codearbiter/decisions/ are authored only via "
1462
1604
  "/adr and are immutable history (ORCHESTRATOR §6) — shell writes, "
1463
1605
  "edits, and deletions there are prohibited.")
@@ -1469,9 +1611,13 @@ def _check_h18_context_md(cmd):
1469
1611
  Write tool, so nothing legitimate is blocked. Reads pass.
1470
1612
 
1471
1613
  #574: CONTEXT_INTERP_RE closes the interpreter-one-liner flank the same
1472
- way LOG_INTERP_RE/DECISIONS_INTERP_RE do for H-05/H-11."""
1473
- if (CONTEXT_REDIRECT_RE.search(cmd) or CONTEXT_WRITE_RE.search(cmd)
1474
- or CONTEXT_INTERP_RE.search(cmd)):
1614
+ way LOG_INTERP_RE/DECISIONS_INTERP_RE do for H-05/H-11.
1615
+
1616
+ #997: reads pass on both flanks. `sed` without in-place or a `w` command
1617
+ is a read, and an interpreter needs write intent or a script argument
1618
+ (`_interp_protected_write`)."""
1619
+ if (CONTEXT_REDIRECT_RE.search(cmd) or _context_shell_write(cmd)
1620
+ or _interp_protected_write(cmd, CONTEXT_INTERP_RE, CONTEXT_NAME_RE)):
1475
1621
  block("H-18", ".codearbiter/CONTEXT.md is the activation switch every enforcement "
1476
1622
  "hook reads (#159) — shell rewrites, edits, or deletions that could flip "
1477
1623
  "`arbiter: enabled` off or corrupt its frontmatter are prohibited. Edit it "
@@ -11,7 +11,7 @@ import hostapi # noqa: E402
11
11
  class PiHost(hostapi.Host):
12
12
  name = "pi"
13
13
  adapter_name = "@arbiterforge/ca-pi"
14
- adapter_version = "0.21.3"
14
+ adapter_version = "0.21.5"
15
15
  update_target = "ca-pi"
16
16
  update_tag_prefix = "ca-pi-v"
17
17
  update_command = "pi update npm:@arbiterforge/ca-pi"
@@ -313,7 +313,7 @@ class Host:
313
313
 
314
314
  name = "claude"
315
315
  adapter_name = "ca"
316
- adapter_version = "3.2.3"
316
+ adapter_version = "3.2.5"
317
317
 
318
318
  # Update-notifier descriptor. Each independently versioned host overrides
319
319
  # these three values in its per-plugin _host.py. Keeping the target,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arbiterforge/ca-pi",
3
- "version": "0.21.3",
3
+ "version": "0.21.5",
4
4
  "private": true,
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",