arkaos 3.75.1 → 3.76.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/VERSION CHANGED
@@ -1 +1 @@
1
- 3.75.1
1
+ 3.76.0
@@ -194,6 +194,18 @@ For each item, in order:
194
194
  Do not skip items. Do not batch QA or Security across multiple
195
195
  items — each item runs the full gate chain.
196
196
 
197
+ **DNA fidelity check at turn end (PR5 v3.76.0, SHOULD `dna-fidelity-warn`).**
198
+ The Stop hook (`config/hooks/stop.sh`) invokes
199
+ `core.governance.dna_fidelity.check_fidelity(agent_id, output)` for the
200
+ current persona (from `[arka:routing]` or `[arka:dispatch]`). Violations
201
+ of `avoid_patterns` or missing `opening_phrases` from the agent's YAML
202
+ `signature_markers` block are recorded to
203
+ `~/.arkaos/telemetry/dna-fidelity.jsonl` — soft-warn only in v3.76.0.
204
+ Operator audit: `python -m core.governance.dna_fidelity_cli summary`.
205
+ Each Agent dispatch also logs to
206
+ `~/.arkaos/telemetry/agent-activations.jsonl`; surface dormant agents
207
+ via `python -m core.governance.agent_activation_cli dead`.
208
+
197
209
  ### Phase 13 — Detailed summary
198
210
  When the TODO list is exhausted, emit a final summary: what was done,
199
211
  where it lives, how to verify, what is open for next time.
@@ -217,6 +217,11 @@ enforcement_levels:
217
217
  rule: "Bottom-line first output. Lead with answer, then why, then how. Confidence tags on assessments."
218
218
  enforcement: "See config/standards/communication.md for full standard"
219
219
 
220
+ # ─── Rule added in PR5 Squad Intelligence Upgrade (2026-05-28) ───────
221
+ - id: dna-fidelity-warn
222
+ rule: "Agent outputs are compared against the `signature_markers` block in each agent's YAML at the end of every turn. Forbidden patterns (avoid_patterns) and missing opening phrases (opening_phrases) generate FidelityViolation records in ~/.arkaos/telemetry/dna-fidelity.jsonl. v3.76.0 is soft-warn — violations are recorded for telemetry and operator review, not blocked. Hard-block mode lands later once the marker set is calibrated against real production usage."
223
+ enforcement: "Stop hook (config/hooks/stop.sh) calls core.governance.dna_fidelity.check_fidelity at turn end. Telemetry inspection: python -m core.governance.dna_fidelity_cli list|summary. Agents in scope for v3.76.0: tech-lead-paulo, cqo-marta, copy-director-eduardo, tech-director-francisca. Remaining 61 agent YAMLs get signature_markers in v3.76.x curation work."
224
+
220
225
  # ─── Rule added in PR4 Squad Intelligence Upgrade (2026-05-28) ───────
221
226
  - id: pattern-library-first
222
227
  rule: "Before designing any new feature, consult the Pattern Library (core.knowledge.pattern_cards). If a similar pattern exists, reuse it or explicitly document why divergence is justified in the spec. New patterns SHOULD be registered with record_pattern() after Quality Gate APPROVED so future feature work inherits the prior art."
@@ -165,6 +165,33 @@ PY
165
165
  fi
166
166
  fi
167
167
  fi
168
+
169
+ # ─── Activation tracking (PR5 v3.76.0) ─────────────────────────────
170
+ # Record every Task/Agent dispatch regardless of subagent_type (CQO or
171
+ # any other). Runs AFTER the cqo branch so verdicts don't block it.
172
+ # Never blocks — _ACT_ROOT reuses the same resolution as _AE_ROOT.
173
+ if [ -n "$SUBAGENT_TYPE" ]; then
174
+ _ACT_ROOT="${ARKAOS_ROOT:-}"
175
+ if [ -z "$_ACT_ROOT" ] && [ -f "$HOME/.arkaos/.repo-path" ]; then
176
+ _ACT_ROOT=$(cat "$HOME/.arkaos/.repo-path" 2>/dev/null)
177
+ fi
178
+ [ -z "$_ACT_ROOT" ] && _ACT_ROOT="$HOME/.arkaos"
179
+ SUBAGENT_TYPE="$SUBAGENT_TYPE" \
180
+ SESSION_ID="$SESSION_ID_PTU" \
181
+ ARKAOS_ROOT="$_ACT_ROOT" \
182
+ python3 - <<'PY' 2>/dev/null || true
183
+ import os, sys
184
+ sys.path.insert(0, os.environ["ARKAOS_ROOT"])
185
+ try:
186
+ from core.governance.activation_tracker import record_activation
187
+ record_activation(
188
+ subagent_type=os.environ.get("SUBAGENT_TYPE", ""),
189
+ session_id=os.environ.get("SESSION_ID", ""),
190
+ )
191
+ except Exception:
192
+ pass
193
+ PY
194
+ fi
168
195
  fi
169
196
 
170
197
  # Only process if there was an error
@@ -20,6 +20,7 @@ TRANSCRIPT_PATH=""
20
20
  STOP_HOOK_ACTIVE=""
21
21
  CWD=""
22
22
  EFFORT_LEVEL=""
23
+ ASSISTANT_MSG_STOP=""
23
24
  if command -v jq &>/dev/null; then
