@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
@@ -12,11 +12,16 @@
12
12
  # Usage:
13
13
  # python init-codearbiter.py [--root PATH] [--stage N]
14
14
  # python init-codearbiter.py --check # report state, create nothing
15
+ # python init-codearbiter.py --check --passive --root PATH # external pre-session read
15
16
  # python init-codearbiter.py --repair-lock-exclusion [--root PATH]
17
+ # python init-codearbiter.py --root PATH --context-request FILE # bounded writer bridge, including initialized refresh
16
18
 
17
19
  import argparse
20
+ import json
18
21
  import subprocess
19
22
  import os
23
+ import re
24
+ import stat
20
25
  import sys
21
26
 
22
27
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
@@ -25,6 +30,8 @@ from _taskboardlib import DONE_TASKS # noqa: E402 — shared archive template (
25
30
  import hostapi # noqa: E402 — host seam (ADR-0011)
26
31
  import _hooklib # noqa: E402 — set_host DI seam (#257)
27
32
  import _entrylib # noqa: E402 — shared run() dispatch (jscpd dedup)
33
+ from _activationlib import passive_activation_inventory # noqa: E402
34
+ import _artifactlib # noqa: E402
28
35
 
29
36
  # NOTE: this stub deliberately does NOT contain the initialization sentinel
30
37
  # (an HTML comment wrapping the word INITIALIZED). The SessionStart hook greps
@@ -122,17 +129,132 @@ def project_root(opt):
122
129
  return os.path.abspath(os.getcwd())
123
130
 
124
131
 
132
+ def _regular_scaffold_entry(path, *, directory):
133
+ """Reject aliases and reparse points before reading or creating scaffold state."""
134
+ try:
135
+ info = os.lstat(path)
136
+ except OSError:
137
+ return False
138
+ reparse = getattr(info, "st_file_attributes", 0) & getattr(
139
+ stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0)
140
+ return not reparse and (stat.S_ISDIR(info.st_mode) if directory
141
+ else stat.S_ISREG(info.st_mode))
142
+
143
+
144
+ def _scaffold_inventory(cad, ctx, requested_stage):
145
+ """Return absent scaffold names only when surviving state is safe to retain."""
146
+ names = ("CONTEXT.md", *FILES)
147
+ if os.path.lexists(cad) and not _regular_scaffold_entry(cad, directory=True):
148
+ raise SystemExit(f"REFUSING: {cad} is not a regular project-state directory.")
149
+ present = {name for name in names if os.path.lexists(os.path.join(cad, name))}
150
+ if not present:
151
+ if os.path.isdir(cad) and os.listdir(cad):
152
+ raise SystemExit(f"REFUSING: {cad} has existing state without a scaffold; inspect it first.")
153
+ return list(names), False
154
+ for name in present:
155
+ path = os.path.join(cad, name)
156
+ if not _regular_scaffold_entry(path, directory=False):
157
+ raise SystemExit(f"REFUSING: {path} is not a regular scaffold file.")
158
+ if "CONTEXT.md" not in present:
159
+ # A missing stub amid content/provenance may be an interrupted native
160
+ # transaction. Only the known scaffold prefix can be resumed here.
161
+ unexpected = set(os.listdir(cad)) - set(FILES)
162
+ if unexpected:
163
+ raise SystemExit(f"REFUSING: {ctx} is absent amid other project state; inspect prior write outcome.")
164
+ else:
165
+ try:
166
+ with open(ctx, encoding="utf-8") as handle:
167
+ body = handle.read()
168
+ except (OSError, UnicodeError) as exc:
169
+ raise SystemExit(f"REFUSING: cannot inspect {ctx}: {exc}") from None
170
+ enabled, malformed = _hooklib.frontmatter_enabled_text(body)
171
+ lines = body.splitlines()
172
+ boundary = next((index for index, line in enumerate(lines[1:], 1)
173
+ if line.strip() == "---"), None)
174
+ stages = ([match.group(1) for line in lines[1:boundary]
175
+ if (match := re.fullmatch(r"\s*stage:\s*([1-9][0-9]*)\s*", line))]
176
+ if boundary is not None else [])
177
+ if (not enabled or malformed or len(stages) != 1 or
178
+ _hooklib.initialized_body_text(body)):
179
+ raise SystemExit(f"REFUSING: {ctx} is invalid or initialized; inspect before recovery.")
180
+ if requested_stage is not None and int(stages[0]) != requested_stage:
181
+ raise SystemExit(f"REFUSING: {ctx} stage differs from --stage; preserve existing context.")
182
+ headers = {"open-tasks.md": "# Open tasks", "done-tasks.md": "# Done tasks",
183
+ "open-questions.md": "# Open questions",
184
+ "overrides.log": "# codeArbiter override log"}
185
+ for name in present & headers.keys():
186
+ path = os.path.join(cad, name)
187
+ try:
188
+ with open(path, encoding="utf-8") as handle:
189
+ valid = handle.readline().rstrip("\r\n").startswith(headers[name])
190
+ except (OSError, UnicodeError):
191
+ valid = False
192
+ if not valid:
193
+ raise SystemExit(f"REFUSING: {path} has an unrecognized header; preserve it for review.")
194
+ if "last-checkpoint" in present:
195
+ try:
196
+ with open(os.path.join(cad, "last-checkpoint"), encoding="utf-8") as handle:
197
+ valid = bool(re.fullmatch(r"[0-9]+\s*", handle.read()))
198
+ except (OSError, UnicodeError):
199
+ valid = False
200
+ if not valid:
201
+ raise SystemExit("REFUSING: last-checkpoint is malformed; preserve it for review.")
202
+ return [name for name in names if name not in present], True
203
+
204
+
205
+ def _create_scaffold_file(path, content, *, newline=None):
206
+ """Create once; a concurrent new file is a conflict, never an overwrite."""
207
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
208
+ flags |= getattr(os, "O_NOFOLLOW", 0)
209
+ try:
210
+ fd = os.open(path, flags, 0o666)
211
+ except FileExistsError:
212
+ raise SystemExit(f"REFUSING: {path} appeared during scaffold recovery.") from None
213
+ with os.fdopen(fd, "w", encoding="utf-8", newline=newline) as handle:
214
+ handle.write(content)
215
+
216
+
125
217
  def main(argv=None):
