@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
@@ -46,7 +46,7 @@
46
46
  # own checkout — the root gate MARKERS
47
47
  # (.codearbiter/.markers/) live under (#604)
48
48
  # repo_rel(fpath, root) -> str repo-relative POSIX path, or "" if outside root
49
- # line_digest(line) -> str sha256 hex of one diff line (H-09b/H-10b gate)
49
+ # line_digest(line, path=None) -> str path-bound sha256 of one diff line (H-09b/H-10b gate)
50
50
  # content_digest(text) -> str sha256 hex of a whole file's content (H-14 gate)
51
51
  # migration_globs(root) -> tuple[list, list] (includes, excludes) for migration detection
52
52
  # scope_globs(root, defaults, decl_re) -> tuple[list, list] generic glob scope resolver
@@ -144,6 +144,7 @@ from _activationlib import ( # noqa: F401
144
144
  arbiter_active,
145
145
  frontmatter_enabled,
146
146
  frontmatter_enabled_text,
147
+ initialized_body_text,
147
148
  get_host,
148
149
  marker_root,
149
150
  project_root,
@@ -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.15.10"
14
+ adapter_version = "0.16.0"
15
15
  update_target = "ca-pi"
16
16
  update_tag_prefix = "ca-pi-v"
17
17
  update_command = "pi update npm:@arbiterforge/ca-pi"
@@ -239,18 +239,18 @@ def main(argv):
239
239
  from _artifactlib import resolve_spec_file, helper_installation
240
240
  client, entry = resolve_spec_file(positional[0], helper_installation(__file__))
241
241
  result = client.call("validate", {"artifact_id": entry["artifact_id"], "gate": "ready"}, permit_invalid=True)
242
- # Native success uses null for an empty diagnostic slice. Normalize
243
- # only that documented absence, never arbitrary falsey malformed data.
244
242
  if (result.get("artifact_id") != entry["artifact_id"]
245
243
  or result.get("gate") != "ready"
246
244
  or type(result.get("valid")) is not bool
247
- or not re.fullmatch(r"[0-9a-f]{64}", result.get("model_sha256", ""))):
245
+ or not isinstance(result.get("model_sha256"), str)
246
+ or not re.fullmatch(r"[0-9a-f]{64}", result["model_sha256"])
247
+ or "diagnostics" not in result):
248
248
  raise RuntimeError("malformed HTML validation result")
249
249
  diagnostics = result.get("diagnostics")
250
250
  if diagnostics is None:
251
251
  diagnostics = []
252
252
  if not isinstance(diagnostics, list):
253
- raise RuntimeError("HTML validation diagnostics must be a list or null")
253
+ raise ValueError("HTML validation diagnostics must be a list or null")
254
254
  findings = []
255
255
  for item in diagnostics:
256
256
  if (not isinstance(item, dict)
@@ -272,7 +272,7 @@ def main(argv):
272
272
  findings.append("[UNCOVERED-CHECKBOX] " + box)
273
273
  fresh = client.call("identity", {"artifact_id": entry["artifact_id"]})
274
274
  if (fresh.get("artifact_id") != entry["artifact_id"]
275
- or fresh["model_sha256"] != result["model_sha256"]):
275
+ or fresh.get("model_sha256") != result["model_sha256"]):
276
276
  raise RuntimeError("HTML spec changed during intent verification; retry a fresh read")
277
277
  for finding in findings:
278
278
  print(finding)
@@ -218,7 +218,7 @@ def is_marker_path(rel):
218
218
  return bool(MARKERS_RE.search(norm_path(rel)))
219
219
 
220
220
 
221
- def is_registered_state_path(rel):
221
+ def is_registered_state_path(rel, root=None):
222
222
  """True iff `rel` carries a policy in the protected-state registry
223
223
  (`_protectedstatelib.lookup_policy`, B1/#564) — the fifth protected class
224
224
  `classify_protected` recognizes (T-05a), evaluated strictly AFTER the
@@ -253,7 +253,7 @@ def is_registered_state_path(rel):
253
253
  It is a local import, not file I/O or a git call, so it does not violate
254
254
  this module's zero-side-effects-at-import invariant."""
255
255
  import _protectedstatelib
256
- return _protectedstatelib.lookup_policy(rel) is not None
256
+ return _protectedstatelib.lookup_policy(rel, root=root) is not None
257
257
 
258
258
 
259
259
  def classify_protected(fpath, root):
@@ -315,7 +315,7 @@ def classify_protected(fpath, root):
315
315
  for p in (raw_repo_rel(fpath, root), repo_rel(fpath, root)):