24
25
  SESSION_ID=$(echo "$input" | jq -r '.session_id // ""' 2>/dev/null)
25
26
  TRANSCRIPT_PATH=$(echo "$input" | jq -r '.transcript_path // ""' 2>/dev/null)
@@ -29,6 +30,8 @@ if command -v jq &>/dev/null; then
29
30
  # $CLAUDE_EFFORT env var. Soft-block checks (kb-cite, meta-tag) only
30
31
  # run at high|xhigh; hard enforcement runs regardless.
31
32
  EFFORT_LEVEL=$(echo "$input" | jq -r '.effort.level // ""' 2>/dev/null)
33
+ # PR5 v3.76.0 — DNA fidelity check needs the closing assistant message.
34
+ ASSISTANT_MSG_STOP=$(echo "$input" | jq -r '.assistant_message // ""' 2>/dev/null)
32
35
  fi
33
36
  # Fallback to env var if stdin didn't carry it
34
37
  [ -z "$EFFORT_LEVEL" ] && EFFORT_LEVEL="${CLAUDE_EFFORT:-}"
@@ -39,6 +42,60 @@ fi
39
42
  # (whether the next turn sees a [arka:suggest] line). Record the level
40
43
  # on the telemetry row so we can later analyze suppression rates.
41
44
 
45
+ # ─── DNA Fidelity Check (PR5 v3.76.0) ───────────────────────────────────
46
+ # Fires for every session (no WF_MARKER dependency — fidelity is always
47
+ # worth measuring). Extracts the dispatched persona from the last routing/
48
+ # dispatch marker in the closing message, then calls check_fidelity() +
49
+ # record_fidelity(). Soft-warn only, never blocks. Zero violations are
50
+ # recorded too — absence of violations is signal.
51
+ if [ -n "$ASSISTANT_MSG_STOP" ] && [ -n "$SESSION_ID" ] && command -v python3 &>/dev/null; then
52
+ _FID_ROOT="${ARKAOS_ROOT:-}"
53
+ if [ -z "$_FID_ROOT" ] && [ -f "$HOME/.arkaos/.repo-path" ]; then
54
+ _FID_ROOT=$(cat "$HOME/.arkaos/.repo-path" 2>/dev/null)
55
+ fi
56
+ [ -z "$_FID_ROOT" ] && _FID_ROOT="$HOME/.arkaos"
57
+
58
+ # Extract persona: dispatch marker takes precedence over routing.
59
+ # Latest match wins (tail -1). Both patterns: [arka:dispatch] X -> Y
60
+ # and [arka:routing] X -> Y. Persona is the right-hand side, lowercased.
61
+ _FIDELITY_PERSONA=""
62
+ _DISPATCH_HIT=$(printf '%s' "$ASSISTANT_MSG_STOP" \
63
+ | grep -ioE '\[arka:dispatch\][[:space:]]*[A-Za-z0-9_-]+[[:space:]]*->[[:space:]]*[A-Za-z0-9_-]+' \
64
+ | tail -1)
65
+ if [ -n "$_DISPATCH_HIT" ]; then
66
+ _FIDELITY_PERSONA=$(printf '%s' "$_DISPATCH_HIT" \
67
+ | sed -E 's/.*->[[:space:]]*//' | tr '[:upper:]' '[:lower:]')
68
+ else
69
+ _ROUTING_HIT=$(printf '%s' "$ASSISTANT_MSG_STOP" \
70
+ | grep -ioE '\[arka:routing\][[:space:]]*[A-Za-z0-9_-]+[[:space:]]*->[[:space:]]*[A-Za-z0-9_-]+' \
71
+ | tail -1)
72
+ if [ -n "$_ROUTING_HIT" ]; then
73
+ _FIDELITY_PERSONA=$(printf '%s' "$_ROUTING_HIT" \
74
+ | sed -E 's/.*->[[:space:]]*//' | tr '[:upper:]' '[:lower:]')
75
+ fi
76
+ fi
77
+
78
+ if [ -n "$_FIDELITY_PERSONA" ]; then
79
+ FIDELITY_PERSONA="$_FIDELITY_PERSONA" \
80
+ FIDELITY_SESSION_ID="$SESSION_ID" \
81
+ FIDELITY_MSG="$ASSISTANT_MSG_STOP" \
82
+ ARKAOS_ROOT="$_FID_ROOT" \
83
+ python3 - <<'PY' 2>/dev/null || true
84
+ import os, sys
85
+ sys.path.insert(0, os.environ["ARKAOS_ROOT"])
86
+ try:
87
+ from core.governance.dna_fidelity import check_fidelity, record_fidelity
88
+ agent_id = os.environ.get("FIDELITY_PERSONA", "")
89
+ session_id = os.environ.get("FIDELITY_SESSION_ID", "")
90
+ output = os.environ.get("FIDELITY_MSG", "")
91
+ violations = check_fidelity(agent_id, output)
92
+ record_fidelity(agent_id, session_id, violations)
93
+ except Exception:
94
+ pass
95
+ PY
96
+ fi
97
+ fi
98
+
42
99
  # Prevent infinite loops when Stop hook was triggered by its own decision.
43
100
  if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
44
101
  exit 0
