@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
|
@@ -27,11 +27,114 @@
|
|
|
27
27
|
# Behavioral contract for M1: under Claude Code, everything routed through
|
|
28
28
|
# this seam resolves to byte-identical results as the pre-seam inline code.
|
|
29
29
|
|
|
30
|
+
import json
|
|
30
31
|
import os
|
|
32
|
+
import re
|
|
31
33
|
import subprocess
|
|
32
34
|
import sys
|
|
33
35
|
|
|
34
|
-
from _gitexec import git_executable
|
|
36
|
+
from _gitexec import git_executable, root_bound_git_env
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PluginRootError(RuntimeError):
|
|
40
|
+
"""A host adapter's installed package boundary cannot be authenticated."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_VERSION_RE = re.compile(
|
|
44
|
+
r"^\d+\.\d+\.\d+(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?$")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _safe_relative_path(value, label):
|
|
48
|
+
"""Reject an absolute or traversing adapter-relative path before joining."""
|
|
49
|
+
if not isinstance(value, str) or not value:
|
|
50
|
+
raise PluginRootError(f"{label} must be a non-empty relative path")
|
|
51
|
+
normalized = os.path.normpath(value)
|
|
52
|
+
if (os.path.isabs(value) or normalized == os.pardir or
|
|
53
|
+
normalized.startswith(os.pardir + os.sep)):
|
|
54
|
+
raise PluginRootError(f"{label} escapes the adapter: {value!r}")
|
|
55
|
+
return normalized
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _inside(root, candidate):
|
|
59
|
+
"""Whether normalized ``candidate`` is contained by normalized ``root``."""
|
|
60
|
+
try:
|
|
61
|
+
return os.path.commonpath((root, candidate)) == root
|
|
62
|
+
except ValueError: # distinct Windows volumes are never contained
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def resolve_plugin_root(authority_file, *, adapter_name, adapter_version, manifest_relpath,
|
|
67
|
+
anchor_relpath, environment=None, signal_names=(),
|
|
68
|
+
required_signal_names=(), legacy_signal_names=()):
|
|
69
|
+
"""Return the authenticated root of the executing adapter, or fail closed.
|
|
70
|
+
|
|
71
|
+
The authority is the file/module that is already executing. Environment
|
|
72
|
+
variables may only corroborate that exact real path; none can select a
|
|
73
|
+
different, merely plausible package. ``anchor_relpath`` and the manifest
|
|
74
|
+
are constrained inside that root so traversal and symlink escapes cannot
|
|
75
|
+
authenticate an external file.
|
|
76
|
+
"""
|
|
77
|
+
if not isinstance(adapter_name, str) or not adapter_name:
|
|
78
|
+
raise PluginRootError("adapter name must be a non-empty string")
|
|
79
|
+
if not isinstance(adapter_version, str) or not _VERSION_RE.fullmatch(adapter_version):
|
|
80
|
+
raise PluginRootError("adapter version must be an exact SemVer string")
|
|
81
|
+
manifest_relpath = _safe_relative_path(manifest_relpath, "manifest path")
|
|
82
|
+
anchor_relpath = _safe_relative_path(anchor_relpath, "anchor path")
|
|
83
|
+
env = os.environ if environment is None else environment
|
|
84
|
+
|
|
85
|
+
# Keep the package boundary lexical until after its parent directories are
|
|
86
|
+
# derived. If hooks/hostapi.py itself is a symlink, realpathing it first
|
|
87
|
+
# would re-home this adapter in a complete, matching foreign package.
|
|
88
|
+
lexical_authority = os.path.abspath(authority_file)
|
|
89
|
+
root = os.path.realpath(os.path.dirname(os.path.dirname(lexical_authority)))
|
|
90
|
+
source = os.path.realpath(lexical_authority)
|
|
91
|
+
anchor = os.path.realpath(os.path.join(root, anchor_relpath))
|
|
92
|
+
if (not _inside(root, source) or not _inside(root, anchor) or
|
|
93
|
+
anchor != source or not os.path.isfile(anchor)):
|
|
94
|
+
raise PluginRootError(
|
|
95
|
+
f"executing adapter anchor is outside or missing from its package: {source}")
|
|
96
|
+
|
|
97
|
+
manifest = os.path.realpath(os.path.join(root, manifest_relpath))
|
|
98
|
+
if not _inside(root, manifest) or not os.path.isfile(manifest):
|
|
99
|
+
raise PluginRootError(
|
|
100
|
+
f"{adapter_name}: required manifest is missing or outside the adapter: {manifest}")
|
|
101
|
+
try:
|
|
102
|
+
with open(manifest, encoding="utf-8") as handle:
|
|
103
|
+
data = json.load(handle)
|
|
104
|
+
except (OSError, ValueError) as error:
|
|
105
|
+
raise PluginRootError(
|
|
106
|
+
f"{adapter_name}: required manifest is unreadable: {manifest} ({error})") from error
|
|
107
|
+
name = data.get("name") if isinstance(data, dict) else None
|
|
108
|
+
version = data.get("version") if isinstance(data, dict) else None
|
|
109
|
+
if name != adapter_name:
|
|
110
|
+
raise PluginRootError(
|
|
111
|
+
f"adapter manifest mismatch at {manifest}: expected {adapter_name!r}, got {name!r}")
|
|
112
|
+
if not isinstance(version, str) or not _VERSION_RE.fullmatch(version):
|
|
113
|
+
raise PluginRootError(
|
|
114
|
+
f"adapter manifest has invalid version at {manifest}: {version!r}")
|
|
115
|
+
if version != adapter_version:
|
|
116
|
+
raise PluginRootError(
|
|
117
|
+
f"adapter manifest version mismatch at {manifest}: "
|
|
118
|
+
f"expected {adapter_version!r}, got {version!r}")
|
|
119
|
+
|
|
120
|
+
for signal in required_signal_names:
|
|
121
|
+
if not env.get(signal):
|
|
122
|
+
raise PluginRootError(
|
|
123
|
+
f"{adapter_name}: required {signal} is absent; executing root is {root}")
|
|
124
|
+
for signal in signal_names:
|
|
125
|
+
value = env.get(signal)
|
|
126
|
+
if not value:
|
|
127
|
+
continue
|
|
128
|
+
claimed = os.path.realpath(os.path.abspath(value))
|
|
129
|
+
if claimed != root:
|
|
130
|
+
raise PluginRootError(
|
|
131
|
+
f"{adapter_name}: {signal} root {claimed} disagrees with "
|
|
132
|
+
f"executing adapter root {root}")
|
|
133
|
+
if signal in legacy_signal_names:
|
|
134
|
+
sys.stderr.write(
|
|
135
|
+
f"codeArbiter: {signal} is deprecated for {adapter_name}; "
|
|
136
|
+
"accepted only as matching corroboration.\n")
|
|
137
|
+
return root
|
|
35
138
|
|
|
36
139
|
|
|
37
140
|
def git_toplevel(cwd=None):
|
|
@@ -52,7 +155,7 @@ def git_toplevel(cwd=None):
|
|
|
52
155
|
try:
|
|
53
156
|
out = subprocess.run(
|
|
54
157
|
args, capture_output=True, text=True, encoding="utf-8",
|
|
55
|
-
errors="replace", timeout=5,
|
|
158
|
+
errors="replace", timeout=5, env=root_bound_git_env(),
|
|
56
159
|
)
|
|
57
160
|
if out.returncode == 0:
|
|
58
161
|
top = out.stdout.strip()
|
|
@@ -63,6 +166,38 @@ def git_toplevel(cwd=None):
|
|
|
63
166
|
return None
|
|
64
167
|
|
|
65
168
|
|
|
169
|
+
def _root_bound_git_env():
|
|
170
|
+
"""Compatibility seam for callers of the former local helper."""
|
|
171
|
+
return root_bound_git_env()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _has_enabled_context(root):
|
|
175
|
+
"""Whether a real CONTEXT.md satisfies the canonical activation parser."""
|
|
176
|
+
canonical_root = os.path.realpath(root)
|
|
177
|
+
state_dir = os.path.join(canonical_root, ".codearbiter")
|
|
178
|
+
if os.path.islink(state_dir) or not os.path.isdir(state_dir):
|
|
179
|
+
return False
|
|
180
|
+
context = os.path.join(state_dir, "CONTEXT.md")
|
|
181
|
+
canonical_context = os.path.realpath(context)
|
|
182
|
+
try:
|
|
183
|
+
contained = os.path.normcase(os.path.commonpath(
|
|
184
|
+
[canonical_root, canonical_context])) == os.path.normcase(canonical_root)
|
|
185
|
+
except (TypeError, ValueError):
|
|
186
|
+
contained = False
|
|
187
|
+
if (not contained or os.path.islink(context)
|
|
188
|
+
or not os.path.isfile(canonical_context)):
|
|
189
|
+
return False
|
|
190
|
+
try:
|
|
191
|
+
# Deferred to avoid hostapi <-> _activationlib's import-time cycle.
|
|
192
|
+
# This must remain the single parser for the activation contract,
|
|
193
|
+
# including its accepted UTF-8 BOM spelling.
|
|
194
|
+
from _activationlib import frontmatter_enabled
|
|
195
|
+
enabled, malformed = frontmatter_enabled(canonical_context)
|
|
196
|
+
return enabled and not malformed
|
|
197
|
+
except Exception: # noqa: BLE001 - root selection must fail closed
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
|
|
66
201
|
def git_worktree_main_root(root):
|
|
67
202
|
"""When `root` (an already-resolved project root — a `project_root()`
|
|
68
203
|
answer, NOT necessarily a fresh `git_toplevel` call) is itself the
|
|
@@ -100,24 +235,71 @@ def git_worktree_main_root(root):
|
|
|
100
235
|
FILE, but only a worktree's `gitdir:` pointer names a path under
|
|
101
236
|
`.git/worktrees/<name>`; a submodule's names `.git/modules/<name>`, which
|
|
102
237
|
is not a "main root" to climb to and must fall through untouched (mirrors
|
|
103
|
-
`_durabilitylib._gitfile_points_at_worktree`'s same distinction).
|
|
238
|
+
`_durabilitylib._gitfile_points_at_worktree`'s same distinction).
|
|
239
|
+
|
|
240
|
+
The selected Git binary is the authority for accepting and resolving the
|
|
241
|
+
worktree metadata. That keeps native relative pointers aligned with Git,
|
|
242
|
+
rejects incomplete or foreign-dialect metadata Git itself cannot use, and
|
|
243
|
+
avoids translating a path grammar in Python. Only Git-confirmed linked
|
|
244
|
+
worktrees whose common directory is the main checkout's `.git` directory,
|
|
245
|
+
and whose linked and reported-main checkouts are both independently
|
|
246
|
+
arbiter-enabled, can escalate the marker root."""
|
|
104
247
|
git_meta = os.path.join(root, ".git")
|
|
105
248
|
if not os.path.isfile(git_meta):
|
|
106
249
|
return None
|
|
107
250
|
try:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
251
|
+
probe = subprocess.run(
|
|
252
|
+
[git_executable(), "-C", root, "rev-parse", "--path-format=absolute",
|
|
253
|
+
"--git-dir", "--git-common-dir"],
|
|
254
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
255
|
+
timeout=5, env=_root_bound_git_env(),
|
|
256
|
+
)
|
|
257
|
+
except Exception: # noqa: BLE001 - root selection must fail closed
|
|
258
|
+
return None
|
|
259
|
+
lines = [line.strip() for line in probe.stdout.splitlines() if line.strip()]
|
|
260
|
+
if probe.returncode != 0 or len(lines) != 2:
|
|
261
|
+
return None
|
|
262
|
+
git_dir = os.path.normpath(lines[0])
|
|
263
|
+
common_dir = os.path.normpath(lines[1])
|
|
264
|
+
if not os.path.isabs(git_dir) or not os.path.isabs(common_dir):
|
|
265
|
+
return None
|
|
266
|
+
real_git_dir = os.path.normcase(os.path.realpath(git_dir))
|
|
267
|
+
real_common_dir = os.path.normcase(os.path.realpath(common_dir))
|
|
268
|
+
if real_git_dir == real_common_dir:
|
|
269
|
+
return None # ordinary checkout or submodule, not a linked worktree
|
|
270
|
+
expected_admin_parent = os.path.normcase(
|
|
271
|
+
os.path.realpath(os.path.join(common_dir, "worktrees")))
|
|
272
|
+
if os.path.normcase(os.path.realpath(os.path.dirname(git_dir))) != expected_admin_parent:
|
|
273
|
+
return None
|
|
274
|
+
if os.path.basename(common_dir) != ".git" or not os.path.isdir(common_dir):
|
|
275
|
+
return None
|
|
276
|
+
main_root = os.path.dirname(common_dir)
|
|
277
|
+
if not os.path.isdir(main_root):
|
|
278
|
+
return None
|
|
279
|
+
try:
|
|
280
|
+
main_probe = subprocess.run(
|
|
281
|
+
[git_executable(), "-C", main_root, "rev-parse", "--path-format=absolute",
|
|
282
|
+
"--show-toplevel", "--git-common-dir"],
|
|
283
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
284
|
+
timeout=5, env=_root_bound_git_env(),
|
|
285
|
+
)
|
|
286
|
+
except Exception: # noqa: BLE001 - root selection must fail closed
|
|
287
|
+
return None
|
|
288
|
+
main_lines = [line.strip() for line in main_probe.stdout.splitlines() if line.strip()]
|
|
289
|
+
if main_probe.returncode != 0 or len(main_lines) != 2:
|
|
290
|
+
return None
|
|
291
|
+
main_toplevel = os.path.normpath(main_lines[0])
|
|
292
|
+
confirmed_common = os.path.normpath(main_lines[1])
|
|
293
|
+
if not os.path.isabs(main_toplevel) or not os.path.isabs(confirmed_common):
|
|
294
|
+
return None
|
|
295
|
+
if os.path.normcase(os.path.realpath(main_toplevel)) != os.path.normcase(
|
|
296
|
+
os.path.realpath(main_root)):
|
|
111
297
|
return None
|
|
112
|
-
if
|
|
298
|
+
if os.path.normcase(os.path.realpath(confirmed_common)) != real_common_dir:
|
|
113
299
|
return None
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if idx == -1:
|
|
118
|
-
return None # not a linked worktree (e.g. a submodule) — nothing to climb to
|
|
119
|
-
main_git_dir = gitdir[:idx + len("/.git")]
|
|
120
|
-
return os.path.dirname(main_git_dir) or None
|
|
300
|
+
if not _has_enabled_context(root) or not _has_enabled_context(main_root):
|
|
301
|
+
return None
|
|
302
|
+
return main_root.replace("\\", "/") if os.name == "nt" else main_root
|
|
121
303
|
|
|
122
304
|
|
|
123
305
|
class Host:
|
|
@@ -130,6 +312,17 @@ class Host:
|
|
|
130
312
|
"""
|
|
131
313
|
|
|
132
314
|
name = "claude"
|
|
315
|
+
adapter_name = "ca"
|
|
316
|
+
adapter_version = "2.17.0"
|
|
317
|
+
|
|
318
|
+
# Update-notifier descriptor. Each independently versioned host overrides
|
|
319
|
+
# these three values in its per-plugin _host.py. Keeping the target,
|
|
320
|
+
# release prefix, and remediation command together on the active Host
|
|
321
|
+
# prevents the shared notifier from comparing or instructing for a sibling
|
|
322
|
+
# product line (RA-02).
|
|
323
|
+
update_target = "ca"
|
|
324
|
+
update_tag_prefix = "v"
|
|
325
|
+
update_command = "/plugin marketplace update codearbiter"
|
|
133
326
|
|
|
134
327
|
# Capability flags — what surfaces this host actually has. A hook that
|
|
135
328
|
# heals/queries a statusline gates on has_statusline; a hook registered
|
|
@@ -220,14 +413,16 @@ class Host:
|
|
|
220
413
|
return git_worktree_main_root(root) or root
|
|
221
414
|
|
|
222
415
|
def plugin_root(self):
|
|
223
|
-
"""The
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
416
|
+
"""The authenticated payload root derived from this executing module.
|
|
417
|
+
|
|
418
|
+
CLAUDE_PLUGIN_ROOT may corroborate that derived root, but can never
|
|
419
|
+
select a different adapter package.
|
|
420
|
+
"""
|
|
421
|
+
return resolve_plugin_root(
|
|
422
|
+
__file__, adapter_name=self.adapter_name, adapter_version=self.adapter_version,
|
|
423
|
+
manifest_relpath=self.manifest_relpath(), anchor_relpath="hooks/hostapi.py",
|
|
424
|
+
signal_names=("CLAUDE_PLUGIN_ROOT",),
|
|
425
|
+
)
|
|
231
426
|
|
|
232
427
|
def manifest_relpath(self):
|
|
233
428
|
"""The plugin manifest's path, relative to plugin_root() (#263,
|
|
@@ -402,6 +597,9 @@ class FailClosedHost(Host):
|
|
|
402
597
|
silent (observability-002)."""
|
|
403
598
|
|
|
404
599
|
name = "unknown"
|
|
600
|
+
update_target = None
|
|
601
|
+
update_tag_prefix = None
|
|
602
|
+
update_command = None
|
|
405
603
|
has_statusline = False
|
|
406
604
|
has_read_tool = False
|
|
407
605
|
has_prunable_transcript = False
|
|
@@ -20,6 +20,7 @@ from _prunepolicy import PrunePolicy, SemanticEntry, plan_prune # noqa: E402
|
|
|
20
20
|
import _arbiterstatelib # noqa: E402
|
|
21
21
|
import _hooklib # noqa: E402
|
|
22
22
|
import _ledgerlib # noqa: E402
|
|
23
|
+
import _modelib # noqa: E402
|
|
23
24
|
import _planfilelib # noqa: E402
|
|
24
25
|
import _segmentslib # noqa: E402
|
|
25
26
|
import _taskboardlib # noqa: E402
|
|
@@ -402,11 +403,16 @@ def _footer_status_snapshot(request):
|
|
|
402
403
|
prune = _bounded_footer_text(_segmentslib.seg_prune({}, session_id), FOOTER_MAX_PRUNE)
|
|
403
404
|
except Exception: # noqa: BLE001 - prune is an independent optional segment
|
|
404
405
|
prune = None
|
|
406
|
+
# #437: the mode plane replaced the `dev-active` presence check. The
|
|
407
|
+
# marker is NOT dual-written, so a reader still probing for it would
|
|
408
|
+
# report inactive forever the moment nothing writes it. `current_mode`
|
|
409
|
+
# resolves through marker_root, matching every other `.markers/` reader
|
|
410
|
+
# — a linked worktree must not read a different file (#604).
|
|
405
411
|
try:
|
|
406
|
-
|
|
407
|
-
except Exception: # noqa: BLE001 -
|
|
412
|
+
mode = _arbiterstatelib.current_mode(session_id, root=request["cwd"])
|
|
413
|
+
except Exception: # noqa: BLE001 - mode is a fail-soft display fact
|
|
408
414
|
return unavailable
|
|
409
|
-
if
|
|
415
|
+
if mode not in _modelib.MODES:
|
|
410
416
|
return unavailable
|
|
411
417
|
return {
|
|
412
418
|
"version": 1,
|
|
@@ -419,7 +425,7 @@ def _footer_status_snapshot(request):
|
|
|
419
425
|
"questions": counts[1],
|
|
420
426
|
"overrides": counts[2],
|
|
421
427
|
"sprint": sprint,
|
|
422
|
-
"
|
|
428
|
+
"mode": mode,
|
|
423
429
|
"prune": prune,
|
|
424
430
|
}},
|
|
425
431
|
}
|