arkaos 4.14.3 → 4.14.4

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/VERSION CHANGED
@@ -1 +1 @@
1
- 4.14.3
1
+ 4.14.4
@@ -3,33 +3,49 @@
3
3
  "generator": "core/agents/roster_manifest.py",
4
4
  "purpose": "every specialist-gate owner resolves to a dispatchable agent source; installer/skill-deploy.js deploys these"
5
5
  },
6
+ "aliases": {
7
+ "andre": "backend-dev",
8
+ "bruno": "security-eng",
9
+ "carlos": "devops-eng",
10
+ "diana": "frontend-dev",
11
+ "gabriel": "architect",
12
+ "vasco": "dba"
13
+ },
14
+ "ambiguous_first_names": [],
6
15
  "gate_owners": {
7
16
  "architect": {
8
17
  "compiled": false,
18
+ "human_name": "Gabriel",
9
19
  "source": "departments/dev/agents/architect.md"
10
20
  },
11
21
  "backend-dev": {
12
22
  "compiled": true,
23
+ "human_name": "Andre",
13
24
  "source": "config/claude-agents/backend-dev.md"
14
25
  },
15
26
  "dba": {
16
27
  "compiled": true,
28
+ "human_name": "Vasco",
17
29
  "source": "config/claude-agents/dba.md"
18
30
  },
19
31
  "devops-eng": {
20
32
  "compiled": true,
33
+ "human_name": "Carlos",
21
34
  "source": "config/claude-agents/devops-eng.md"
22
35
  },
23
36
  "frontend-dev": {
24
37
  "compiled": false,
38
+ "human_name": "Diana",
25
39
  "source": "departments/dev/agents/frontend-dev.md"
26
40
  },
27
41
  "security-eng": {
28
42
  "compiled": true,
43
+ "human_name": "Bruno",
29
44
  "source": "config/claude-agents/security-eng.md"
30
45
  },
31
46
  "senior-dev": {
32
47
  "compiled": false,
48
+ "human_name": "",
33
49
  "source": "departments/dev/agents/senior-dev.md"
34
50
  }
35
51
  }