126
218
  ap = argparse.ArgumentParser(add_help=True)
127
219
  ap.add_argument("--root")
128
220
  ap.add_argument("--stage", type=int)
129
221
  ap.add_argument("--check", action="store_true")
222
+ ap.add_argument("--passive", action="store_true", help=(
223
+ "inspect explicit root before host activation; no Git or network probes"))
224
+ ap.add_argument("--context-request", help=argparse.SUPPRESS)
130
225
  ap.add_argument("--repair-lock-exclusion", action="store_true", help=(
131
226
  "repair only Git-local info/exclude for an existing scaffold; retain the "
132
227
  "task-board OS lock, never rewrite project state (cannot combine with --check/--stage)"))
133
228
  args = ap.parse_args(argv)
229
+ if args.context_request:
230
+ if not args.root or args.stage is not None or args.check or args.passive or args.repair_lock_exclusion:
231
+ ap.error("--context-request requires --root and excludes scaffold flags")
232
+ try:
233
+ root = _artifactlib._trusted_directory(args.root, "UNSAFE_ROOT")
234
+ request = _artifactlib.read_context_preview_request(root, args.context_request)
235
+ operation, payload = _artifactlib.validate_context_writer_request(request)
236
+ client = _artifactlib.context_writer_client(root)
237
+ if operation == "preview":
238
+ from _artifactauthoritylib import arm_context_preview
239
+ result = arm_context_preview(root, client, payload)
240
+ else:
241
+ result = client.call("context-apply", payload)
242
+ except RuntimeError as exc:
243
+ raise SystemExit(f"REFUSING context request: {exc}") from None
244
+ print(json.dumps(result, sort_keys=True))
245
+ return
246
+ if args.passive:
247
+ if not args.check or not args.root or args.stage is not None or args.repair_lock_exclusion:
248
+ ap.error("--passive requires --check and --root; excludes --stage and repair")
249
+ try:
250
+ report = passive_activation_inventory(args.root)
251
+ except (OSError, UnicodeError, ValueError) as exc:
252
+ raise SystemExit(f"REFUSING passive inspection: {exc}") from None
253
+ print(json.dumps(report, sort_keys=True))
254
+ return
134
255
  if args.repair_lock_exclusion and (args.check or args.stage is not None):
135
256
  ap.error("--repair-lock-exclusion cannot combine with --check or --stage")
257
+ requested_stage = args.stage
136
258
  if args.stage is None:
137
259
  args.stage = 1
138
260
 
@@ -149,13 +271,17 @@ def main(argv=None):
149
271
  name = os.path.basename(root.rstrip("/\\")) or "project"
150
272
 
151
273
  if args.check:
274
+ if os.path.lexists(cad) and not _regular_scaffold_entry(cad, directory=True):
275
+ raise SystemExit(f"REFUSING: {cad} is not a regular project-state directory.")
276
+ if os.path.lexists(ctx) and not _regular_scaffold_entry(ctx, directory=False):
277
+ raise SystemExit(f"REFUSING: {ctx} is not a regular scaffold file.")
152
278
  if os.path.exists(ctx):
153
279
  print(f"ALREADY SCAFFOLDED: {ctx} exists.")
154
280
  with open(ctx, encoding="utf-8", errors="replace") as f:
155
281
  text = f.read()
156
- import re
157
- initd = bool(re.search(r"(?m)^\s*<!--\s*INITIALIZED\s*-->\s*$", text))
158
- print("arbiter: " + ("enabled" if re.search(r"(?m)^arbiter:\s*enabled\s*$", text) else "not enabled"))
282
+ initd = _hooklib.initialized_body_text(text)
283
+ enabled, _malformed = _hooklib.frontmatter_enabled_text(text)
284
+ print("arbiter: " + ("enabled" if enabled else "not enabled"))
159
285
  print("initialized body: " + ("yes" if initd else
160
286
  "no (run " + _cc + " or " + _dc + ")"))
161
287
  else:
@@ -164,7 +290,8 @@ def main(argv=None):
164
290
 
165
291
  from _taskexcludelib import ensure_task_lock_excluded, TaskExclusionError
166
292
  if args.repair_lock_exclusion:
167
- if not os.path.isfile(ctx) or os.path.islink(ctx):
293
+ if (not _regular_scaffold_entry(cad, directory=True) or
294
+ not _regular_scaffold_entry(ctx, directory=False)):
168
295
  raise SystemExit("REFUSING: task lock exclusion repair requires an existing scaffold.")