@@ -0,0 +1,178 @@
1
+ """Activation Tracker — PR5 Squad Intelligence Upgrade v3.76.0.
2
+
3
+ Counts how often each agent (`subagent_type`) is dispatched via the Agent
4
+ tool. Surfaces top callers and dead agents (no activation in N days).
5
+
6
+ Wired into the PostToolUse hook in a later task. For now: persistence +
7
+ query layer. Mirrors the JSONL + path-safe pattern of agent_experiences.py.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from collections import Counter
14
+ from contextlib import contextmanager
15
+ from dataclasses import asdict, dataclass
16
+ from datetime import datetime, timedelta, timezone
17
+ from pathlib import Path
18
+
19
+ from core.shared import safe_session_id as _safe_session_id_module
20
+
21
+ try:
22
+ import fcntl
23
+ _HAS_FLOCK = True
24
+ except ImportError:
25
+ _HAS_FLOCK = False
26
+
27
+
28
+ # ─── Module-level constant (monkeypatched by tests) ───────────────────────────
29
+
30
+ TELEMETRY_PATH: Path = Path.home() / ".arkaos" / "telemetry" / "agent-activations.jsonl"
31
+
32
+
33
+ # ─── Dataclass ────────────────────────────────────────────────────────────────
34
+
35
+ @dataclass
36
+ class Activation:
37
+ ts: str
38
+ subagent_type: str
39
+ session_id: str
40
+
41
+
42
+ # ─── Internal helpers ─────────────────────────────────────────────────────────
43
+
44
+ @contextmanager
45
+ def _locked_append(path: Path):
46
+ """Append to path under POSIX flock; Windows falls back to O_APPEND atomicity."""
47
+ path.parent.mkdir(parents=True, exist_ok=True)
48
+ fh = path.open("a", encoding="utf-8")
49
+ try:
50
+ if _HAS_FLOCK:
51
+ fcntl.flock(fh.fileno(), fcntl.LOCK_EX)
52
+ yield fh
53
+ finally:
54
+ if _HAS_FLOCK:
55
+ try:
56
+ fcntl.flock(fh.fileno(), fcntl.LOCK_UN)
57
+ except OSError:
58
+ pass
59
+ fh.close()
60
+
61
+
62
+ def _safe_id(value: str) -> str | None:
63
+ """Delegate to safe_session_id for CWE-22 path-traversal guard."""
64
+ return _safe_session_id_module.safe_session_id(value)
65
+
66
+
67
+ def _parse_ts(iso: str) -> datetime | None:
68
+ """Parse an ISO timestamp string; return None on any failure."""
69
+ try:
70
+ return datetime.fromisoformat(iso)
71
+ except (TypeError, ValueError):
72
+ return None
73
+
74
+
75
+ def _load_all() -> list[dict]:
76
+ """Single-pass read of TELEMETRY_PATH; skip malformed lines silently."""
77
+ entries: list[dict] = []
78
+ try:
79
+ with TELEMETRY_PATH.open(encoding="utf-8") as fh:
80
+ for line in fh:
81
+ if not line.strip():
82
+ continue
83
+ try:
84
+ entries.append(json.loads(line))
85
+ except json.JSONDecodeError:
86
+ continue
87
+ except OSError:
88
+ return []
89
+ return entries
90
+
91
+
92
+ def _filter_by_since(entries: list[dict], since: datetime | None) -> list[dict]:
93
+ """Remove entries whose ts is strictly before `since`. Pass-through when since is None."""
94
+ if since is None:
95
+ return entries
96
+ result: list[dict] = []
97
+ for e in entries:
98
+ ts = _parse_ts(e.get("ts", ""))
99
+ if ts is not None and ts >= since:
100
+ result.append(e)
101
+ return result
102
+
103
+
104
+ def _most_recent_per_agent(entries: list[dict]) -> dict[str, str]:
105
+ """Return a map of subagent_type → latest ts string seen across all entries."""
106
+ latest: dict[str, str] = {}
107
+ for e in entries:
108
+ agent = e.get("subagent_type", "")
109
+ ts = e.get("ts", "")
110
+ if not agent or not ts:
111
+ continue
112
+ if agent not in latest or ts > latest[agent]:
113
+ latest[agent] = ts
114
+ return latest
115
+
116
+
117
+ # ─── Public API ───────────────────────────────────────────────────────────────
118
+
119
+ def record_activation(subagent_type: str, session_id: str) -> None:
120
+ """Append one JSONL activation record to TELEMETRY_PATH.
121
+
122
+ Silently drops when subagent_type is empty, fails the safe-id check,
123
+ or when filesystem I/O fails.
124
+ """
125
+ if not subagent_type:
126
+ return
127
+ if _safe_id(subagent_type) is None:
128
+ return
129
+ record = asdict(Activation(
130
+ ts=datetime.now(timezone.utc).isoformat(),
131
+ subagent_type=subagent_type,
132
+ session_id=session_id,
133
+ ))
134
+ try:
135
+ with _locked_append(TELEMETRY_PATH) as fh:
136
+ fh.write(json.dumps(record) + "\n")
137
+ except OSError:
138
+ return
139
+
140
+
141
+ def query_top_callers(
142
+ *,
143
+ limit: int = 10,
144
+ since: datetime | None = None,
145
+ ) -> list[tuple[str, int]]:
146
+ """Return the most-dispatched subagent types in descending count order.
147
+
148
+ Reads TELEMETRY_PATH, skips malformed lines silently. Optional `since`
149
+ filters to activations at or after that datetime. Capped at `limit`.
150
+ Empty store returns [].
151
+ """
152
+ entries = _load_all()
153
+ entries = _filter_by_since(entries, since)
154
+ counter: Counter[str] = Counter()
155
+ for e in entries:
156
+ agent = e.get("subagent_type", "")
157
+ if agent:
158
+ counter[agent] += 1
159
+ return counter.most_common(limit)
160
+
161
+
162
+ def query_dead_agents(*, since_days: int = 30) -> list[tuple[str, str]]:
163
+ """Return agents whose most-recent activation is older than `since_days`.
164
+
165
+ Returns [(subagent_type, last_seen_ts), ...] sorted ascending by
166
+ last_seen_ts (most-dormant first). Agents activated within the window
167
+ are excluded even if they have older activations too.
168
+ """
169
+ cutoff = datetime.now(timezone.utc) - timedelta(days=since_days)
170
+ entries = _load_all()
171
+ recent_map = _most_recent_per_agent(entries)
172
+ dead: list[tuple[str, str]] = []
173
+ for agent, last_ts in recent_map.items():
174
+ ts = _parse_ts(last_ts)
175
+ if ts is not None and ts < cutoff:
176
+ dead.append((agent, last_ts))
177
+ dead.sort(key=lambda pair: pair[1])
178
+ return dead
@@ -0,0 +1,107 @@
1
+ """CLI viewer for agent activation telemetry.
2
+
3
+ Usage:
4
+ python -m core.governance.agent_activation_cli top [--since DAYS] [--limit N]
5
+ python -m core.governance.agent_activation_cli dead [--since-days N]
6
+
7
+ Examples:
8
+ python -m core.governance.agent_activation_cli top --limit 10
9
+ python -m core.governance.agent_activation_cli dead --since-days 30
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import sys
16
+ from datetime import datetime, timedelta, timezone
17
+
18
+ from core.governance.activation_tracker import query_dead_agents, query_top_callers
19
+
20
+
21
+ def _since_dt(days: int | None) -> datetime | None:
22
+ if days is None:
23
+ return None
24
+ return datetime.now(timezone.utc) - timedelta(days=days)
25
+
26
+
27
+ def _print_top(rows: list[tuple[str, int]]) -> int:
28
+ """Print rank | subagent_type | call_count table."""
29
+ if not rows:
30
+ print("No activation records found.")
31
+ return 0
32
+ print(f"{'rank':>4} {'subagent_type':<36} {'calls':>6}")
33
+ print("-" * 50)
34
+ for rank, (agent, count) in enumerate(rows, start=1):
35
+ print(f"{rank:>4} {agent:<36} {count:>6}")
36
+ return 0
37
+
38
+
39
+ def _parse_ts_for_display(iso: str) -> tuple[str, int]:
40
+ """Return (formatted_ts, days_since) from an ISO string."""
41
+ try:
42
+ ts = datetime.fromisoformat(iso)
43
+ now = datetime.now(timezone.utc)
44
+ if ts.tzinfo is None:
45
+ ts = ts.replace(tzinfo=timezone.utc)
46
+ days_since = (now - ts).days
47
+ return ts.strftime("%Y-%m-%d %H:%M UTC"), days_since
48
+ except (TypeError, ValueError):
49
+ return iso, -1
50
+
51
+
52
+ def _print_dead(rows: list[tuple[str, str]]) -> int:
53
+ """Print subagent_type | last_seen | days_since table."""
54
+ if not rows:
55
+ print("No dormant agents found.")
56
+ return 0
57
+ print(f"{'subagent_type':<36} {'last_seen':<22} {'days_since':>10}")
58
+ print("-" * 72)
59
+ for agent, last_ts in rows:
60
+ last_seen, days_since = _parse_ts_for_display(last_ts)
61
+ print(f"{agent:<36} {last_seen:<22} {days_since:>10}")
62
+ return 0
63
+
64
+
65
+ def _do_top(args: argparse.Namespace) -> int:
66
+ since = _since_dt(args.since)
67
+ rows = query_top_callers(limit=args.limit, since=since)
68
+ return _print_top(rows)
69
+
70
+
71
+ def _do_dead(args: argparse.Namespace) -> int:
72
+ rows = query_dead_agents(since_days=args.since_days)
73
+ return _print_dead(rows)
74
+
75
+
76
+ def _build_parser() -> argparse.ArgumentParser:
77
+ parser = argparse.ArgumentParser(
78
+ prog="python -m core.governance.agent_activation_cli",
79
+ description="Inspect agent activation telemetry.",
80
+ )
81
+ subparsers = parser.add_subparsers(dest="cmd", required=True)
82
+
83
+ top_p = subparsers.add_parser("top", help="Show most-dispatched agents by call count.")
84
+ top_p.add_argument("--since", type=int, default=None, help="Limit to last N days")
85
+ top_p.add_argument("--limit", type=int, default=10, help="Max records (default 10)")
86
+
87
+ dead_p = subparsers.add_parser("dead", help="Show agents with no activation in N days.")
88
+ dead_p.add_argument(
89
+ "--since-days", type=int, default=30, dest="since_days",
90
+ help="Dormancy threshold in days (default 30)",
91
+ )
92
+ return parser
93
+
94
+
95
+ def main(argv: list[str] | None = None) -> int:
96
+ parser = _build_parser()
97
+ args = parser.parse_args(argv if argv is not None else sys.argv[1:])
98
+ if args.cmd == "top":
99
+ return _do_top(args)
100
+ if args.cmd == "dead":
101
+ return _do_dead(args)
102
+ parser.print_help()
103
+ return 2
104
+
105
+
106
+ if __name__ == "__main__": # pragma: no cover
107
+ sys.exit(main())
@@ -0,0 +1,246 @@
1
+ """DNA Fidelity Checker — PR5 Squad Intelligence Upgrade v3.76.0.
2
+
3
+ Compares agent output text against the `signature_markers` block declared
4
+ in the agent's YAML. Detects forbidden patterns (avoid_patterns) and missing
5
+ opening phrases, then records violations to telemetry.
6
+
7
+ Soft-warn mode for v1. Hard-block mode lands in a later PR once telemetry
8
+ shows the marker set is stable. Never raises — hook helpers must not break
9
+ the execution path.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import re
16
+ from contextlib import contextmanager
17
+ from dataclasses import asdict, dataclass, field
18
+ from datetime import datetime, timezone
19
+ from functools import lru_cache
20
+ from pathlib import Path
21
+
22
+ import yaml
23
+
24
+ from core.shared import safe_session_id as _safe_session_id_module
25
+
26
+ try:
27
+ import fcntl
28
+ _HAS_FLOCK = True
29
+ except ImportError:
30
+ _HAS_FLOCK = False
31
+
32
+
33
+ # ─── Module-level constants (monkeypatched by tests) ──────────────────────────
34
+
35
+ AGENT_YAML_SEARCH_DIRS: list[Path] = [
36
+ Path(__file__).resolve().parent.parent.parent / "departments",
37
+ ]
38
+ TELEMETRY_PATH: Path = Path.home() / ".arkaos" / "telemetry" / "dna-fidelity.jsonl"
39
+
40
+ _OPENING_WINDOW: int = 300
41
+
42
+
43
+ # ─── Dataclasses ──────────────────────────────────────────────────────────────
44
+
45
+ @dataclass
46
+ class SignatureMarkers:
47
+ opening_phrases: list[str] = field(default_factory=list)
48
+ typical_patterns: list[str] = field(default_factory=list)
49
+ closing_style: str | None = None
50
+ avoid_patterns: list[str] = field(default_factory=list)
51
+
52
+
53
+ @dataclass
54
+ class FidelityViolation:
55
+ kind: str # "forbidden_pattern" | "missing_opening"
56
+ pattern: str
57
+ span: str
58
+
59
+
60
+ # ─── Internal helpers ─────────────────────────────────────────────────────────
61
+
62
+ def _register_yaml_entry(index: dict[str, Path], path: Path, data: dict) -> None:
63
+ """Add all lookup keys for one agent YAML into the shared index dict.
64
+
65
+ Keys registered per file:
66
+ - ``data["id"].lower()`` e.g. ``tech-lead-paulo``
67
+ - ``data["name"].lower()`` e.g. ``paulo``
68
+ - last hyphen-separated segment of id e.g. ``paulo`` (deduped)
69
+ """
70
+ agent_id: str | None = data.get("id")
71
+ if agent_id and isinstance(agent_id, str):
72
+ key = agent_id.lower()
73
+ index.setdefault(key, path)
74
+ suffix = key.rsplit("-", 1)[-1]
75
+ index.setdefault(suffix, path)
76
+ name: str | None = data.get("name")
77
+ if name and isinstance(name, str):
78
+ index.setdefault(name.lower(), path)
79
+
80
+
81
+ @lru_cache(maxsize=1)
82
+ def _index_agents() -> dict[str, Path]:
83
+ """Walk AGENT_YAML_SEARCH_DIRS once and build a name/id → path index.
84
+
85
+ Three keys per YAML (id, name, id-suffix) allow callers to pass the
86
+ short persona name (``paulo``) instead of the full id (``tech-lead-paulo``).
87
+ """
88
+ index: dict[str, Path] = {}
89
+ for search_dir in AGENT_YAML_SEARCH_DIRS:
90
+ if not search_dir.is_dir():
91
+ continue
92
+ for candidate in search_dir.rglob("*.yaml"):
93
+ try:
94
+ data = yaml.safe_load(candidate.read_text(encoding="utf-8"))
95
+ except (OSError, yaml.YAMLError):
96
+ continue
97
+ if isinstance(data, dict):
98
+ _register_yaml_entry(index, candidate, data)
99
+ return index
100
+
101
+
102
+ def _yaml_path_for(agent_id: str) -> Path | None:
103
+ """Resolve an agent persona name to its YAML path via the index.
104
+
105
+ Returns None when agent_id fails the safe-session-id check (CWE-22
106
+ hardening) or when no matching YAML is found.
107
+ """
108
+ if _safe_session_id_module.safe_session_id(agent_id) is None:
109
+ return None
110
+ return _index_agents().get(agent_id.lower())
111
+
112
+
113
+ @lru_cache(maxsize=128)
114
+ def _load_markers(agent_id: str) -> SignatureMarkers | None:
115
+ """Load and parse signature_markers from an agent YAML. Cached per process."""
116
+ path = _yaml_path_for(agent_id)
117
+ if path is None:
118
+ return None
119
+ try:
120
+ raw = yaml.safe_load(path.read_text(encoding="utf-8"))
121
+ except (OSError, yaml.YAMLError):
122
+ return None
123
+ if not isinstance(raw, dict):
124
+ return None
125
+ block = raw.get("signature_markers")
126
+ if not block or not isinstance(block, dict):
127
+ return None
128
+ return SignatureMarkers(
129
+ opening_phrases=block.get("opening_phrases") or [],
130
+ typical_patterns=block.get("typical_patterns") or [],
131
+ closing_style=block.get("closing_style"),
132
+ avoid_patterns=block.get("avoid_patterns") or [],
133
+ )
134
+
135
+
136
+ # Chain cache invalidation so existing callers of `_load_markers.cache_clear()`
137
+ # also flush the agent index (both are file-system snapshots; stale index
138
+ # would return wrong paths after AGENT_YAML_SEARCH_DIRS is monkeypatched).
139
+ _original_load_markers_cache_clear = _load_markers.cache_clear
140
+
141
+
142
+ def _chained_cache_clear() -> None:
143
+ _index_agents.cache_clear()
144
+ _original_load_markers_cache_clear()
145
+
146
+
147
+ _load_markers.cache_clear = _chained_cache_clear # type: ignore[method-assign]
148
+
149
+
150
+ def _search_avoid_patterns(
151
+ output: str, patterns: list[str]
152
+ ) -> list[FidelityViolation]:
153
+ """Return one FidelityViolation per avoid_pattern that matches output."""
154
+ violations: list[FidelityViolation] = []
155
+ for pattern in patterns:
156
+ match = re.search(pattern, output, re.IGNORECASE)
157
+ if match:
158
+ violations.append(
159
+ FidelityViolation(
160
+ kind="forbidden_pattern",
161
+ pattern=pattern,
162
+ span=match.group(0),
163
+ )
164
+ )
165
+ return violations
166
+
167
+
168
+ def _check_opening(
169
+ output: str, opening_phrases: list[str]
170
+ ) -> FidelityViolation | None:
171
+ """Return a violation when none of the opening phrases appear near the top."""
172
+ if not opening_phrases:
173
+ return None
174
+ window = output[:_OPENING_WINDOW]
175
+ for phrase in opening_phrases:
176
+ if re.search(re.escape(phrase), window, re.IGNORECASE):
177
+ return None
178
+ return FidelityViolation(
179
+ kind="missing_opening",
180
+ pattern=", ".join(opening_phrases),
181
+ span=output[:80],
182
+ )
183
+
184
+
185
+ @contextmanager
186
+ def _locked_append(path: Path):
187
+ """Append to path under POSIX flock; Windows falls back to O_APPEND atomicity."""
188
+ path.parent.mkdir(parents=True, exist_ok=True)
189
+ fh = path.open("a", encoding="utf-8")
190
+ try:
191
+ if _HAS_FLOCK:
192
+ fcntl.flock(fh.fileno(), fcntl.LOCK_EX)
193
+ yield fh
194
+ finally:
195
+ if _HAS_FLOCK:
196
+ try:
197
+ fcntl.flock(fh.fileno(), fcntl.LOCK_UN)
198
+ except OSError:
199
+ pass
200
+ fh.close()
201
+
202
+
203
+ # ─── Public API ───────────────────────────────────────────────────────────────
204
+
205
+ def check_fidelity(agent_id: str, output: str) -> list[FidelityViolation]:
206
+ """Compare output against the agent's signature_markers.
207
+
208
+ Returns a list of FidelityViolation. Empty list means clean pass or
209
+ no markers defined for the agent.
210
+ """
211
+ markers = _load_markers(agent_id)
212
+ if markers is None:
213
+ return []
214
+ violations: list[FidelityViolation] = []
215
+ violations.extend(_search_avoid_patterns(output, markers.avoid_patterns))
216
+ opening_violation = _check_opening(output, markers.opening_phrases)
217
+ if opening_violation is not None:
218
+ violations.append(opening_violation)
219
+ return violations
220
+
221
+
222
+ def record_fidelity(
223
+ agent_id: str,
224
+ session_id: str,
225
+ violations: list[FidelityViolation],
226
+ ) -> None:
227
+ """Append one JSONL telemetry record for this fidelity check.
228
+
229
+ Silently drops when agent_id is unsafe or filesystem I/O fails.
230
+ Zero violations are still recorded — absence of violations is signal too.
231
+ """
232
+ safe_id = _safe_session_id_module.safe_session_id(agent_id)
233
+ if safe_id is None:
234
+ return
235
+ record = {
236
+ "ts": datetime.now(timezone.utc).isoformat(),
237
+ "agent_id": agent_id,
238
+ "session_id": session_id,
239
+ "violation_count": len(violations),
240
+ "violations": [asdict(v) for v in violations],
241
+ }
242
+ try:
243
+ with _locked_append(TELEMETRY_PATH) as fh:
244
+ fh.write(json.dumps(record) + "\n")
245
+ except OSError:
246
+ return
@@ -0,0 +1,149 @@
1
+ """CLI viewer for DNA fidelity telemetry.
2
+
3
+ Usage:
4
+ python -m core.governance.dna_fidelity_cli list [--agent AGENT_ID] [--since DAYS] [--limit N]
5
+ python -m core.governance.dna_fidelity_cli summary [--since DAYS]
6
+
7
+ Examples:
8
+ python -m core.governance.dna_fidelity_cli list --limit 10
9
+ python -m core.governance.dna_fidelity_cli list --agent tech-lead-paulo --since 7
10
+ python -m core.governance.dna_fidelity_cli summary --since 30
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import sys
18
+ from datetime import datetime, timedelta, timezone
19
+ from pathlib import Path
20
+
21
+ _TELEMETRY_PATH: Path = Path.home() / ".arkaos" / "telemetry" / "dna-fidelity.jsonl"
22
+
23
+
24
+ def _load_records(since_dt: datetime | None, agent: str | None) -> list[dict]:
25
+ """Read JSONL, filter by agent and since, skip malformed lines silently."""
26
+ results: list[dict] = []
27
+ try:
28
+ with _TELEMETRY_PATH.open(encoding="utf-8") as fh:
29
+ for line in fh:
30
+ if not line.strip():
31
+ continue
32
+ try:
33
+ rec = json.loads(line)
34
+ except json.JSONDecodeError:
35
+ continue
36
+ if agent and rec.get("agent_id") != agent:
37
+ continue
38
+ if since_dt is not None:
39
+ ts = _parse_ts(rec.get("ts", ""))
40
+ if ts is None or ts < since_dt:
41
+ continue
42
+ results.append(rec)
43
+ except OSError:
44
+ pass
45
+ results.sort(key=lambda r: r.get("ts", ""), reverse=True)
46
+ return results
47
+
48
+
49
+ def _parse_ts(value: str) -> datetime | None:
50
+ """Parse ISO timestamp; return None on failure."""
51
+ try:
52
+ return datetime.fromisoformat(value)
53
+ except (TypeError, ValueError):
54
+ return None
55
+
56
+
57
+ def _since_dt(days: int | None) -> datetime | None:
58
+ if days is None:
59
+ return None
60
+ return datetime.now(timezone.utc) - timedelta(days=days)
61
+
62
+
63
+ def _format_record(rec: dict, index: int) -> str:
64
+ """Format a single fidelity record for list output."""
65
+ ts = rec.get("ts", "?")
66
+ agent = rec.get("agent_id", "?")
67
+ vcount = rec.get("violation_count", 0)
68
+ label = "CLEAN" if vcount == 0 else f"VIOLATIONS({vcount})"
69
+ lines = [f" [{index}] {ts} {agent} {label}"]
70
+ for v in (rec.get("violations") or [])[:3]:
71
+ lines.append(f" - {v.get('kind')} | {v.get('pattern')}")
72
+ return "\n".join(lines)
73
+
74
+
75
+ def _do_list(args: argparse.Namespace) -> int:
76
+ """List fidelity records, most recent first."""
77
+ since = _since_dt(args.since)
78
+ records = _load_records(since, args.agent)
79
+ records = records[: args.limit]
80
+ if not records:
81
+ print("No records found.")
82
+ return 0
83
+ scope = f"agent={args.agent}" if args.agent else "all agents"
84
+ print(f"DNA fidelity ({len(records)} record(s), most recent first) [{scope}]:\n")
85
+ for i, rec in enumerate(records, start=1):
86
+ print(_format_record(rec, i))
87
+ print()
88
+ return 0
89
+
90
+
91
+ def _do_summary(args: argparse.Namespace) -> int:
92
+ """Aggregate by agent_id, show violation rate sorted descending."""
93
+ since = _since_dt(args.since)
94
+ records = _load_records(since, agent=None)
95
+ if not records:
96
+ print("No records found.")
97
+ return 0
98
+ totals: dict[str, int] = {}
99
+ violations: dict[str, int] = {}
100
+ for rec in records:
101
+ aid = rec.get("agent_id", "?")
102
+ totals[aid] = totals.get(aid, 0) + 1
103
+ if rec.get("violation_count", 0) > 0:
104
+ violations[aid] = violations.get(aid, 0) + 1
105
+ rows = sorted(
106
+ totals.keys(),
107
+ key=lambda a: violations.get(a, 0) / totals[a],
108
+ reverse=True,
109
+ )
110
+ print(f"{'agent':<32} {'total':>6} {'violations':>10} {'rate':>8}")
111
+ print("-" * 62)
112
+ for aid in rows:
113
+ total = totals[aid]
114
+ viol = violations.get(aid, 0)
115
+ rate = viol / total * 100
116
+ print(f"{aid:<32} {total:>6} {viol:>10} {rate:>7.1f}%")
117
+ return 0
118
+
119
+
120
+ def _build_parser() -> argparse.ArgumentParser:
121
+ parser = argparse.ArgumentParser(
122
+ prog="python -m core.governance.dna_fidelity_cli",
123
+ description="Inspect DNA fidelity telemetry records.",
124
+ )
125
+ subparsers = parser.add_subparsers(dest="cmd", required=True)
126
+
127
+ list_p = subparsers.add_parser("list", help="List fidelity records, most recent first.")
128
+ list_p.add_argument("--agent", default=None, help="Filter by agent_id")
129
+ list_p.add_argument("--since", type=int, default=None, help="Show records from last N days")
130
+ list_p.add_argument("--limit", type=int, default=20, help="Max records (default 20)")
131
+
132
+ summary_p = subparsers.add_parser("summary", help="Aggregate violation rate per agent.")
133
+ summary_p.add_argument("--since", type=int, default=None, help="Aggregate last N days only")
134
+ return parser
135
+
136
+
137
+ def main(argv: list[str] | None = None) -> int:
138
+ parser = _build_parser()
139
+ args = parser.parse_args(argv if argv is not None else sys.argv[1:])
140
+ if args.cmd == "list":
141
+ return _do_list(args)
142
+ if args.cmd == "summary":
143
+ return _do_summary(args)
144
+ parser.print_help()
145
+ return 2
146
+
147
+
148
+ if __name__ == "__main__": # pragma: no cover
149
+ sys.exit(main())
@@ -71,3 +71,21 @@ communication:
71
71
  avoid:
72
72
  - "blame language"
73
73
  - "vague task assignments"
74
+
75
+ # PR5 v3.76.0 — DNA fidelity markers (soft-warn). Regex case-insensitive.
76
+ # Paulo (lead, supportive style) does not gate on opening phrases — only
77
+ # detects sycophant/placeholder language that contradicts his DISC=I
78
+ # servant-leader tone.
79
+ signature_markers:
80
+ opening_phrases: []
81
+ typical_patterns:
82
+ - "vamos"
83
+ - "alinhamos"
84
+ - "OK"
85
+ closing_style: null
86
+ avoid_patterns:
87
+ - "you're absolutely right"
88
+ - "amazing work"
89
+ - "great job, everyone"
90
+ - "I appreciate your patience"
91
+ - "thanks for understanding"
@@ -72,3 +72,26 @@ communication:
72
72
  - "approving text with known issues"
73
73
  - "subjective opinions without evidence"
74
74
  - "AI cliches: leverage, utilize, robust, streamline, cutting-edge"
75
+
76
+ # PR5 v3.76.0 — DNA fidelity markers (soft-warn). Regex case-insensitive.
77
+ signature_markers:
78
+ opening_phrases:
79
+ - "Copy & Language"
80
+ - "Reviewed"
81
+ typical_patterns:
82
+ - "PASS"
83
+ - "FAIL"
84
+ - "pt-PT"
85
+ - "AI cliche"
86
+ closing_style: null
87
+ avoid_patterns:
88
+ - "delve into"
89
+ - "tapestry"
90
+ - "in today's fast-paced"
91
+ - "navigate the landscape"
92
+ - "underscore"
93
+ - "leverage"
94
+ - "utilize"
95
+ - "robust"
96
+ - "streamline"
97
+ - "cutting-edge"
@@ -75,3 +75,23 @@ communication:
75
75
  - "ambiguous quality feedback"