@@ -0,0 +1,205 @@
1
+ """Authority brief — the write rules a session must know BEFORE it writes.
2
+
3
+ The 2026-07-12 incident: a session was blocked writing a Laravel
4
+ controller and, never having been told the rule existed, invented a
5
+ "documented bug" to justify a bypass. Grep confirms the gap: ownership
6
+ and ``specialist-bypass`` appear NOWHERE in CLAUDE.md, arka/SKILL.md,
7
+ flow/SKILL.md or constitution.yaml. The deny message was the first and
8
+ only place a session ever learned any of it.
9
+
10
+ This module renders the ``[ARKA:AUTHORITY]`` block the SessionStart hook
11
+ injects. Everything is GENERATED from ``config/agent-ownership.yaml`` +
12
+ ``config/agent-roster.json`` + the agents actually deployed on THIS
13
+ machine — never hand-typed.
14
+
15
+ Two rules this file learned the hard way (QG, redo 1):
16
+
17
+ 1. **Relevance is decided by the FULL literal spine, never its first
18
+ segment.** The first cut matched ``**/app/Http/Controllers/**`` in
19
+ this repo because a directory called ``dashboard/app`` exists — so it
20
+ taught a session five Laravel rules that govern nothing here.
21
+ 2. **Never truncate in silence.** The same cut capped the list at 8 and
22
+ dropped ``core/workflow/**/*.py`` and ``core/agents/**/*.py`` — the
23
+ rules that own the very files that PR was editing — under a heading
24
+ that claims "Owned paths in THIS project". A brief that hides the
25
+ rules that apply is the incident, re-delivered as its own fix.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import json
31
+ import re
32
+ from pathlib import Path
33
+
34
+ import yaml
35
+
36
+ REPO_ROOT = Path(__file__).resolve().parents[2]
37
+ OWNERSHIP_YAML = REPO_ROOT / "config" / "agent-ownership.yaml"
38
+ ROSTER_JSON = REPO_ROOT / "config" / "agent-roster.json"
39
+
40
+ # Enough to state every rule that really applies to a project this size
41
+ # (arka-os itself renders 14). Anything beyond is COUNTED, never dropped.
42
+ _MAX_RULES = 16
43
+ _MAX_FIELD = 120
44
+ # `{` is a glob char too: the enforcer expands `{core,dashboard}` braces,
45
+ # so treating that segment as a literal spine would make the brief DROP a
46
+ # rule the gate enforces — the "brief hides an applicable rule" incident.
47
+ _GLOB_CHARS = ("*", "?", "[", "{")
48
+ # The brief lands in a system prompt. A pattern is operator-editable YAML
49
+ # (agent-ownership.yaml is `lead_allowed`), so a newline in it could forge
50
+ # an authority line — OWASP LLM01. Control bytes never reach the prompt.
51
+ _CONTROL_RE = re.compile(r"[\x00-\x1f\x7f]")
52
+
53
+
54
+ def _load(path: Path) -> dict:
55
+ try:
56
+ text = path.read_text(encoding="utf-8")
57
+ except OSError:
58
+ return {}
59
+ try:
60
+ data = (
61
+ json.loads(text) if path.suffix == ".json" else yaml.safe_load(text)
62
+ )
63
+ except (json.JSONDecodeError, yaml.YAMLError):
64
+ return {}
65
+ return data if isinstance(data, dict) else {}
66
+
67
+
68
+ def _clean(value: str) -> str:
69
+ """Strip control bytes and bound the length — prompt-injection guard."""
70
+ return _CONTROL_RE.sub("", str(value))[:_MAX_FIELD]
71
+
72
+
73
+ def deployed_agents(agents_dirs: list[Path] | None = None) -> set[str]:
74
+ """Slugs the Task tool can actually dispatch on this machine."""
75
+ dirs = agents_dirs or [
76
+ Path.home() / ".claude" / "agents",
77
+ Path.cwd() / ".claude" / "agents",
78
+ ]
79
+ found: set[str] = set()
80
+ for base in dirs:
81
+ try:
82
+ for md in base.glob("*.md"):
83
+ found.add(md.stem.removeprefix("arka-"))
84
+ except OSError:
85
+ continue
86
+ return found
87
+
88
+
89
+ def _spine(pattern: str) -> list[str]:
90
+ """The literal path segments of a glob: `**/app/Http/Controllers/**`
91
+ -> ['app', 'Http', 'Controllers']; `**/*.vue` -> []."""
92
+ return [
93
+ part for part in pattern.split("/")
94
+ if part and not any(ch in part for ch in _GLOB_CHARS)
95
+ ]
96
+
97
+
98
+ def rule_applies(pattern: str, root: Path) -> bool:
99
+ """Does this rule govern anything in `root`?
100
+
101
+ The FULL spine must exist — checking only its first segment is what
102
+ made `**/app/Http/Controllers/**` look relevant here (a `dashboard/app`
103
+ directory exists) while the rules that actually govern the repo were
104
+ pushed out of the list. Extension-only rules always apply: they cost
105
+ one line and are honest ("write a .vue and frontend-dev owns it").
106
+ """
107
+ spine = _spine(pattern)
108
+ if not spine:
109
+ return True
110
+ joined = Path(*spine)
111
+ try:
112
+ if (root / joined).exists():
113
+ return True
114
+ # One level down covers monorepo layouts (dashboard/app/…).
115
+ return any(
116
+ (child / joined).exists()
117
+ for child in root.iterdir()
118
+ if child.is_dir() and not child.name.startswith(".")
119
+ )
120
+ except OSError:
121
+ return False
122
+
123
+
124
+ def applicable_rules(rules: list[dict], root: Path) -> list[dict]:
125
+ return [
126
+ rule for rule in rules
127
+ if isinstance(rule, dict)
128
+ and rule.get("pattern")
129
+ and rule.get("owners")
130
+ and "*" not in rule["owners"]
131
+ and rule_applies(str(rule["pattern"]), root)
132
+ ]
133
+
134
+
135
+ def _dispatch_status(named: set[str], owners_meta: dict,
136
+ dispatchable: set[str]) -> list[str]:
137
+ status: list[str] = []
138
+ for slug in sorted(named):
139
+ # agent-roster.json is operator-editable and this line lands in a
140
+ # system prompt — human_name walks through _clean like every other
141
+ # field (QG redo 2: it was the one field that did not).
142
+ tokens = _clean(owners_meta.get(slug, {}).get("human_name", "")).split()
143
+ label = f"{slug} ({tokens[0]})" if tokens else slug
144
+ status.append(
145
+ f"{label} ok" if slug in dispatchable else f"{label} MISSING"
146
+ )
147
+ return status
148
+
149
+
150
+ def render(cwd: str | Path | None = None,
151
+ agents_dirs: list[Path] | None = None) -> str:
152
+ """The [ARKA:AUTHORITY] block, or "" when there is nothing to say."""
153
+ ownership = _load(OWNERSHIP_YAML)
154
+ roster = _load(ROSTER_JSON)
155
+ rules = ownership.get("ownership") or []
156
+ owners_meta = roster.get("gate_owners") or {}
157
+ if not isinstance(rules, list) or not owners_meta:
158
+ return ""
159
+
160
+ root = Path(cwd) if cwd else Path.cwd()
161
+ relevant = applicable_rules(rules, root)
162
+ if not relevant:
163
+ return ""
164
+ shown, overflow = relevant[:_MAX_RULES], len(relevant) - _MAX_RULES
165
+
166
+ lines = [
167
+ "[ARKA:AUTHORITY] Write permissions (enforced by a PreToolUse gate)",
168
+ "1. Your last [arka:routing] / [arka:dispatch] marker is an "
169
+ "AUTHORIZATION TOKEN a gate reads before every Write/Edit. It is "
170
+ "not narration.",
171
+ "2. Owned paths in THIS project (first match wins):",
172
+ ]
173
+ named: set[str] = set()
174
+ for rule in shown:
175
+ owners = [_clean(o) for o in rule["owners"]]
176
+ named.update(owners)
177
+ lines.append(
178
+ f" {_clean(rule['pattern']):<26s} -> {', '.join(owners)}"
179
+ )
180
+ if overflow > 0:
181
+ lines.append(
182
+ f" (+{overflow} more — see config/agent-ownership.yaml)"
183
+ )
184
+
185
+ status = _dispatch_status(named, owners_meta, deployed_agents(agents_dirs))
186
+ lines.append(
187
+ f"3. Dispatchable here (Task subagent_type): {', '.join(status)}"
188
+ )
189
+ if any(entry.endswith("MISSING") for entry in status):
190
+ lines.append(" MISSING -> run `npx arkaos update` (never bypass).")
191
+ lines.append(
192
+ "4. The dispatched specialist writes with NO block. Retrying a "
193
+ "blocked Write never works — dispatch instead."
194
+ )
195
+ return "\n".join(lines)
196
+
197
+
198
+ def main() -> int:
199
+ brief = render()
200
+ print(brief or "(no ownership rules apply to this directory)")
201
+ return 0
202
+
203
+
204
+ if __name__ == "__main__":
205
+ raise SystemExit(main())
@@ -68,6 +68,55 @@ def _resolve_source(slug: str) -> Path | None:
68
68
  return None
69
69
 
70
70
 
71
+ def _agent_yaml(slug: str) -> Path | None:
72
+ matches = sorted(DEPARTMENTS_DIR.glob(f"*/agents/**/{slug}.yaml"))
73
+ return matches[0] if matches else None
74
+
75
+
76
+ def _human_name(slug: str) -> str:
77
+ source = _agent_yaml(slug)
78
+ if source is None:
79
+ return ""
80
+ try:
81
+ data = yaml.safe_load(source.read_text(encoding="utf-8"))
82
+ except yaml.YAMLError:
83
+ return ""
84
+ return str(data.get("name") or "") if isinstance(data, dict) else ""
85
+
86
+
87
+ def _first_name(human: str) -> str:
88
+ """'Andre — Backend Core Lead' -> 'andre'."""
89
+ return human.split()[0].split("—")[0].strip().lower() if human else ""
90
+
91
+
92
+ def build_aliases(entries: dict[str, dict]) -> dict[str, str]:
93
+ """First-name -> owner slug, resolved WITHIN the gate-owner set.
94
+
95
+ Sessions route by human name (``[arka:dispatch] paulo -> diana``) but
96
+ ownership rules name slugs (``frontend-dev``). Unresolved, that
97
+ mismatch blocked the RIGHT specialist from her own files: 22 of 189
98
+ measured blocks. (The 22 `senior-dev` blocks are a separate, still
99
+ open class: there the slug IS the persona, so no alias helps.)
100
+
101
+ Scoping to gate owners is what makes this safe. Globally, ``diana``
102
+ is ambiguous (frontend-dev AND hr-specialist) and so is ``andre``
103
+ (backend-dev AND growth-engineer) — a naive first-name alias would
104
+ trade one bug for a worse one. Among the owners the gate can
105
+ actually demand, both are unique. Anything still ambiguous here is
106
+ REFUSED, never guessed.
107
+ """
108
+ by_first: dict[str, set[str]] = {}
109
+ for slug in entries:
110
+ first = _first_name(_human_name(slug))
111
+ if first:
112
+ by_first.setdefault(first, set()).add(slug)
113
+ return {
114
+ first: next(iter(slugs))
115
+ for first, slugs in sorted(by_first.items())
116
+ if len(slugs) == 1
117
+ }
118
+
119
+
71
120
  def build_roster() -> dict:
72
121
  owners = gate_owners()
73
122
  entries: dict[str, dict] = {}
@@ -80,6 +129,7 @@ def build_roster() -> dict:
80
129
  entries[slug] = {
81
130
  "source": str(source.relative_to(REPO_ROOT)),
82
131
  "compiled": source.parent == COMPILED_DIR,
132
+ "human_name": _human_name(slug),
83
133
  }
84
134
  if ghosts:
85
135
  raise ValueError(
@@ -88,6 +138,14 @@ def build_roster() -> dict:
88
138
  f"config/agent-ownership.yaml; a gate must never demand an "
89
139
  f"owner nobody can invoke"
90
140
  )
141
+ aliases = build_aliases(entries)
142
+ ambiguous = sorted(
143
+ first
144
+ for first in {
145
+ _first_name(e["human_name"]) for e in entries.values()
146
+ }
147
+ if first and first not in aliases
148
+ )
91
149
  return {
92
150
  "_meta": {
93
151
  "generator": "core/agents/roster_manifest.py",
@@ -97,6 +155,8 @@ def build_roster() -> dict:
97
155
  ),
98
156
  },
99
157
  "gate_owners": entries,
158
+ "aliases": aliases,
159
+ "ambiguous_first_names": ambiguous,
100
160
  }
101
161
 
102
162
 
@@ -242,6 +242,30 @@ def _ensure_dashboard(repo: str, config: dict) -> None:
242
242
  )
243
243
 
244
244
 
245
+ def _authority_brief(cwd: str) -> str:
246
+ """[ARKA:AUTHORITY] — who may write what, BEFORE the first Write.
247
+
248
+ The 2026-07-12 incident happened because this information existed
249
+ nowhere in the prompt surface: a session met the ownership rule for
250
+ the first time in a deny message, and rationalized a bypass instead
251
+ of dispatching. Generated per project, never hand-typed.
252
+ """
253
+ try:
254
+ from core.agents.authority_brief import render
255
+ brief = render(cwd)
256
+ except Exception as exc:
257
+ # But it must not fail SILENTLY either: a swallowed error here
258
+ # restores the exact pre-incident state (a session writing with no
259
+ # idea the rules exist). One honest line beats a silent void.
260
+ return (
261
+ f"\n\n[ARKA:AUTHORITY] unavailable ({type(exc).__name__}) — "
262
+ f"write rules could not be rendered; check "
263
+ f"config/agent-ownership.yaml. Dispatch the owning specialist "
264
+ f"rather than assuming you may write."
265
+ )
266
+ return f"\n\n{brief}" if brief else ""
267
+
268
+
245
269
  def build_message(cwd: str) -> str:
246
270
  repo = repo_path()
247
271
  config = _config()
@@ -254,6 +278,7 @@ def build_message(cwd: str) -> str:
254
278
  msg += _drift(version)
255
279
  msg += _EVIDENCE_CONTRACT
256
280
  msg += _META_TAG_CONTRACT
281
+ msg += _authority_brief(cwd)
257
282
  msg += _model_fabric()
258
283
  _trigger_reorganizer(repo, config)
259
284
  _ensure_dashboard(repo, config)
@@ -24,11 +24,12 @@ Read by: config/hooks/pre-tool-use.sh between the KB-gate and the
24
24
  flow-gate. Same Decision JSON contract as core.workflow.flow_enforcer.
25
25
  """