169
296
  try:
170
297
  result = ensure_task_lock_excluded(root, required=True)
@@ -173,7 +300,8 @@ def main(argv=None):
173
300
  print(f"task lock exclusion: {result}")
174
301
  return
175
302
 
176
- if os.path.exists(ctx):
303
+ absent, recovering = _scaffold_inventory(cad, ctx, requested_stage)
304
+ if recovering and not absent:
177
305
  raise SystemExit(
178
306
  f"REFUSING: {ctx} already exists. .codearbiter/ is already scaffolded here. "
179
307
  f"To populate it, run {_cc} or {_dc}; to repair, edit by hand.")
@@ -187,19 +315,16 @@ def main(argv=None):
187
315
 
188
316
  os.makedirs(cad, exist_ok=True)
189
317
  created = []
190
- if not os.path.exists(ctx):
191
- with open(ctx, "w", encoding="utf-8") as f:
192
- f.write(CONTEXT.format(stage=args.stage, name=name,
193
- create_context=_cc, decompose=_dc))
318
+ if "CONTEXT.md" in absent:
319
+ _create_scaffold_file(ctx, CONTEXT.format(stage=args.stage, name=name,
320
+ create_context=_cc, decompose=_dc))
194
321
  created.append("CONTEXT.md")
195
322
  for fname, content in FILES.items():
196
- fp = os.path.join(cad, fname)
197
- if not os.path.exists(fp):
323
+ if fname in absent:
198
324
  # Match taskwrite's LF output for this shared append-only template
199
325
  # on Windows too; other scaffold files retain their existing EOLs.
200
326
  newline = "\n" if fname == "done-tasks.md" else None
201
- with open(fp, "w", encoding="utf-8", newline=newline) as f:
202
- f.write(content)
327
+ _create_scaffold_file(os.path.join(cad, fname), content, newline=newline)
203
328
  created.append(fname)
204
329
 
205
330
  # #161: install the git-level enforcement backstop (pre-commit/pre-push) so
@@ -215,9 +340,12 @@ def main(argv=None):
215
340
  except Exception as e: # noqa: BLE001
216
341
  print(f"git hooks: install skipped ({e})", file=sys.stderr)
217
342
 
218
- print(f"SCAFFOLDED .codearbiter/ at {cad}")
343
+ print(f"{'RECOVERED' if recovering else 'SCAFFOLDED'} .codearbiter/ at {cad}")
219
344
  print("created: " + ", ".join(created))
220
- print(f"arbiter: enabled (stage {args.stage}); CONTEXT.md is a stub (no <!--INITIALIZED--> sentinel).")
345
+ if recovering and "CONTEXT.md" not in created:
346
+ print("arbiter: enabled; existing uninitialized CONTEXT.md and history retained.")
347
+ else:
348
+ print(f"arbiter: enabled (stage {args.stage}); CONTEXT.md is a stub (no <!--INITIALIZED--> sentinel).")
221
349
  print(f"Next: run {_cc} (source exists) or {_dc} (greenfield) to populate.")
222
350
 
223
351
 
@@ -65,6 +65,7 @@ import sys
65
65
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
66
66
  import hostapi # noqa: E402 — host seam (ADR-0011)
67
67
  import _approvallib # noqa: E402 — host-owned artifact approval capture
68
+ import _artifactauthoritylib # noqa: E402 — exact context preview approval capture
68
69
  import _hooklib # noqa: E402
69
70
  import _modelib # noqa: E402 — mode plane core (T-06..T-16, Lane A)
70
71
  import _prerequisitelib # noqa: E402 — host-owned prerequisite capture
@@ -485,6 +486,15 @@ def _handle_user_prompt_submit(payload, host):
485
486
  )
486
487
  if reconciliation:
487
488
  contexts.append(reconciliation)
489
+ context_preview = _artifactauthoritylib.capture_context_preview_from_hook(
490
+ root=root,
491
+ plugin_root=host.plugin_root(),
492
+ prompt=prompt,
493
+ host=host.name,
494
+ session_id=session_id,
495
+ )
496
+ if context_preview:
497
+ contexts.append(context_preview)
488
498
  approval_context = "\n".join(contexts)
489
499
  if not active:
490
500
  if not approval_context:
@@ -513,6 +523,10 @@ def _handle_precompact(payload, host):
513
523
  if not _hooklib.arbiter_active(root):
514
524
  return 0
515
525
  _bump_compaction_generation(_hooklib.marker_root(payload), session_id)
526
+ # Read-time context pointers have their own worktree-local epoch. A
527
+ # surviving read marker cannot prove that a compacted actor still carries
528
+ # those pointers. Keep this separate from the persona generation above.
529
+ _readinjectlib.bump_context_generation(root, session_id)
516
530
  return 0
517
531
 
518
532
 
@@ -34,7 +34,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
34
34
  import hostapi # noqa: E402 — host seam (ADR-0011): plugin root + capability flags
35
35
  from _durabilitylib import is_ephemeral_path # noqa: E402
36
36
  from _hooklib import ( # noqa: E402
37
- frontmatter_enabled, get_host, marker_root, project_root, set_host,
37
+ frontmatter_enabled, get_host, initialized_body_text, marker_root, project_root, set_host,
38
38
  utf8_stdio, write_text_atomic,
39
39
  )