316
316
  if not p:
317
317
  continue
318
- if is_registered_state_path(p):
318
+ if is_registered_state_path(p, root):
319
319
  hits.add("state")
320
320
  break
321
321
  return hits
@@ -120,6 +120,9 @@
120
120
  from __future__ import annotations
121
121
 
122
122
  import os
123
+ import posixpath
124
+ import re
125
+ import stat
123
126
  from enum import Enum
124
127
 
125
128
  from _hooklib import MARKER_FRESHNESS_MINUTES, marker_fresh
@@ -211,6 +214,150 @@ REGISTRY: dict[str, ProtectedPolicy] = {
211
214
  }
212
215
 
213
216
 
217
+ def context_writer_protection() -> dict[str, ProtectedPolicy]:
218
+ """Return context candidates only after a fresh shipped-writer check.
219
+
220
+ Called only while resolving a context target, never at module import.
221
+ """
222
+ import _artifactlib
223
+
224
+ if not _artifactlib.context_writer_qualified():
225
+ return {}
226
+ return {path: ProtectedPolicy.HELPER_ONLY
227
+ for path in _artifactlib._CONTEXT_TARGETS.values()}
228
+
229
+
230
+ def _committed_context_journal(root, document_id, target):
231
+ """Find the native writer's durable two-target commit, with bounded reads.
232
+
233
+ None means the journal inventory is uncertain: protect context paths until
234
+ it is reconciled. An empty or absent directory is not enrollment.
235
+ """
236
+ import _artifactlib
237
+
238
+ directory = os.path.join(root, ".codearbiter", ".artifacts", "transactions")
239
+ for parent in (os.path.join(root, ".codearbiter"),
240
+ os.path.join(root, ".codearbiter", ".artifacts"), directory):
241
+ if os.path.islink(parent):
242
+ return None
243
+ if not os.path.lexists(directory):
244
+ return False
245
+ if not os.path.isdir(directory):
246
+ return None
247
+ try:
248
+ entries = []
249
+ with os.scandir(directory) as scan:
250
+ for entry in scan:
251
+ entries.append(entry)
252
+ if len(entries) > 1024:
253
+ return None
254
+ total_bytes = 0
255
+ for entry in entries:
256
+ if entry.name.endswith(".next"):
257
+ continue
258
+ if (not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]{7,79}\.json", entry.name)
259
+ or not entry.is_file(follow_symlinks=False)):
260
+ return None
261
+ descriptor = _artifactlib._open_pinned_regular(entry.path)
262
+ try:
263
+ info = os.fstat(descriptor)
264
+ total_bytes += info.st_size
265
+ if (info.st_size > 65536 or info.st_size <= 0 or total_bytes > 8 << 20
266
+ or not stat.S_ISREG(info.st_mode)
267
+ or getattr(info, "st_file_attributes", 0) &
268
+ getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0)):
269
+ return None
270
+ raw = os.read(descriptor, 65537)
271
+ finally:
272
+ os.close(descriptor)
273
+ journal = _artifactlib._decode(raw)
274
+ if not isinstance(journal, dict):
275
+ return None
276
+ historical = journal.get("format") == "codearbiter.transaction/0.1.0"
277
+ if (journal.get("format") not in {"codearbiter.transaction/0.1.0",
278
+ "codearbiter.transaction/0.2.0"}
279
+ or set(journal) != ({"format", "operation_id", "request_sha256",
280
+ "state", "entries"} if historical else
281
+ {"format", "operation_id", "request_sha256",
282
+ "state", "entries", "result"})
283
+ or journal.get("operation_id") != entry.name[:-5]
284
+ or not isinstance(journal.get("request_sha256"), str)
285
+ or re.fullmatch(r"[0-9a-f]{64}", journal["request_sha256"]) is None
286
+ or journal.get("state") not in {"prepared", "committed", "rolled_back"}):
287
+ return None
288
+ rows = journal.get("entries")
289
+ if (not isinstance(rows, list) or not 1 <= len(rows) <= 128
290
+ or any(not isinstance(row, dict)
291
+ or set(row) != {"path", "stage", "backup", "before_sha256",
292
+ "after_sha256", "size"}
293
+ or not isinstance(row["path"], str)
294
+ or not row["path"] or row["path"].startswith("/")
295
+ or posixpath.normpath(row["path"]) != row["path"]
296
+ or any(part in {"", ".", ".."} or part.endswith((".", " "))
297
+ or re.fullmatch(
298
+ r"(?i)(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\..*)?",
299
+ part) is not None
300
+ for part in row["path"].split("/"))
301
+ or any(char in row["path"] for char in '\\:*?<>|"')
302
+ or any(ord(char) < 32 or ord(char) == 127 for char in row["path"])
303
+ or not isinstance(row["after_sha256"], str)
304
+ or re.fullmatch(r"[0-9a-f]{64}", row["after_sha256"]) is None
305
+ or (row["before_sha256"] is not None and
306
+ (not isinstance(row["before_sha256"], str) or
307
+ re.fullmatch(r"[0-9a-f]{64}", row["before_sha256"]) is None))
308
+ or type(row["size"]) is not int or not 0 <= row["size"] <= 8 << 20
309
+ or row["stage"] !=
310
+ f"{os.path.dirname(row['path'])}/.ca-artifact-{journal['operation_id']}-{i}.new"
311
+ or row["backup"] !=
312
+ f".codearbiter/.artifacts/history/{journal['operation_id']}-{i}.before"
313
+ for i, row in enumerate(rows))):
314
+ return None
315
+ paths = [row["path"] for row in rows]
316
+ if historical:
317
+ if target in paths:
318
+ return None
319
+ continue
320
+ result = journal.get("result")
321
+ if not isinstance(result, dict):
322
+ if target in paths:
323
+ return None
324
+ continue
325
+ if result.get("document_id") != document_id or result.get("target_path") != target:
326
+ if target in paths:
327
+ return None
328
+ continue
329
+ if (set(result) != {"document_id", "target_path", "document_sha256",
330
+ "provenance_sha256", "mode"}
331
+ or result["mode"] not in {"create", "adopt", "update"}
332
+ or len(rows) != 2
333
+ or paths !=
334
+ [target, f".codearbiter/.provenance/{document_id}.json"]
335
+ or rows[0].get("after_sha256") != result["document_sha256"]
336
+ or rows[1].get("after_sha256") != result["provenance_sha256"]):
337
+ return None
338
+ if journal.get("state") == "committed":
339
+ return True
340
+ if journal.get("state") == "prepared":
341
+ return None
342
+ return False
343
+ except (OSError, ValueError, _artifactlib.ArtifactError):
344
+ return None
345
+
346
+
347
+ def _context_policy(rel_path, root):
348
+ import _artifactlib
349
+
350
+ document_id = next((doc for doc, target in _artifactlib._CONTEXT_TARGETS.items()
351
+ if _canon(target) == _canon(rel_path)), None)
352
+ if document_id is None:
353
+ return None
354
+ target = _artifactlib._CONTEXT_TARGETS[document_id]
355
+ enrolled = _committed_context_journal(root, document_id, target)
356
+ if enrolled is not False:
357
+ return ProtectedPolicy.HELPER_ONLY
358
+ return context_writer_protection().get(target)
359
+
360
+
214
361
  def _canon(rel_path):
