@arbiterforge/ca-pi 0.6.3 → 0.10.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.
- package/README.md +41 -98
- package/package.json +1 -1
- package/plugins/ca-pi/CHANGELOG.md +145 -0
- package/plugins/ca-pi/COMMANDS.md +138 -68
- package/plugins/ca-pi/SKILLS.md +137 -30
- package/plugins/ca-pi/agents/INDEX.md +3 -2
- package/plugins/ca-pi/agents/checkpoint-aggregator.md +8 -7
- package/plugins/ca-pi/agents/design-quality-reviewer.md +1 -1
- package/plugins/ca-pi/agents/finding-triage.md +31 -14
- package/plugins/ca-pi/agents/verdict-aggregator.md +64 -0
- package/plugins/ca-pi/{ORCHESTRATOR.md → arbiter.md} +37 -36
- package/plugins/ca-pi/extensions/codearbiter.js +844 -19
- package/plugins/ca-pi/generated/command-catalog.json +386 -196
- package/plugins/ca-pi/generated/roles.json +9 -0
- package/plugins/ca-pi/hooks/_arbiterstatelib.py +59 -11
- package/plugins/ca-pi/hooks/_bashguardlib.py +30 -12
- package/plugins/ca-pi/hooks/_gitexec.py +23 -0
- package/plugins/ca-pi/hooks/_githooks.py +50 -23
- package/plugins/ca-pi/hooks/_hooklib.py +148 -20
- package/plugins/ca-pi/hooks/_host.py +9 -1
- package/plugins/ca-pi/hooks/_metricslib.py +20 -0
- package/plugins/ca-pi/hooks/_modelib.py +762 -0
- package/plugins/ca-pi/hooks/_protectedlib.py +13 -4
- package/plugins/ca-pi/hooks/_prunelib.py +51 -12
- package/plugins/ca-pi/hooks/_prunepolicy.py +33 -7
- package/plugins/ca-pi/hooks/_readinjectlib.py +10 -4
- package/plugins/ca-pi/hooks/_releaselib.py +278 -48
- package/plugins/ca-pi/hooks/_updatelib.py +230 -50
- package/plugins/ca-pi/hooks/doctor.py +58 -9
- package/plugins/ca-pi/hooks/git-enforce.py +10 -3
- package/plugins/ca-pi/hooks/hostapi.py +220 -22
- package/plugins/ca-pi/hooks/pi-bridge.py +10 -4
- package/plugins/ca-pi/hooks/prompt-submit.py +486 -0
- package/plugins/ca-pi/hooks/prune-transcript.py +23 -3
- package/plugins/ca-pi/hooks/session-start.py +529 -435
- package/plugins/ca-pi/hooks/statusline.py +28 -10
- package/plugins/ca-pi/hooks/wire-statusline.py +13 -8
- package/plugins/ca-pi/includes/anti-slop-design/INDEX.md +1 -1
- package/plugins/ca-pi/includes/command-compatibility.md +16 -0
- package/plugins/ca-pi/includes/dangerous-mode.md +57 -0
- package/plugins/ca-pi/includes/ops-mode.md +96 -0
- package/plugins/ca-pi/includes/pi-host-notes.md +10 -1
- package/plugins/ca-pi/includes/redirect.md +12 -1
- package/plugins/ca-pi/includes/routing-table.md +14 -5
- package/plugins/ca-pi/includes/safety-core.md +86 -0
- package/plugins/ca-pi/includes/smarts/core.md +1 -1
- package/plugins/ca-pi/routines/INDEX.md +1 -1
- package/plugins/ca-pi/routines/decision-lifecycle/SKILL.md +55 -3
- package/plugins/ca-pi/routines/decision-lifecycle/references/adr-template.md +9 -1
- package/plugins/ca-pi/routines/decompose/SKILL.md +1 -1
- package/plugins/ca-pi/routines/dispatching-parallel-agents/SKILL.md +4 -4
- package/plugins/ca-pi/routines/release/SKILL.md +1 -1
- package/plugins/ca-pi/skills/ca-checkpoint/SKILL.md +5 -4
- package/plugins/ca-pi/skills/ca-cleanup/SKILL.md +6 -0
- package/plugins/ca-pi/skills/ca-context-check/SKILL.md +6 -0
- package/plugins/ca-pi/skills/ca-create-context/SKILL.md +6 -0
- package/plugins/ca-pi/skills/ca-decompose/SKILL.md +6 -0
- package/plugins/ca-pi/skills/ca-doctor/SKILL.md +4 -0
- package/plugins/ca-pi/skills/ca-init/SKILL.md +18 -1
- package/plugins/ca-pi/skills/ca-pr/SKILL.md +17 -1
- package/plugins/ca-pi/skills/ca-review/SKILL.md +3 -4
- package/plugins/ca-pi/skills/ca-spike/SKILL.md +15 -8
- package/plugins/ca-pi/skills/ca-status/SKILL.md +13 -1
- package/plugins/ca-pi/skills/ca-watch/SKILL.md +6 -0
- package/plugins/ca-pi/includes/dev-mode.md +0 -30
- package/plugins/ca-pi/skills/ca-arbiter/SKILL.md +0 -36
- package/plugins/ca-pi/skills/ca-dev/SKILL.md +0 -42
|
@@ -89,9 +89,17 @@ AUDIT_LOG_FLAT_BASENAMES = ("overrides.log", "triage.log", "gate-events.log", "s
|
|
|
89
89
|
# from the H-11 set below leaves the append blocked, because classify_protected
|
|
90
90
|
# reports every class a path hits and pre-write checks them independently.
|
|
91
91
|
DECISION_LOG_BASENAME = "decision-log.md"
|
|
92
|
+
ADR_LIFECYCLE_LOG_BASENAME = "adr-lifecycle.jsonl"
|
|
93
|
+
DECISION_AUDIT_LOG_BASENAMES = (DECISION_LOG_BASENAME, ADR_LIFECYCLE_LOG_BASENAME)
|
|
94
|
+
DECISION_AUDIT_LOG_NAMES = "(?:" + "|".join(
|
|
95
|
+
re.escape(n) for n in DECISION_AUDIT_LOG_BASENAMES) + ")"
|
|
92
96
|
DECISION_LOG_RE = re.compile(
|
|
93
97
|
r"\.codearbiter[\\/]+decisions[\\/]+" + re.escape(DECISION_LOG_BASENAME) + r"$"
|
|
94
98
|
)
|
|
99
|
+
ADR_LIFECYCLE_LOG_RE = re.compile(
|
|
100
|
+
r"\.codearbiter[\\/]+decisions[\\/]+" + re.escape(ADR_LIFECYCLE_LOG_BASENAME) + r"$",
|
|
101
|
+
re.I,
|
|
102
|
+
)
|
|
95
103
|
# AUDIT_LOG_BASENAMES stays the SINGLE AUTHORITATIVE BASENAME LIST, and the
|
|
96
104
|
# arbitration log is in it. _bashguardlib's H-05 shell check pre-filters with
|
|
97
105
|
# `any(n in cmd for n in AUDIT_LOG_BASENAMES)` precisely so a newly added audit
|
|
@@ -99,7 +107,7 @@ DECISION_LOG_RE = re.compile(
|
|
|
99
107
|
# alternation below would sail past that pre-filter and leave the log deletable
|
|
100
108
|
# from the shell. (Caught by test_hook_guards.py, which the comment on that
|
|
101
109
|
# pre-filter predicted verbatim.)
|
|
102
|
-
AUDIT_LOG_BASENAMES = AUDIT_LOG_FLAT_BASENAMES +
|
|
110
|
+
AUDIT_LOG_BASENAMES = AUDIT_LOG_FLAT_BASENAMES + DECISION_AUDIT_LOG_BASENAMES
|
|
103
111
|
AUDIT_LOG_NAMES = "(?:" + "|".join(re.escape(n) for n in AUDIT_LOG_BASENAMES) + ")"
|
|
104
112
|
# The path anchor stays scoped to the FLAT logs — those sit directly under
|
|
105
113
|
# .codearbiter/, the arbitration log one level deeper — so is_audit_log() tests
|
|
@@ -148,9 +156,10 @@ GATE_MARKER_NAMES = r"(?:security-gate-passed|migration-gate-passed)"
|
|
|
148
156
|
def is_audit_log(rel):
|
|
149
157
|
"""True iff `rel` is one of the append-only .codearbiter audit logs
|
|
150
158
|
(overrides.log, triage.log, sprint-log.md, gate-events.log) or the SMARTS
|
|
151
|
-
|
|
159
|
+
decision ledgers under decisions/ — the H-05 guard set."""
|
|
152
160
|
n = norm_path(rel)
|
|
153
|
-
return bool(AUDIT_LOG_RE.search(n) or DECISION_LOG_RE.search(n)
|
|
161
|
+
return bool(AUDIT_LOG_RE.search(n) or DECISION_LOG_RE.search(n)
|
|
162
|
+
or ADR_LIFECYCLE_LOG_RE.search(n))
|
|
154
163
|
|
|
155
164
|
|
|
156
165
|
def is_tail_append(current, old, new):
|
|
@@ -190,7 +199,7 @@ def is_decisions_path(rel):
|
|
|
190
199
|
cannot launder itself out of the marker gate. (`decision-log.md.bak` is in
|
|
191
200
|
NEITHER set: it does not end in `.md`, so it was never an H-11 path either.)"""
|
|
192
201
|
n = norm_path(rel)
|
|
193
|
-
if DECISION_LOG_RE.search(n):
|
|
202
|
+
if DECISION_LOG_RE.search(n) or ADR_LIFECYCLE_LOG_RE.search(n):
|
|
194
203
|
return False
|
|
195
204
|
return bool(DECISIONS_PATH_RE.search(n))
|
|
196
205
|
|
|
@@ -30,10 +30,16 @@ import sys
|
|
|
30
30
|
import time
|
|
31
31
|
|
|
32
32
|
import _hooklib
|
|
33
|
+
import _modelib
|
|
33
34
|
import _prunepolicy as _policy
|
|
34
35
|
|
|
35
36
|
BOM = b"\xef\xbb\xbf"
|
|
36
37
|
MARKER_PREFIX = _policy.MARKER_PREFIX
|
|
38
|
+
# R-5 (#437, mode-plane-deterministic-flip): the sentinel embedded in every
|
|
39
|
+
# composed persona injection. A line whose serialized content carries it is
|
|
40
|
+
# the injected persona -- built here into SemanticEntry(pinned=True) (T-50)
|
|
41
|
+
# so `_prunepolicy` (AC-26) refuses to fold/condense/evict it at any tier.
|
|
42
|
+
PERSONA_SENTINEL = _modelib.PERSONA_SENTINEL
|
|
37
43
|
|
|
38
44
|
|
|
39
45
|
def _dumps(o):
|
|
@@ -127,7 +133,7 @@ def _tool_result_ids(o):
|
|
|
127
133
|
|
|
128
134
|
class Index:
|
|
129
135
|
__slots__ = ("protected_from", "last_assistant_idx", "tu_ids", "tr_ids",
|
|
130
|
-
"edited_paths", "edited_at", "tool_meta")
|
|
136
|
+
"edited_paths", "edited_at", "tool_meta", "pinned_ordinals")
|
|
131
137
|
|
|
132
138
|
|
|
133
139
|
def build_index(lines, cfg):
|
|
@@ -171,14 +177,35 @@ def build_index(lines, cfg):
|
|
|
171
177
|
semantic = []
|
|
172
178
|
for ln in lines:
|
|
173
179
|
o = ln.obj if isinstance(ln.obj, dict) else {}
|
|
180
|
+
# T-50: a line whose serialized content carries the injected-persona
|
|
181
|
+
# sentinel is pinned -- AC-26 requires it survive every strategy at
|
|
182
|
+
# every tier, not just the recent-turn protected tail (the persona is
|
|
183
|
+
# typically injected once, early in the transcript, and stays live
|
|
184
|
+
# for the rest of the session -- well outside `protected_from` by the
|
|
185
|
+
# time pruning runs). Same _dumps(o)-based check _has_marker already
|
|
186
|
+
# uses for the elision marker, so re-serialization quirks can't make
|
|
187
|
+
# the two disagree about the same line.
|
|
188
|
+
dumped = _dumps(o) if o else ""
|
|
174
189
|
semantic.append(_policy.SemanticEntry(
|
|
175
190
|
id=str(ln.idx), ordinal=ln.idx, role=str(o.get("type", "other")),
|
|
176
191
|
kind=("tool-result" if _tool_result_ids(o) else "message"),
|
|
177
192
|
byte_size=len(ln.raw), tool_bearing=bool(_tool_use_ids(o)),
|
|
178
|
-
marked=_has_marker(
|
|
193
|
+
marked=_has_marker(dumped) if o else False,
|
|
194
|
+
# A TOOL RESULT that merely quotes the sentinel is not an injected
|
|
195
|
+
# persona. The literal lives in this repo's own sources
|
|
196
|
+
# (`_modelib.py`, `prompt-submit.py`, `extension.ts`, their tests),
|
|
197
|
+
# so a Read of any of them, or a Grep for the sentinel itself,
|
|
198
|
+
# produced a result that pinned permanently at every tier — the
|
|
199
|
+
# transcript kept growing while the pruner reported it protected.
|
|
200
|
+
# Agents here read and grep those files routinely, so this was
|
|
201
|
+
# reachable rather than theoretical. AC-26 needs the INJECTION
|
|
202
|
+
# pinned, not every copy of the string; the injection arrives as a
|
|
203
|
+
# message, never as a tool result.
|
|
204
|
+
pinned=(PERSONA_SENTINEL in dumped and not _tool_result_ids(o)) if o else False,
|
|
179
205
|
))
|
|
180
206
|
prot = _policy.protected_ordinal(semantic, cfg.keep_recent)
|
|
181
207
|
idx.protected_from = prot
|
|
208
|
+
idx.pinned_ordinals = frozenset(e.ordinal for e in semantic if e.pinned)
|
|
182
209
|
idx.last_assistant_idx = last_assistant
|
|
183
210
|
idx.tu_ids = tu
|
|
184
211
|
idx.tr_ids = tr
|
|
@@ -196,6 +223,17 @@ def _is_small_scalar(v, limit=200):
|
|
|
196
223
|
return False
|
|
197
224
|
|
|
198
225
|
|
|
226
|
+
def _protected(ln, index):
|
|
227
|
+
"""True iff `ln` must not be touched by ANY strategy at ANY tier: either
|
|
228
|
+
it sits in the recent-turn protected tail, or it is PINNED (T-50/AC-26 --
|
|
229
|
+
the injected persona, which is typically well before `protected_from` by
|
|
230
|
+
the time pruning runs). Every strategy's skip-guard below routes through
|
|
231
|
+
this single predicate so the two protection reasons can never drift
|
|
232
|
+
apart -- a strategy that checked `ln.idx >= index.protected_from` alone
|
|
233
|
+
would silently reach a pinned line sitting earlier in the transcript."""
|
|
234
|
+
return ln.idx >= index.protected_from or ln.idx in index.pinned_ordinals
|
|
235
|
+
|
|
236
|
+
|
|
199
237
|
# --------------------------------------------------------------------------- #
|
|
200
238
|
# Strategies (Phase 1). Each mutates obj, sets dirty, records a report row.
|
|
201
239
|
# All obey the net-negative guard: a strategy is a no-op on any unit whose
|
|
@@ -216,7 +254,7 @@ def s_sidecar_collapse(lines, index, cfg, report):
|
|
|
216
254
|
small scalar fields worth keeping (status, exit codes, agentId, paths)."""
|
|
217
255
|
touched = before = after = 0
|
|
218
256
|
for ln in lines:
|
|
219
|
-
if ln
|
|
257
|
+
if _protected(ln, index) or not isinstance(ln.obj, dict):
|
|
220
258
|
continue
|
|
221
259
|
tur = ln.obj.get("toolUseResult")
|
|
222
260
|
if not isinstance(tur, (dict, list, str)):
|
|
@@ -260,7 +298,7 @@ def s_oversize_result_clamp(lines, index, cfg, report):
|
|
|
260
298
|
touched = before = after = 0
|
|
261
299
|
mb, ml = cfg.max_bytes, 100
|
|
262
300
|
for ln in lines:
|
|
263
|
-
if ln
|
|
301
|
+
if _protected(ln, index) or not isinstance(ln.obj, dict):
|
|
264
302
|
continue
|
|
265
303
|
msg = ln.obj.get("message")
|
|
266
304
|
if not isinstance(msg, dict) or not isinstance(msg.get("content"), list):
|
|
@@ -341,7 +379,7 @@ def s_reasoning_fold(lines, index, cfg, report):
|
|
|
341
379
|
most recent assistant turn is always inside the protected tail."""
|
|
342
380
|
touched = before = 0
|
|
343
381
|
for ln in lines:
|
|
344
|
-
if ln
|
|
382
|
+
if _protected(ln, index) or not isinstance(ln.obj, dict):
|
|
345
383
|
continue
|
|
346
384
|
if ln.obj.get("type") != "assistant":
|
|
347
385
|
continue
|
|
@@ -369,7 +407,7 @@ def s_aged_result_condense(lines, index, cfg, report):
|
|
|
369
407
|
tail. Specific handlers (shell/superseded) run earlier and mark their own."""
|
|
370
408
|
touched = before = after = 0
|
|
371
409
|
for ln in lines:
|
|
372
|
-
if ln
|
|
410
|
+
if _protected(ln, index) or not isinstance(ln.obj, dict):
|
|
373
411
|
continue
|
|
374
412
|
content = _content_list(ln.obj)
|
|
375
413
|
if not content:
|
|
@@ -394,7 +432,7 @@ def s_mcp_payload_condense(lines, index, cfg, report):
|
|
|
394
432
|
"""Condense the bulky `input` of mcp__ tool_use blocks (older turns)."""
|
|
395
433
|
touched = before = after = 0
|
|
396
434
|
for ln in lines:
|
|
397
|
-
if ln
|
|
435
|
+
if _protected(ln, index) or not isinstance(ln.obj, dict):
|
|
398
436
|
continue
|
|
399
437
|
content = _content_list(ln.obj)
|
|
400
438
|
if not content:
|
|
@@ -435,7 +473,7 @@ def s_shell_tail_keep(lines, index, cfg, report):
|
|
|
435
473
|
keep_lines = 30
|
|
436
474
|
touched = before = after = 0
|
|
437
475
|
for ln in lines:
|
|
438
|
-
if ln
|
|
476
|
+
if _protected(ln, index) or not isinstance(ln.obj, dict):
|
|
439
477
|
continue
|
|
440
478
|
content = _content_list(ln.obj)
|
|
441
479
|
if not content:
|
|
@@ -487,7 +525,7 @@ def s_superseded_read_condense(lines, index, cfg, report):
|
|
|
487
525
|
transcript — that snapshot is stale; the later edit is the source of truth."""
|
|
488
526
|
touched = before = after = 0
|
|
489
527
|
for ln in lines:
|
|
490
|
-
if ln
|
|
528
|
+
if _protected(ln, index) or not isinstance(ln.obj, dict):
|
|
491
529
|
continue
|
|
492
530
|
content = _content_list(ln.obj)
|
|
493
531
|
if not content:
|
|
@@ -536,8 +574,9 @@ def s_repeat_reminder_fold(lines, index, cfg, report):
|
|
|
536
574
|
if key not in seen:
|
|
537
575
|
seen.add(key)
|
|
538
576
|
continue
|
|
539
|
-
# A later duplicate: fold it (only past the protected tail
|
|
540
|
-
|
|
577
|
+
# A later duplicate: fold it (only past the protected tail, and
|
|
578
|
+
# never a pinned line -- T-50/AC-26).
|
|
579
|
+
if _protected(ln, index):
|
|
541
580
|
continue
|
|
542
581
|
marker = _marker(txt)
|
|
543
582
|
if len(marker.encode("utf-8")) >= len(txt.encode("utf-8")):
|
|
@@ -576,7 +615,7 @@ def s_inline_image_evict(lines, index, cfg, report):
|
|
|
576
615
|
changed = True
|
|
577
616
|
return changed
|
|
578
617
|
for ln in lines:
|
|
579
|
-
if ln
|
|
618
|
+
if _protected(ln, index) or not isinstance(ln.obj, dict):
|
|
580
619
|
continue
|
|
581
620
|
content = _content_list(ln.obj)
|
|
582
621
|
if not content:
|
|
@@ -73,6 +73,16 @@ class SemanticEntry:
|
|
|
73
73
|
byte_size: int
|
|
74
74
|
tool_bearing: bool = False
|
|
75
75
|
marked: bool = False
|
|
76
|
+
# R-5 / AC-26 (#437, mode-plane-deterministic-flip): True for the entry
|
|
77
|
+
# carrying an injected persona (`_modelib.PERSONA_SENTINEL` -- set by the
|
|
78
|
+
# codec that builds SemanticEntry values, e.g. `_prunelib.build_index`,
|
|
79
|
+
# T-50). A pinned entry is retained at EVERY tier including aggressive,
|
|
80
|
+
# regardless of where it sits relative to the recent-turn protected tail
|
|
81
|
+
# -- see plan_prune below. Distinct from `marked`: `marked` means "already
|
|
82
|
+
# condensed by a prior pass" (still eligible to be LEFT alone, but not
|
|
83
|
+
# load-bearing the way a pin is); `pinned` means "must never be folded,
|
|
84
|
+
# condensed, or evicted," full stop.
|
|
85
|
+
pinned: bool = False
|
|
76
86
|
|
|
77
87
|
|
|
78
88
|
@dataclass(frozen=True)
|
|
@@ -198,19 +208,35 @@ def plan_prune(entries, policy):
|
|
|
198
208
|
if [entry.ordinal for entry in entries] != list(range(len(entries))):
|
|
199
209
|
raise ValueError("semantic entry ordinals must be contiguous and ordered")
|
|
200
210
|
boundary = protected_ordinal(entries, policy.keep_recent)
|
|
201
|
-
|
|
211
|
+
# AC-26: a pinned entry (the injected persona) is protected regardless of
|
|
212
|
+
# ordinal -- it need not sit in the recent-turn tail at all (the persona
|
|
213
|
+
# is typically injected once, early, and stays pinned for the rest of the
|
|
214
|
+
# session). Protection holds at EVERY tier, including aggressive: pinning
|
|
215
|
+
# is checked before tier selection even runs, so no strategy set can ever
|
|
216
|
+
# reach a pinned entry.
|
|
217
|
+
protected = tuple(entry.id for entry in entries
|
|
218
|
+
if entry.ordinal >= boundary or entry.pinned)
|
|
202
219
|
selected = select_strategies(policy.tier, policy.strategies)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
marked
|
|
220
|
+
# `candidates` is named once and reused for the actions, the counts and the
|
|
221
|
+
# audit code, because those three drifted apart the moment pinning arrived:
|
|
222
|
+
# `marked` excluded pinned entries while `boundary` still counted them, so
|
|
223
|
+
# a single pinned entry below the boundary capped `marked` at `boundary-1`
|
|
224
|
+
# and made CA-PRUNE-IDEMPOTENT unreachable — a fully condensed transcript
|
|
225
|
+
# reporting CA-PRUNE-PLAN forever. That is the normal case, not an edge:
|
|
226
|
+
# the persona is injected once, early, which puts a pinned entry below the
|
|
227
|
+
# boundary in essentially every governed session.
|
|
228
|
+
candidates = tuple(entry for entry in entries
|
|
229
|
+
if entry.ordinal < boundary and not entry.pinned)
|
|
230
|
+
actions = tuple((entry.id, _action_for(entry, selected, policy)) for entry in candidates)
|
|
231
|
+
marked = sum(1 for entry in candidates if entry.marked)
|
|
206
232
|
metrics = {
|
|
207
233
|
"entries_before": len(entries),
|
|
208
|
-
"candidate_entries":
|
|
234
|
+
"candidate_entries": len(candidates),
|
|
209
235
|
"protected_entries": len(protected),
|
|
210
236
|
"marked_candidates": marked,
|
|
211
237
|
}
|
|
212
|
-
audit_codes = (("CA-PRUNE-NOOP",) if
|
|
213
|
-
else ("CA-PRUNE-IDEMPOTENT",) if marked ==
|
|
238
|
+
audit_codes = (("CA-PRUNE-NOOP",) if not candidates
|
|
239
|
+
else ("CA-PRUNE-IDEMPOTENT",) if marked == len(candidates)
|
|
214
240
|
else ("CA-PRUNE-PLAN",))
|
|
215
241
|
fingerprint_source = {
|
|
216
242
|
"boundary": boundary,
|
|
@@ -827,7 +827,7 @@ def governing_docs(rel, index, runner=None):
|
|
|
827
827
|
# ---------------------------------------------------------------------------
|
|
828
828
|
|
|
829
829
|
|
|
830
|
-
def marker_path(root, session_id, rel):
|
|
830
|
+
def marker_path(root, session_id, rel, prefix="readinject-"):
|
|
831
831
|
"""Return the absolute path of the dedup marker for (session_id, rel).
|
|
832
832
|
|
|
833
833
|
The marker lives under <root>/.codearbiter/.markers/ with a filename
|
|
@@ -835,9 +835,15 @@ def marker_path(root, session_id, rel):
|
|
|
835
835
|
null-byte separator ensures ('ab', 'c') and ('a', 'bc') hash to different
|
|
836
836
|
filenames.
|
|
837
837
|
|
|
838
|
+
`prefix` selects the marker namespace within the shared .markers/
|
|
839
|
+
directory. Defaults to 'readinject-' so every existing caller is
|
|
840
|
+
unaffected; a second consumer (e.g. mode-flip injection) can pass
|
|
841
|
+
prefix='modeinject-' to keep its markers distinct from the 790+
|
|
842
|
+
readinject- markers already on disk.
|
|
843
|
+
|
|
838
844
|
PURE — no filesystem access of any kind. Inputs are coerced to str so any
|
|
839
845
|
type is accepted. Never raises; on the (essentially impossible) error path,
|
|
840
|
-
returns a fallback path whose last segment is '
|
|
846
|
+
returns a fallback path whose last segment is '<prefix>error.marker'
|
|
841
847
|
which will not match any normally-written marker.
|
|
842
848
|
"""
|
|
843
849
|
try:
|
|
@@ -848,11 +854,11 @@ def marker_path(root, session_id, rel):
|
|
|
848
854
|
str(root),
|
|
849
855
|
".codearbiter",
|
|
850
856
|
".markers",
|
|
851
|
-
"
|
|
857
|
+
"{}{}.marker".format(prefix, digest),
|
|
852
858
|
)
|
|
853
859
|
except Exception: # noqa: BLE001
|
|
854
860
|
return os.path.join(
|
|
855
|
-
str(root), ".codearbiter", ".markers", "
|
|
861
|
+
str(root), ".codearbiter", ".markers", "{}error.marker".format(prefix)
|
|
856
862
|
)
|
|
857
863
|
|
|
858
864
|
|