40
40
  from _standuplib import ( # noqa: E402
@@ -59,7 +59,6 @@ import _updatelib # noqa: E402 — update-available notifier (cache read + noti
59
59
  from _modelib import _DEV_PENDING_CLOSE_MAX, _settle_dev_close # noqa: E402,F401
60
60
  import _modelib # noqa: E402
61
61
 
62
- INITIALIZED_RE = re.compile(r"<!--\s*INITIALIZED\s*-->")
63
62
  STAGE_RE = re.compile(r"^stage:\s*([0-9]+)", re.I | re.M)
64
63
  CONFIRM_RE = re.compile(r"CONFIRM-[0-9]+")
65
64
 
@@ -853,7 +852,7 @@ def clear_mode_marker(root, host_name=None, session_id=None, now=None):
853
852
  _settle_dev_close(root, marker=marker, new_line=line, host_name=host_name)
854
853
 
855
854
 
856
- def provenance_drift_line(root, runner=None):
855
+ def provenance_drift_line(root, runner=None, report_unknown=False):
857
856
  """One-line SessionStart drift notice, or "" when clean/degraded.
858
857
 
859
858
  Wraps _provenancelib.startup_drift_line; any failure degrades to "" so the
@@ -861,12 +860,26 @@ def provenance_drift_line(root, runner=None):
861
860
  injectable so tests are deterministic/offline; production passes None which
862
861
  lets the lib bind its default `git -C root hash-object` runner. (T-16)"""
863
862
  try:
864
- return _provenancelib.startup_drift_line(
865
- root, runner=runner, cmd_ref=get_host().cmd_ref)
863
+ unknown = [] if report_unknown else None
864
+ line = _provenancelib.startup_drift_line(
865
+ root, runner=runner, cmd_ref=get_host().cmd_ref, unknown=unknown)
866
+ if report_unknown and unknown:
867
+ return "context coverage: unknown ({}) -- run {}".format(
868
+ unknown[0], get_host().cmd_ref("context-check"))
869
+ return line
866
870
  except Exception: # noqa: BLE001 — never crash session startup
867
871
  return ""
868
872
 
869
873
 
874
+ def context_coverage_line(root):
875
+ """Bounded provenance coverage notice; no source hash or authority claim."""
876
+ try:
877
+ return _provenancelib.startup_context_coverage_line(
878
+ root, cmd_ref=get_host().cmd_ref)
879
+ except Exception: # noqa: BLE001 — never crash session startup
880
+ return "context coverage: unknown -- run /ca:context-check"
881
+
882
+
870
883
  # --- Update-available notifier (spec: update-available-notifier.md) ---------
871
884
  # codeArbiter ships via a third-party marketplace, which Claude Code does NOT
872
885
  # auto-update by default. This surfaces a single line when the cached "latest"
@@ -1110,6 +1123,11 @@ def emit_daily_briefing(root, summary, date_iso, marker_present, ctx_text=None,
1110
1123
 
1111
1124
 
1112
1125
  def main():
1126
+ # This is the ACTIVE host startup path. It may settle mode state, heal a
1127
+ # host-owned statusline pin, install Git enforcement, and spawn fetch/update
1128
+ # children. An unfamiliar repository must be inspected by the explicit-root
1129
+ # init --check --passive route BEFORE entering a host session; a later skill
1130
+ # cannot retroactively prevent SessionStart effects.
1113
1131
  utf8_stdio()
1114
1132
  # get_host() (#257): resolves the SAME Host run(host) already primed via
1115
1133
  # set_host(), instead of a second hostapi.load_host() disk/probe.
@@ -1231,7 +1249,7 @@ def main():
1231
1249
  emit_banner(getattr(host, "name", "unknown"), mode)
1232
1250
 
1233
1251
  ctx_text = read_text(ctx) or ""
1234
- if not INITIALIZED_RE.search(ctx_text):
1252
+ if not initialized_body_text(ctx_text):
1235
1253
  emit_not_initialized(root, host, mode)
1236
1254
  sys.exit(0)
1237
1255
 
@@ -1246,8 +1264,9 @@ def main():
1246
1264
  emit_task_summary(ot_text)
1247
1265
 
1248
1266
  # --- Passive provenance drift notice (T-16, spec pillar 4) ---
1249
- _drift = provenance_drift_line(root)
1267
+ _drift = provenance_drift_line(root, report_unknown=True)
1250
1268
  emit_provenance_drift(_drift)
1269
+ emit_provenance_drift(context_coverage_line(root))
1251
1270
 
1252
1271
  # --- Update-available notice (AC-1/AC-2/AC-3) --------------------------
1253
1272
  _update = update_notice_line(plugin)
@@ -301,6 +301,47 @@ rewrite the returned envelope yourself.
301
301
  The host hooks bind the exact launch call, child start and
302
302
  the child's first stop; a pasted or coordinator-authored decision is not review
303
303
  authority.
304
+
305
+ When reviewing completed work that relies on a verification run, linked
306
+ worktrees or supporting evidence, explicitly select the completion path. Add
307
+ `--completion-receipt <exact-published-receipt>` once per task and
308
+ `--supporting-file <absolute-regular-file>` for each supporting document to the
309
+ review `arm` command. A task review selects one verification receipt; a scope
310
+ review selects the exact receipt for every member. Supporting files require a
311
+ receipt selection. The existing review-before-verification path remains
312
+ available; it does not satisfy an explicitly requested completion review.
313
+
314
+ The native engine validates the selected published verification chain and
315
+ derives the worktree mappings from its qualified observation. It retains the
316
+ selected identities, worktree closure and supporting bytes in the immutable
317
+ context. Supporting material is data for review, not authority. Selection is
318
+ explicit: no newest-receipt search, directory import or attachment execution.
319
+ At most 16 regular files are admitted, each at most 64 KiB and together at most
320
+ 256 KiB; links, reparse indirection, duplicate paths and changed content fail
321
+ closed. Read canonical spec and plan artifacts through the native artifact API;
322
+ they cannot be imported as supporting files. The generated prompt directs the reviewer to assess performed work
323
+ against each criterion, step and `done_when` obligation, using selected evidence
324
+ and source references. A consistent plan or an empty diff is insufficient.
325
+
326
+ Use the returned `completion_sha256` in the native `task-review` or
327
+ `accept-scope` request. The completed review and its exact verification run must
328
+ agree; a definition-only review or a different successful run is rejected.
329
+ The recorded task selection remains required by downstream freshness and scope
330
+ acceptance. Scope review uses validated task completion bindings. If verification
331
+ is rerun, obtain and record its completed task review before selecting that run
332
+ for scope review; do not infer equivalence between receipts.
333
+
334
+ Creating the first completion context recomputes the current raw worktree
335
+ fingerprint and requires it to match the selected verifier's recorded fingerprint.
336
+ Later freshness checks allow native task-status and execution bookkeeping updates
337
+ in validated canonical artifacts while retaining their normative content hashes.
338
+ Changes to task definitions, other HTML or other governance files still affect
339
+ freshness under the normal snapshot policy. The closure covers
340
+ tracked working-tree bytes and non-ignored untracked files, not ignored
341
+ dependencies or outputs. The completion assessment is still an independent
342
+ reviewer's judgment; schema validation does not prove that every file was read
343
+ or every conclusion is correct. Preserve the unchanged launch envelope and
344
+ genuine host capture throughout this path.
304
345
  Publish the completed request through the same `publish` command. `task-review`
305
346
  records the separate verification and spec-review receipts. After every task in
306
347
  the scope reaches `REVIEW`, reverify against one current source snapshot, run the
@@ -1,15 +1,27 @@
1
1
  # Author TDD workflow — non-negotiable, fixed order
2
2
 
3
- Canonical for the author agents (`backend-author`, `frontend-author`). Loaded via each agent's
3
+ Canonical for the author agents (`backend-author`, `frontend-author`, `infra-author`). Loaded via each agent's
4
4
  Required Reading; the agent bodies do not restate it. This is the worker's execution order for
5
5
  one task — the tdd skill's phase gates govern the lane above it.
6
6
  Apply `<plugin-root>/includes/verification-boundary.md` throughout.
7
7
 
8
+ Use the Phase 1 checklist for the owning route. For a bug-origin `/fix`, carry the
9
+ current expectation, cited evidence, supported cause, and regression oracle into
10
+ the existing obligation ledger; confirm the original caller's repair authority and
11
+ source freshness through the fix owner. Do not fabricate a feature artifact. For
12
+ `/feature`, keep the approved spec and its criteria; for `/refactor`, keep its
13
+ approved surface and parity contract. An ADR remains a user-attributed decision
14
+ through `/adr`, never an author shortcut.
15
+
8
16
  Fixed order. Do not skip or reorder.
9
17
 
10
- 1. **Write failing tests** — one test per obligation in the Phase 1 checklist
11
- 2. **Confirm tests fail for the right reason** — run the test command from `tech-stack.md`; the failure must match the obligation, not an unrelated error
18
+ 1. **Map failing tests** — bind each Phase 1 obligation to a test ID. For `/fix`, assess an existing failing test against target red using the current regression oracle and evidence. Name the existing test ID in the ledger and reuse that named existing target-red test when its oracle is adequate and the assertion matches the defect. Do not duplicate a test because of its age or because it predates repair. Add tests only for uncovered obligations.
19
+ 2. **Confirm tests are red for the right reason** — run the test command from `tech-stack.md`; the target failure must match the obligation. Record unrelated existing failures in the baseline; an unrelated failure is not target red. An import or setup error is not target red. Preserve and report their identities and outputs. Required commit gates remain blocked while required checks fail. Do not skip tests, suppress failures, or waive gates to proceed.
12
20
  3. **Write minimum implementation** — only enough code to make the failing tests pass; no extra scope
13
- 4. **Run impact-bounded local suite** — every affected test green; exhaustive and cross-platform proof runs in exact-head hosted CI before merge
21
+ 4. **Run impact-bounded local suite** — rerun the same named target test for fresh green on the repaired source, with its assertions unchanged between red and green. Rerun fresh affected-contract tests on that source and record their results; exhaustive and cross-platform proof runs in exact-head hosted CI before merge
14
22
  5. **Run lint and type-check** — both clean
15
23
  6. **Stage for commit** — only after steps 1–5 complete
24
+
25
+ For a repair, observe target red on the broken source before minimum implementation. If a material fixture or setup change occurs after that observation, the earlier red no longer qualifies: repeat target red on the broken implementation with the changed fixture or setup before continuing the repair. Keep the test's assertions unchanged between red and green.
26
+
27
+ For an intermittent or timing defect, use a faithful deterministic seam when one exists. Otherwise define a finite repeated-trial oracle with its trial bound and pass/fail rule before running it. Record the stochastic sample limit; do not claim deterministic proof from a passing sample or retry until green. A failing trial must still match the target mechanism, and fresh green remains bounded by the stated oracle.
@@ -0,0 +1,128 @@
1
+ # Installed private helper location
2
+
3
+ Use this card only for the debug packet validator and an authorized task-board
4
+ follow-up. The exact installed helper leaves are `hooks/debug-handoff.py` (the
5
+ private `validate` operation) and `hooks/taskwrite.py` (the existing board
6
+ writer). The caller chooses one of those literal leaves before resolution;
7
+ packet text, a project file, and a user argument never choose executable code.
8
+ Select and validate one Python 3 interpreter before invoking the helper. Try
9
+ `python3` first, then `python` only if the first candidate is absent, a launcher
10
+ stub, or incompatible. The probe executes the candidate to obtain its actual
11
+ `sys.executable`, then validates that absolute executable directly as Python
12
+ 3.8 or newer. The installed debug validator's `_debughandofflib.py` uses an
13
+ assignment expression, which Python 3.7 cannot parse; the shared selection
14
+ must satisfy that helper even when the current call targets `taskwrite.py`.
15
+ Invoke the helper through that executable, not a batch or shell launcher that
16
+ could alter quoted arguments. A name on `PATH` or a successful `--version`
17
+ alone does not establish compatibility. This source-derived minimum is not a
18
+ claim that every newer minor has been qualified. Once selected, invoke the one resolved absolute helper with `-B` and
19
+ literal arguments. Preserve its exit, stdout, and stderr; do not replay a
20
+ failure under another interpreter or helper.
21
+
22
+ Use the selection block for the current shell before the first helper call.
23
+ These blocks set `$caPython` in PowerShell or `ca_python` in POSIX sh. A missing
24
+ compatible interpreter stops before the helper; fix the host installation
25
+ before retrying the whole operation.
26
+
27
+ ### Pi PowerShell interpreter selection
28
+ ```powershell
29
+ $caPython = $null
30
+ foreach ($caName in @('python3', 'python')) {
31
+ $caCommand = Get-Command -Name $caName -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
32
+ if ($null -eq $caCommand) { continue }
33
+ try {
34
+ $caExecutable = & $caCommand.Source -B -c "import sys; sys.exit(1) if not (sys.version_info[0] == 3 and sys.version_info >= (3, 8)) else None; print(sys.executable)" 2>$null
35
+ if ($LASTEXITCODE -ne 0 -or $caExecutable -isnot [string] -or
36
+ -not [System.IO.Path]::IsPathFullyQualified($caExecutable) -or
37
+ -not (Test-Path -LiteralPath $caExecutable -PathType Leaf)) { continue }
38
+ $caIdentity = & $caExecutable -B -c "import sys; print('codearbiter-python3' if sys.version_info[0] == 3 and sys.version_info >= (3, 8) else 'incompatible')" 2>$null
39
+ if ($LASTEXITCODE -eq 0 -and $caIdentity -ceq 'codearbiter-python3') {
40
+ $caPython = $caExecutable
41
+ break
42
+ }
43
+ } catch { continue }
44
+ }
45
+ if (-not $caPython) { throw 'compatible Python 3 interpreter unavailable' }
46
+ ```
47
+
48
+ ### Pi POSIX interpreter selection
49
+ ```sh
50
+ ca_python=
51
+ for ca_candidate in python3 python; do
52
+ ca_path=$(command -v "$ca_candidate" 2>/dev/null) || continue
53
+ case "$ca_path" in /*) ;; *) continue;; esac
54
+ [ -f "$ca_path" ] || continue
55
+ ca_executable=$("$ca_path" -B -c 'import sys; sys.exit(1) if not (sys.version_info[0] == 3 and sys.version_info >= (3, 8)) else None; print(sys.executable)' 2>/dev/null) || continue
56
+ case "$ca_executable" in
57
+ /*) ;;
58
+ [A-Za-z]:'\'*)
59
+ command -v cygpath >/dev/null 2>&1 || continue
60
+ ca_executable=$(cygpath -u "$ca_executable") || continue ;;
61
+ *) continue ;;
62
+ esac
63
+ [ -f "$ca_executable" ] && [ -x "$ca_executable" ] || continue
64
+ ca_identity=$("$ca_executable" -B -c 'import sys; print("codearbiter-python3" if sys.version_info[0] == 3 and sys.version_info >= (3, 8) else "incompatible")' 2>/dev/null) || continue
65
+ if [ "$ca_identity" = codearbiter-python3 ]; then ca_python=$ca_executable; break; fi
66
+ done
67
+ if [ -z "$ca_python" ]; then printf '%s\n' 'compatible Python 3 interpreter unavailable' >&2; exit 2; fi
68
+ ```
69
+
70
+ Start from the absolute path of the **loaded installed entry resource** that
71
+ the host actually supplied: `commands/task.md` or `commands/debug.md` on
72
+ Claude, `skills/ca-task/SKILL.md` or `skills/ca-debug/SKILL.md` on Codex and
73
+ Pi. The Codex and Pi skill directories are two levels below the package root;
74
+ Claude's command directory is one level below it. Do not use the current directory, a project checkout,
75
+ an unset `PLUGIN_ROOT`/`CLAUDE_PLUGIN_ROOT` token from a hook runner, `/hooks`,
76
+ a source clone, or a recursive cache search. If the loaded resource, package
77
+ identity, or exact helper is absent, report that failure and stop. Keep the
78
+ current working directory at the intended project when invoking `taskwrite.py`.
79
+
80
+
81
+ Pi's loaded `skills/ca-task/SKILL.md` or `skills/ca-debug/SKILL.md` is the
82
+ anchor. `package.json` is the installed package identity marker; a written
83
+ `<plugin-root>` string is not an executable path. Bind the selected resource
84
+ and one literal helper leaf as above.
85
+
86
+ ### Pi PowerShell resolution
87
+ ```powershell
88
+ $caResolvedResource = (Resolve-Path -LiteralPath $caLoadedResource -ErrorAction Stop).ProviderPath
89
+ $caResourceDirectory = Split-Path -Parent $caResolvedResource
90
+ $caInstalledRoot = (Resolve-Path -LiteralPath (Join-Path $caResourceDirectory '..\..') -ErrorAction Stop).ProviderPath
91
+ $caManifest = Join-Path $caInstalledRoot 'package.json'
92
+ if (-not (Test-Path -LiteralPath $caManifest -PathType Leaf)) { throw "installed ca-pi package identity missing" }
93
+ $caHelper = Join-Path $caInstalledRoot $caHelperRelative
94
+ if (-not (Test-Path -LiteralPath $caHelper -PathType Leaf)) { throw "installed helper missing: $caHelper" }
95
+ ```
96
+
97
+ ### Pi POSIX resolution
98
+ ```sh
99
+ case "$ca_loaded_resource" in /*) ;; *) printf '%s\n' 'loaded installed resource must be absolute' >&2; exit 2;; esac
100
+ if [ ! -f "$ca_loaded_resource" ]; then printf '%s\n' 'loaded installed resource missing' >&2; exit 2; fi
101
+ ca_installed_root=$(CDPATH= cd -P "$(dirname "$ca_loaded_resource")/../.." && pwd -P) || exit 2
102
+ if [ ! -f "$ca_installed_root/package.json" ]; then printf '%s\n' 'installed ca-pi package identity missing' >&2; exit 2; fi
103
+ ca_helper=$ca_installed_root/$ca_helper_relative
104
+ if [ ! -f "$ca_helper" ]; then printf '%s\n' 'installed helper missing' >&2; exit 2; fi
105
+ ```
106
+
107
+
108
+ Never repair a missing installed helper by selecting a similarly named file
109
+ from a checkout or cache. A missing or partial package blocks the operation.
110
+
111
+ For private debug validation, select the literal leaf
112
+ `hooks/debug-handoff.py` with the resolution block above. Supply one finite
113
+ packet of at most 65,536 bytes through the host execution tool's stdin and
114
+ retain that tool's timeout and output controls. The helper owns rejection of
115
+ oversize input; never build a shell pipeline, command string, path argument,
116
+ or unbounded read to transport the packet. Invoke only once with the literal
117
+ `validate` operation and retain its original exit, stdout, and stderr.
118
+
119
+ ### Pi PowerShell validate invocation
120
+ ```powershell
121
+ & $caPython -B $caHelper validate
122
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
123
+ ```
124
+
125
+ ### Pi POSIX validate invocation
126
+ ```sh
127
+ "$ca_python" -B "$ca_helper" validate
128
+ ```
@@ -22,6 +22,19 @@ file maps those actions to Pi's extension API.
22
22
  silently allows classified reads. Plan mode is read-only except for the
23
23
  current canonical spec, plan, and plan-ledger files; source, configuration,
24
24
  unrelated project-state, and external mutations deny.
25
+ - For an authorized debug validator or task-board writer call, load
26
+ [helper-invocation.md](helper-invocation.md) relative to this installed host
27
+ note. Derive the installed package
28
+ from the absolute path of the currently loaded `ca-debug` or `ca-task`
29
+ `SKILL.md`, then check the exact helper and run the card's PowerShell or POSIX
30
+ Python 3 selection block before invoking it. Select once by an executed
31
+ identity probe; preserve a failed helper's original output and exit status
32
+ without a second-interpreter attempt. Put task options before `--` and pass
33
+ user text as one literal argument. Feed validation one finite packet of at
34
+ most 65,536 bytes through the host tool's stdin using the card's direct
35
+ `validate` call. The literal `<plugin-root>` spelling
36
+ is not a shell path. A missing helper blocks the call; preserve the current
37
+ project working directory for board writes.
25
38
  - `codearbiter_background_bash` and `/ca-jobs list|tail|cancel` are bounded,
26
39
  session-only parent capabilities. Jobs and their metadata terminate at
27
40
  shutdown and are never restored from Pi session entries.
@@ -27,7 +27,7 @@ reached the same way.
27
27
  | Docs edit / dep bump / revert | `/chore` (type-scaled gates) | `dependency-reviewer` for deps | No behavioral code; suite green for deps/revert; exits via `commit-gate` |
28
28
  | Exploratory throwaway spike | `/spike` (self-contained command) → `spike/<slug>` branch | — | Never merges or PRs; `commit-gate`-exempt (nothing on the branch can land); exits to a findings note or `/feature` |
29
29
  | Behavior-preserving restructure | `refactor` skill at `<plugin-root>/routines/refactor/SKILL.md` (explicit `/refactor` uses the same owner) | `tdd` Phase 1 (coverage backfill or new seams) | No refactor without parity-coverage proof |
30
- | Unknown defect / investigation | `debug` skill at `<plugin-root>/routines/debug/SKILL.md` (explicit `/debug` uses the same owner) | — | No code change in the skill; one named exit |
30
+ | Unknown defect / investigation | `debug` skill at `<plugin-root>/routines/debug/SKILL.md` (explicit `/debug` uses the same owner) | — | No code change in the skill; one of confirmed_code_defect, confirmed_noncode_cause, design_question, no_action, or unresolved with evidence; continuation retains caller authority |
31
31
  | Commit | `commit-gate` skill at `<plugin-root>/routines/commit-gate/SKILL.md` (explicit `/commit` uses the same owner) | — | No commit without all nine gates green |
32
32
  | Open a PR / finish a branch | `finishing-a-development-branch` at `<plugin-root>/routines/finishing-a-development-branch/SKILL.md`; select direct-open or caller mode first | reviewer fleet per path; PR-body prose applies `anti-slop-design` (`core` + `medium-documents` §7.A.1) | PR only; no direct-to-default, no force-push |
33
33
  | Watch a PR's CI / babysit checks | `/watch` → detached `gh pr checks --watch` | on-red diagnose (propose\|branch) | Never auto-merges; green → notify + offer; merge-to-default routes through the hard gate; no poll loop |
@@ -14,9 +14,11 @@ code instead of the first's (#577) — then run:
14
14
 
15
15
  It writes `<project-root>/.codearbiter/.markers/security-gate-passed` containing a digest of
16
16
  every sensitive added line it approved. The PreToolUse commit hook (**H-09b** for crypto/TLS, **H-10b**
17
- for secrets) blocks any commit whose staged diff touches a guarded pattern until this marker is fresh
18
- (< 30 min) AND covers every sensitive line being committed — a pass recorded for one diff cannot
19
- launder a later, different change through the freshness window.
17
+ for secrets) blocks any commit whose staged diff touches a guarded pattern until this marker exists
18
+ AND covers every sensitive line being committed. There is no time window: a pass stays valid for
19
+ exactly the lines it reviewed, in the files it reviewed them in. Any new or changed sensitive line,
20
+ or a reviewed line moved to another file, is uncovered and blocks, so a pass recorded for one diff
21
+ cannot launder a later, different change.
20
22
 
21
23
  On any BLOCK, do **not** record the pass — the commit stays blocked until the finding is resolved and
22
24
  the gate genuinely passes. A premature or unconditional recording defeats the gate.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arbiterforge/ca-pi",
3
- "version": "0.15.10",
3
+ "version": "0.16.0",
4
4
  "private": true,
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",
@@ -15,7 +15,7 @@ not both merely to repeat the same procedure. This index lists what each skill O
15
15
  | [tdd](tdd/SKILL.md) | `/feature` (after spec approval), `/fix`, `/refactor` | The test-first gate: six phases — obligation scan → red → green → obligation verify → coverage → lint. No implementation before Phase 1; no path to `commit-gate` until all six gates are green. |
16
16
  | [commit-gate](commit-gate/SKILL.md) | `/commit` | The commit gate: nine phases — permission, branch, classification, impact-bounded local verification under the shared [verification boundary](../includes/verification-boundary.md), behavioral proof, diff review, selective stage, message, commit. No commit while an applicable check is red, the behavior unproven, or secrets/cruft present; exhaustive exact-head proof remains a hosted-CI merge gate. |
17
17
  | [decision-variance](decision-variance/SKILL.md) | Architectural reconciliation or variance report; explicit `/reconcile` retained | One SMARTS arbitration owner; report-only is non-mutating. Analysis card loads on demand; only explicit user choices enter the append-only log. |
18
- | [debug](debug/SKILL.md) | Diagnosis intent or `/debug` | Investigate-then-decide root-cause analysis: five phases. Investigation only, no code change here; forces one named exit — `/fix`, `/adr`, or a no-action close. |
18
+ | [debug](debug/SKILL.md) | Diagnosis intent or `/debug` | Investigate-then-decide root-cause analysis: five phases. Investigation only, no code change here; returns one evidence-backed result: confirmed_code_defect, confirmed_noncode_cause, design_question, no_action, or unresolved. Follow-up action retains caller authority; no default task or ADR write. |
19
19
  | [refactor](refactor/SKILL.md) | Behavior-preserving restructure intent or `/refactor` | Behavior-preserving change behind a parity-coverage proof gate; routes to `tdd` Phase 1 for coverage backfill or new test seams. |
20
20
  | [context-creation](context-creation/SKILL.md) | `/create-context` (brownfield) | Back-fills `.codearbiter/` from existing source via parallel scouts; writes `CONTEXT.md` (`arbiter: enabled` + `stage:`) and locks it `<!--INITIALIZED-->`. |
21
21
  | [decompose](decompose/SKILL.md) | greenfield startup, `/decompose` | Six-layer senior-architect interview, compaction-resilient via per-layer disk drafts + DRAFT ADRs; populates `.codearbiter/` and locks it initialized. |