@arbiterforge/ca-pi 0.14.12 → 0.14.16

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.
@@ -244,6 +244,145 @@ def _resolve_workflow_pair(root: str | Path, slug: str) -> dict[str, object]:
244
244
  }
245
245
 
246
246
 
247
+ def _installed_workflow_preflight(installation: Path) -> dict[str, object]:
248
+ """Inspect installed prerequisites, not host trust or live-event evidence.
249
+
250
+ This never imports an adapter, executes a hook, or writes consumer state.
251
+ Package integrity and the actual authority boundaries remain separate checks.
252
+ A successful engine capabilities call alone cannot admit a host workflow.
253
+ """
254
+ result: dict[str, object] = {
255
+ "host": None, "adapter_version": None, "resources_available": False,
256
+ "evidence_kind": "installed-resource-preflight", "live_host_verified": False,
257
+ "missing": [],
258
+ }
259
+ missing: list[str] = []
260
+ if installation.name != "artifacts" or installation.parent.name != "helpers":
261
+ result["missing"] = ["installed host package layout"]
262
+ return result
263
+ plugin = installation.parent.parent
264
+
265
+ def resource(relative: str, *, document: bool = False):
266
+ path = plugin / relative
267
+ try:
268
+ _trusted_directory(installation, "HOST_WORKFLOW_UNAVAILABLE")
269
+ _trusted_directory(path.parent, "HOST_WORKFLOW_UNAVAILABLE")
270
+ descriptor = _open_pinned_regular(path)
271
+ try:
272
+ info = os.fstat(descriptor)
273
+ reparse = getattr(info, "st_file_attributes", 0) & getattr(
274
+ stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0)
275
+ if reparse or not stat.S_ISREG(info.st_mode) or info.st_size <= 0:
276
+ raise OSError("required installed resource is not a nonempty regular file")
277
+ return _decode(os.read(descriptor, MAX_RESPONSE + 1)) if document else True
278
+ finally:
279
+ os.close(descriptor)
280
+ except (OSError, ArtifactError):
281
+ missing.append(relative)
282
+ return None
283
+
284
+ manifests = [("claude", "ca", ".claude-plugin/plugin.json"),
285
+ ("codex", "ca-codex", ".codex-plugin/plugin.json")]
286
+ present = [row for row in manifests if (plugin / row[2]).exists()
287
+ or (plugin / row[2]).is_symlink()]
288
+ if not present:
289
+ present = [("pi", "ca-pi", "package.json")]
290
+ if len(present) != 1:
291
+ result["missing"] = ["one unambiguous installed host manifest"]
292
+ return result
293
+ host, name, manifest_path = present[0]
294
+ manifest = resource(manifest_path, document=True)
295
+ if (not isinstance(manifest, dict) or manifest.get("name") != name
296
+ or not isinstance(manifest.get("version"), str)
297
+ or re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?",
298
+ manifest["version"]) is None):
299
+ result["missing"] = ["matching installed host identity"]
300
+ return result
301
+ result.update(host=host, adapter_version=manifest["version"])
302
+ if host == "pi":
303
+ result["missing"] = ["Pi production prompt, verification and review authority"]
304
+ return result
305
+
306
+ for filename in ("_approvallib.py", "_prerequisitelib.py", "_sprintapprovallib.py",
307
+ "_reconciliationlib.py", "_artifactauthoritylib.py", "_gitexec.py",
308
+ "artifact-authority.py", "artifact-authority-hook.py", "prompt-submit.py"):
309
+ resource("hooks/" + filename)
310
+ if host == "claude":
311
+ resource("agents/authority-reviewer.md")
312
+ registry = resource("hooks/hooks.json", document=True)
313
+ hooks = registry.get("hooks") if isinstance(registry, dict) else None
314
+ if not isinstance(hooks, dict):
315
+ result["missing"] = sorted(set(missing + ["valid installed hook registry"]))
316
+ return result
317
+
318
+ token = "${CLAUDE_PLUGIN_ROOT}" if host == "claude" else "${PLUGIN_ROOT}"
319
+ command_field = "commandWindows" if host == "codex" and platform.system() == "Windows" else "command"
320
+
321
+ def registered(event: str, tool: str | None, script: str) -> bool:
322
+ groups = hooks.get(event)
323
+ if not isinstance(groups, list):
324
+ return False
325
+ commands = {f'{python} "{token}/hooks/{script}"' for python in ("python", "python3")}
326
+ for group in groups:
327
+ if not isinstance(group, dict):
328
+ continue
329
+ matcher = group.get("matcher", "")
330
+ if not isinstance(matcher, str):
331
+ continue
332
+ # Check the shipped literal alternatives, not a second regex DSL.
333
+ if (tool is None and matcher not in {"", "*"}) or (
334
+ tool is not None and tool not in matcher.split("|")):
335
+ continue
336
+ entries = group.get("hooks")
337
+ if not isinstance(entries, list):
338
+ continue
339
+ if any(isinstance(entry, dict) and entry.get("type") == "command"
340
+ and entry.get("async", False) is False
341
+ and isinstance(entry.get(command_field), str)
342
+ and entry[command_field] in commands for entry in entries):
343
+ return True
344
+ return False
345
+
346
+ required = [("UserPromptSubmit", None, "prompt-submit.py")]
347
+ tools = ("Bash", "Agent") if host == "claude" else (
348
+ "Bash", "shell_command", "exec_command", "unified_exec", "spawn_agent")
349
+ required += [(event, tool, "artifact-authority-hook.py")
350
+ for event in ("PreToolUse", "PostToolUse") for tool in tools]
351
+ required += [(event, None, "artifact-authority-hook.py")
352
+ for event in ("SubagentStart", "SubagentStop")]
353
+ if host == "claude":
354
+ required += [("PostToolUseFailure", "Bash", "artifact-authority-hook.py"),
355
+ ("PreToolUse", "SendMessage", "artifact-authority-hook.py")]
356
+ missing.extend(f"{event}:{tool or '*'}:{script}"
357
+ for event, tool, script in required if not registered(event, tool, script))
358
+ result["missing"] = sorted(set(missing))
359
+ result["resources_available"] = not missing
360
+ return result
361
+
362
+
363
+ def _require_host_workflow(client: "ArtifactClient") -> None:
364
+ readiness = client.workflow_preflight()
365
+ if readiness["resources_available"] is not True:
366
+ raise ArtifactError(
367
+ "HOST_WORKFLOW_UNAVAILABLE",
368
+ ("installed HTML workflow prerequisites are unavailable: "
369
+ + ", ".join(readiness["missing"]))[:400]
370
+ + "; repair this package or use a supported host; no fallback artifacts",
371
+ )
372
+
373
+
374
+ # The native engine ships only through the release distribution channels, never
375
+ # through a main-branch checkout, so the repair is always "install from the
376
+ # channel". Named concretely so the user is not left to guess (ADR-0040).
377
+ PAYLOAD_REPAIR_HINT = (
378
+ "Claude Code: /plugin marketplace update codearbiter, then /plugin update ca@codearbiter; "
379
+ "Codex (registered without --ref): codex plugin remove ca-codex@codearbiter, "
380
+ "codex plugin marketplace remove codearbiter, "
381
+ "codex plugin marketplace add arbiterForge/codeArbiter --ref ca-codex-marketplace, "
382
+ "then codex plugin add ca-codex@codearbiter; Pi: pi install npm:@arbiterforge/ca-pi"
383
+ )
384
+
385
+
247
386
  def _require_authoring_capability(client: "ArtifactClient") -> None:
248
387
  """Fail one default-HTML route with a bounded repair diagnostic."""
249
388
  try:
@@ -252,7 +391,7 @@ def _require_authoring_capability(client: "ArtifactClient") -> None:
252
391
  raise ArtifactError(
253
392
  "CAPABILITY_MISSING",
254
393
  f"repair or reinstall the pinned artifact payload ({exc.code}); "
255
- "new HTML work cannot fall back to Markdown",
394
+ f"new HTML work cannot fall back to Markdown. Repair: {PAYLOAD_REPAIR_HINT}",
256
395
  ) from exc
257
396
  if (
258
397
  not isinstance(capabilities, dict)
@@ -262,7 +401,7 @@ def _require_authoring_capability(client: "ArtifactClient") -> None:
262
401
  raise ArtifactError(
263
402
  "CAPABILITY_MISSING",
264
403
  "repair or reinstall the qualified default-on artifact payload; "
265
- "new HTML work cannot fall back to Markdown",
404
+ f"new HTML work cannot fall back to Markdown. Repair: {PAYLOAD_REPAIR_HINT}",
266
405
  )
267
406
 
268
407
 
@@ -305,10 +444,13 @@ def _select_authoring_route(
305
444
  "an installation-pinned artifact client bound to this repository is required",
306
445
  )
307
446
  _require_authoring_capability(client)
447
+ if _resolve_workflow_pair(root, slug) != selected:
448
+ raise ArtifactError("STALE_ROUTE", "artifact namespace changed during the capability probe")
449
+ _require_host_workflow(client)
308
450
  if _resolve_workflow_pair(root, slug) != selected:
309
451
  raise ArtifactError(
310
452
  "STALE_ROUTE",
311
- "artifact namespace changed during the capability probe",
453
+ "artifact namespace changed during the installed host preflight",
312
454
  )
313
455
  return {"workflow": workflow, "lane": lane, "mode": "artifact", **selected}
314
456
 
@@ -371,6 +513,7 @@ def _preflight_plan_authoring(
371
513
  ):
372
514
  raise ArtifactError("AUTHORITY_MISMATCH", "spec normative digest is invalid")
373
515
 
516
+ _require_host_workflow(client)
374
517
  identity = client.call("identity", {"artifact_id": spec_artifact_id})
375
518
  expected_path = f".codearbiter/specs/{slug}.html"
376
519
  if (
@@ -654,6 +797,10 @@ class ArtifactClient:
654
797
  self.installation = _trusted_directory(installation, "CAPABILITY_MISSING")
655
798
  self.timeout = timeout
656
799
 
800
+ def workflow_preflight(self) -> dict[str, object]:
801
+ """Read fresh installed prerequisites; never attest live host authority."""
802
+ return _installed_workflow_preflight(self.installation)
803
+
657
804
  def _open_binary(self) -> int:
658
805
  system = {"Linux":"linux", "Darwin":"darwin", "Windows":"windows"}.get(platform.system())
659
806
  arch = {"x86_64":"amd64", "amd64":"amd64", "aarch64":"arm64", "arm64":"arm64"}.get(platform.machine().lower())
@@ -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.14.12"
14
+ adapter_version = "0.14.16"
15
15
  update_target = "ca-pi"
16
16
  update_tag_prefix = "ca-pi-v"
17
17
  update_command = "pi update npm:@arbiterforge/ca-pi"