76
76
  - "soft approvals with caveats"
77
77
  - "skipping review steps"
78
+
79
+ # PR5 v3.76.0 — DNA fidelity markers (soft-warn). Regex case-insensitive.
80
+ signature_markers:
81
+ opening_phrases:
82
+ - "Quality Gate Verdict:"
83
+ - "Verdict:"
84
+ typical_patterns:
85
+ - "REJECTED"
86
+ - "APPROVED"
87
+ - "Final:"
88
+ - "does not"
89
+ closing_style: "Final:"
90
+ avoid_patterns:
91
+ - "you're absolutely right"
92
+ - "I appreciate your"
93
+ - "happy to help"
94
+ - "great question"
95
+ - "let me know if"
96
+ - "softening"
97
+ - "no offense"
@@ -78,3 +78,24 @@ communication:
78
78
  - "vague feedback without specific location"
79
79
  - "approving with known technical debt"
80
80
  - "skipping test coverage check"
81
+
82
+ # PR5 v3.76.0 — DNA fidelity markers (soft-warn). Regex case-insensitive.
83
+ signature_markers:
84
+ opening_phrases:
85
+ - "Technical & UX"
86
+ - "Technical & UX Quality"
87
+ typical_patterns:
88
+ - "B1."
89
+ - "M1."
90
+ - "PASS"
91
+ - "FAIL"
92
+ - "30-line"
93
+ closing_style: null
94
+ avoid_patterns:
95
+ - "I think"
96
+ - "I believe"
97
+ - "perhaps"
98
+ - "kind of"
99
+ - "sort of"
100
+ - "might be a"
101
+ - "could be a problem"
@@ -184,6 +184,8 @@ export async function update() {
184
184
  "post-tool-use",
185
185
  "pre-compact",
186
186
  "cwd-changed",
187
+ "pre-tool-use",
188
+ "stop",
187
189
  ];
188
190
  const hookExt = HOOK_EXT;
189
191
  const srcHooksDir = join(ARKAOS_ROOT, "config", "hooks");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.75.1",
3
+ "version": "3.76.0",
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 = "3.75.1"
3
+ version = "3.76.0"
4
4
  description = "Core engine for ArkaOS — The Operating System for AI Agent Teams"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}