215
362
  """Canonical comparison form of a repo-relative path: separator-
216
363
  normalized (`norm_path`), then whitespace-stripped, `./`-prefix-
@@ -263,7 +410,7 @@ def _canon(rel_path):
263
410
  return p.lower()
264
411
 
265
412
 
266
- def lookup_policy(rel_path, registry=None):
413
+ def lookup_policy(rel_path, registry=None, *, root=None):
267
414
  """The ProtectedPolicy registered for `rel_path`, or None if it carries
268
415
  no policy. Both `rel_path` and every registry key are canonicalized
269
416
  (`_canon`, above) before comparison — separator-normalized, `./`/
@@ -284,12 +431,15 @@ def lookup_policy(rel_path, registry=None):
284
431
  and realpath-resolved forms of a path - a flank that calls
285
432
  `lookup_policy` directly with its own ad hoc dict re-opens the symlink
286
433
  alias this module does not itself guard against."""
434
+ dynamic = registry is None and root is not None
287
435
  if registry is None:
288
436
  registry = REGISTRY
289
437
  normalized = _canon(rel_path)
290
438
  for key, policy in registry.items():
291
439
  if _canon(key) == normalized:
292
440
  return policy
441
+ if dynamic:
442
+ return _context_policy(rel_path, root)
293
443
  return None
294
444
 
295
445
 
@@ -341,7 +491,7 @@ def resolve_registered_path(fpath, root, registry=None):
341
491
  for p in (raw_repo_rel(fpath, root), repo_rel(fpath, root)):
342
492
  if not p:
343
493
  continue
344
- policy = lookup_policy(p, registry)
494
+ policy = lookup_policy(p, registry, root=root)
345
495
  if policy is not None:
346
496
  return p, policy
347
497
  return None, None