26
26
 
27
+ import contextlib
27
28
  import json
28
29
  import re
29
30
  from contextlib import contextmanager
30
31
  from dataclasses import asdict, dataclass, field
31
- from datetime import datetime, timezone
32
+ from datetime import UTC, datetime
32
33
  from functools import lru_cache
33
34
  from pathlib import Path
34
35
 
@@ -55,10 +56,8 @@ def _locked_append(path: Path):
55
56
  yield fh
56
57
  finally:
57
58
  if _HAS_FLOCK:
58
- try:
59
+ with contextlib.suppress(OSError):
59
60
  fcntl.flock(fh.fileno(), fcntl.LOCK_UN)
60
- except OSError:
61
- pass
62
61
  fh.close()
63
62
 
64
63
 
@@ -73,6 +72,11 @@ OWNERSHIP_YAML_PATH = (
73
72
  / "config"
74
73
  / "agent-ownership.yaml"
75
74
  )
75
+ ROSTER_PATH = (
76
+ Path(__file__).resolve().parent.parent.parent
77
+ / "config"
78
+ / "agent-roster.json"
79
+ )
76
80
 
77
81
  GATED_TOOLS: frozenset[str] = frozenset(
78
82
  {"Write", "Edit", "MultiEdit", "NotebookEdit"}
@@ -85,8 +89,13 @@ ROUTING_RE = re.compile(
85
89
  DISPATCH_RE = re.compile(
86
90
  r"\[arka:dispatch\]\s*[\w-]+\s*->\s*([\w-]+)", re.IGNORECASE
87
91
  )
92
+ # Single-line, length-bounded body: the lazy `([^\]]+?)\s*` form
93
+ # backtracked catastrophically on an unclosed marker (4000 trailing
94
+ # spaces = 22s) — inside a BLOCKING PreToolUse hook, on model-emitted
95
+ # text. 400 chars is far above any substantive reason; beyond it the
96
+ # marker simply does not parse and the gate stays closed.
88
97
  BYPASS_RE = re.compile(
89
- r"\[arka:specialist-bypass\s+([^\]]+?)\s*\]", re.IGNORECASE
98
+ r"\[arka:specialist-bypass\s+([^\]\n]{1,400})\]", re.IGNORECASE
90
99
  )
91
100
 
92
101
  ASSISTANT_WINDOW = 20
@@ -107,22 +116,42 @@ class Decision:
107
116
  bypass_used: bool = False
108
117
  bypass_reason: str | None = None
109
118
  target_file: str | None = None
119
+ # Alias resolution (PR-1): the marker said "diana", the rules say
120
+ # "frontend-dev" — record both so telemetry can prove the fix.
121
+ persona_raw: str | None = None
122
+ alias_resolved: bool = False
110
123
 
111
124
  def to_stderr_message(self) -> str:
125
+ """The message a blocked session reads.
126
+
127
+ Rewritten after the 2026-07-12 incident, where a session read the
128
+ old text and went looking for a bug to justify a bypass. Three
129
+ changes carry the weight: it refuses the false diagnosis up front,
130
+ it hands over the correct path already filled in (two copyable
131
+ lines), and it demotes the bypass to what it is — an audited
132
+ exception the operator sees. The old text also called EVERY
133
+ persona "(lead)", so a blocked specialist was told she was a lead:
134
+ that actively fed the "the gate is buggy" story.
135
+ """
112
136
  if self.allow:
113
137
  return ""
114
- persona = self.current_persona or "lead"
115
- owners = ", ".join(self.required_owners) or "specialist"
138
+ persona = self.current_persona or "unrouted"
139
+ owner = (self.required_owners or ["the owning specialist"])[0]
140
+ owners = ", ".join(self.required_owners) or "the owning specialist"
116
141
  target = self.target_file or "this file"
117
142
  return (
118
- f"[ARKA:SPECIALIST] {persona} (lead) is not authorised to write "
119
- f"{target}. Required owners: {owners}. Choose one: (1) dispatch "
120
- f"the specialist via the Agent tool AND emit "
121
- f"`[arka:dispatch] {persona} -> <specialist>` immediately before "
122
- f"the dispatch call (NON-NEGOTIABLE constitution rule "
123
- f"`dispatch-must-be-announced`), OR (2) add "
124
- f"`[arka:specialist-bypass <reason>]` to the same assistant "
125
- f"message to override (logged for accountability)."
143
+ f"[ARKA:SPECIALIST] BLOCKED. {target} is owned by: {owners} "
144
+ f" dispatch any ONE of them. You are {persona}.\n"
145
+ f"This is NOT a bug. The gate is working as designed. Retrying "
146
+ f"this Write will fail again, every time.\n"
147
+ f"DO THIS — two lines, and the specialist writes with no block:\n"
148
+ f" [arka:dispatch] {persona} -> {owner}\n"
149
+ f" Task(subagent_type=\"{owner}\", prompt=\"<what you were "
150
+ f"about to write>\")\n"
151
+ f"Audited exception (visible to the operator in "
152
+ f"`/arka enforcement`) — ONLY if no specialist can do this:\n"
153
+ f" [arka:specialist-bypass owner={owner} reason=<24+ chars "
154
+ f"explaining why a specialist cannot>]"
126
155
  )
127
156
 
128
157
 
@@ -139,6 +168,8 @@ class _Ctx:
139
168
  messages: list[str] = field(default_factory=list)
140
169
  persona: str | None = None
141
170
  marker: str | None = None
171
+ persona_raw: str | None = None
172
+ alias_resolved: bool = False
142
173
  config: dict = field(default_factory=dict)
143
174
  preloaded_messages: list[str] | None = None
144
175
 
@@ -233,34 +264,110 @@ def _glob_match(pattern: str, path: str) -> bool:
233
264
  # ─── Persona, bypass, ownership resolution ─────────────────────────────
234
265
 
235
266
 
236
- def _resolve_persona(messages: list[str]) -> tuple[str | None, str | None]:
237
- """Find the current persona, scanning newest-to-oldest assistant turns.
267
+ def _load_aliases() -> dict[str, str]:
268
+ """First-name -> owner slug, from the generated roster.
269
+
270
+ Sessions dispatch by human name (``-> diana``); ownership rules name
271
+ slugs (``frontend-dev``). Without this, the RIGHT specialist was
272
+ blocked from her own files — 22 of 189 measured blocks. The roster
273
+ only emits aliases that are unambiguous among gate owners, and never
274
+ guesses (core/agents/roster_manifest.py::build_aliases).
275
+ """
276
+ try:
277
+ data = json.loads(ROSTER_PATH.read_text(encoding="utf-8"))
278
+ except (OSError, json.JSONDecodeError):
279
+ return {}
280
+ aliases = data.get("aliases")
281
+ return aliases if isinstance(aliases, dict) else {}
282
+
283
+
284
+ def _normalize_persona(raw: str) -> tuple[str, bool]:
285
+ """Return (slug, alias_resolved). Unknown names pass through."""
286
+ slug = _load_aliases().get(raw)
287
+ return (slug, True) if slug else (raw, False)
288
+
289
+
290
+ def _resolve_persona(
291
+ messages: list[str],
292
+ ) -> tuple[str | None, str | None, str | None, bool]:
293
+ """Find the current persona: (slug, marker, raw, alias_resolved).
238
294
 
239
295
  Dispatch tag wins over routing because dispatching is more specific.
296
+ The name is normalized through the roster aliases, so a marker that
297
+ names the human (``-> diana``) matches an ownership rule that names
298
+ the slug (``frontend-dev``).
240
299
  """
241
300
  for text in reversed(messages):
242
301
  dispatch = DISPATCH_RE.search(text)
243
302
  if dispatch:
244
- return dispatch.group(1).lower(), "dispatch"
303
+ raw = dispatch.group(1).lower()
304
+ slug, resolved = _normalize_persona(raw)
305
+ return slug, "dispatch", raw, resolved
245
306
  routing = ROUTING_RE.search(text)
246
307
  if routing:
247
- return routing.group(1).lower(), "routing"
248
- return None, None
308
+ raw = routing.group(1).lower()
309
+ slug, resolved = _normalize_persona(raw)
310
+ return slug, "routing", raw, resolved
311
+ return None, None, None, False
312
+
313
+
314
+ # A bypass must cost more than doing the right thing. Before the
315
+ # 2026-07-12 incident the only barrier was a non-empty string, and the
316
+ # deny message advertised the bypass beside the dispatch as an equal
317
+ # option — the single place in the whole prompt surface that taught it.
318
+ _MIN_BYPASS_REASON = 24
319
+ _EMPTY_REASONS = (
320
+ "typo", "quick fix", "quickfix", "trivial", "urgent", "just this once",
321
+ "small change", "minor", "one char", "fast", "quick typo fix",
322
+ "just a quick typo fix", "just a quick typo fix honestly",
323
+ "nothing important", "it is faster this way",
324
+ )
325
+ # Structured form the deny message teaches: `owner=<slug> reason=<text>`.
326
+ _BYPASS_STRUCTURED_RE = re.compile(
327
+ r"^owner=(?P<owner>[\w-]+)\s+reason=(?P<reason>.*)$",
328
+ re.IGNORECASE | re.DOTALL,
329
+ )
249
330
 
250
331
 
251
- def _find_bypass(messages: list[str]) -> str | None:
252
- """Return bypass reason from LAST assistant message, or None.
332
+ def _reason_is_substantive(reason: str) -> bool:
333
+ """A reason must say something. QG redo 1 caught the hole: the floor
334
+ was applied to the WHOLE marker body, so
335
+ ``owner=senior-dev reason=`` (24 chars of boilerplate, empty reason)
336
+ opened the gate — with the deny message teaching that very template.
337
+ The floor now applies to the reason ALONE."""
338
+ reason = reason.strip()
339
+ if len(reason) < _MIN_BYPASS_REASON:
340
+ return False
341
+ normalized = reason.lower().strip(" .!,;")
342
+ return normalized not in _EMPTY_REASONS
343
+
344
+
345
+ def _find_bypass(messages: list[str], owners: list[str] | None = None) -> str | None:
346
+ """Return the bypass reason from the LAST assistant message, or None.
253
347
 
254
348
  Scope is strict: only the immediately preceding assistant message can
255
- grant a bypass. Empty / whitespace reasons are rejected.
349
+ grant a bypass. The structured form is required in spirit — a bare
350
+ legacy reason is still accepted for one release, but it must clear the
351
+ same substance floor. When ``owners`` is given, a structured
352
+ ``owner=`` that names someone other than an actual owner is rejected:
353
+ a bypass may not invent its own justification target.
256
354
  """
257
355
  if not messages:
258
356
  return None
259
357
  match = BYPASS_RE.search(messages[-1])
260
358
  if not match:
261
359
  return None
262
- reason = match.group(1).strip()
263
- return reason if reason else None
360
+ body = match.group(1).strip()
361
+ structured = _BYPASS_STRUCTURED_RE.match(body)
362
+ if structured:
363
+ if owners:
364
+ claimed = structured.group("owner").lower()
365
+ if claimed not in {o.lower() for o in owners}:
366
+ return None
367
+ reason = structured.group("reason")
368
+ else:
369
+ reason = body
370
+ return reason.strip() if _reason_is_substantive(reason) else None
264
371
 
265
372
 
266
373
  def _match_ownership(
@@ -312,7 +419,9 @@ def _populate_context(ctx: _Ctx) -> None:
312
419
  ctx.messages = _load_last_assistant_messages(
313
420
  ctx.transcript_path, ASSISTANT_WINDOW
314
421
  )
315
- ctx.persona, ctx.marker = _resolve_persona(ctx.messages)
422
+ ctx.persona, ctx.marker, ctx.persona_raw, ctx.alias_resolved = (
423
+ _resolve_persona(ctx.messages)
424
+ )
316
425
 
317
426
 
318
427
  def _check_no_persona(ctx: _Ctx) -> Decision | None:
@@ -360,7 +469,8 @@ def _decide_owner_match(ctx: _Ctx, owners: list[str]) -> Decision:
360
469
  return Decision(
361
470
  allow=True, reason=f"owner-match:{ctx.persona}",
362
471
  current_persona=ctx.persona, marker_found=ctx.marker,
363
- target_file=ctx.file_path, required_owners=owners,
472
+ target_file=ctx.file_path,
473
+ persona_raw=ctx.persona_raw, alias_resolved=ctx.alias_resolved, required_owners=owners,
364
474
  )
365
475
 
366
476
 
@@ -368,7 +478,8 @@ def _decide_bypass(ctx: _Ctx, owners: list[str], reason: str) -> Decision:
368
478
  return Decision(
369
479
  allow=True, reason="bypass-with-reason",
370
480
  current_persona=ctx.persona, marker_found=ctx.marker,
371
- target_file=ctx.file_path, required_owners=owners,
481
+ target_file=ctx.file_path,
482
+ persona_raw=ctx.persona_raw, alias_resolved=ctx.alias_resolved, required_owners=owners,
372
483
  bypass_used=True, bypass_reason=reason,
373
484
  )
374
485
 
@@ -379,7 +490,8 @@ def _decide_block(ctx: _Ctx, owners: list[str]) -> Decision:
379
490
  allow=False,
380
491
  reason=f"lead-blocked:{ctx.persona}-not-in-[{','.join(owners_lower)}]",
381
492
  current_persona=ctx.persona, marker_found=ctx.marker,
382
- target_file=ctx.file_path, required_owners=owners,
493
+ target_file=ctx.file_path,
494
+ persona_raw=ctx.persona_raw, alias_resolved=ctx.alias_resolved, required_owners=owners,
383
495
  )
384
496
 
385
497
 
@@ -389,7 +501,7 @@ def _resolve_with_owners(ctx: _Ctx, owners: list[str], rule_reason: str | None)
389
501
  return _decide_open_access(ctx, owners, rule_reason)
390
502
  if ctx.persona in {o.lower() for o in owners}:
391
503
  return _decide_owner_match(ctx, owners)
392
- bypass = _find_bypass(ctx.messages)
504
+ bypass = _find_bypass(ctx.messages, owners)
393
505
  if bypass:
394
506
  return _decide_bypass(ctx, owners, bypass)
395
507
  return _decide_block(ctx, owners)
@@ -464,7 +576,7 @@ def record_telemetry(
464
576
  if safe is None:
465
577
  return
466
578
  entry = {
467
- "ts": datetime.now(timezone.utc).isoformat(),
579
+ "ts": datetime.now(UTC).isoformat(),
468
580
  "session_id": safe,
469
581
  "tool": tool,
470
582
  "cwd": cwd,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "4.14.3",
3
+ "version": "4.14.4",
4
4
  "description": "The Operating System for AI Agent Teams",
5
5
  "type": "module",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arkaos-core"
3
- version = "4.14.3"
3
+ version = "4.14.4"
4
4
  description = "Core engine for ArkaOS — The Operating System for AI Agent Teams"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}