arkaos 3.75.2 → 3.77.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.2
1
+ 3.77.0
@@ -81,6 +81,20 @@ spec why divergence is justified. Manual audit:
81
81
  Dispatch specialists via the `Agent` tool. The squad lead from Phase 3
82
82
  names them. Specialists run in parallel when work is independent.
83
83
 
84
+ **Design-system check on UI work (PR6 v3.77.0, SHOULD `design-system-locked`).**
85
+ Before dispatching frontend/landing specialists, run the per-project
86
+ linter to surface UI/UX drift:
87
+
88
+ ```
89
+ python -m core.governance.design_system_lint_cli <project_path>
90
+ ```
91
+
92
+ If the project has a `design-system.yaml`, violations come back with
93
+ file:line + the suggestion text declared in the YAML. The frontend
94
+ specialist should fix existing violations OR document why the new work
95
+ diverges. Projects without a `design-system.yaml` skip the check.
96
+ Template: `docs/examples/design-system-example.yaml`.
97
+
84
98
  **Experience injection (PR3 v3.74.0).** When a specialist is dispatched,
85
99
  Synapse layer `L2.6 AgentExperiences`
86
100
  (`core/synapse/agent_experiences_layer.py`) detects the
@@ -194,6 +208,18 @@ For each item, in order:
194
208
  Do not skip items. Do not batch QA or Security across multiple
195
209
  items — each item runs the full gate chain.
196
210
 
211
+ **DNA fidelity check at turn end (PR5 v3.76.0, SHOULD `dna-fidelity-warn`).**
212
+ The Stop hook (`config/hooks/stop.sh`) invokes
213
+ `core.governance.dna_fidelity.check_fidelity(agent_id, output)` for the
214
+ current persona (from `[arka:routing]` or `[arka:dispatch]`). Violations
215
+ of `avoid_patterns` or missing `opening_phrases` from the agent's YAML
216
+ `signature_markers` block are recorded to
217
+ `~/.arkaos/telemetry/dna-fidelity.jsonl` — soft-warn only in v3.76.0.
218
+ Operator audit: `python -m core.governance.dna_fidelity_cli summary`.
219
+ Each Agent dispatch also logs to
220
+ `~/.arkaos/telemetry/agent-activations.jsonl`; surface dormant agents
221
+ via `python -m core.governance.agent_activation_cli dead`.
222
+
197
223
  ### Phase 13 — Detailed summary
198
224
  When the TODO list is exhausted, emit a final summary: what was done,
199
225
  where it lives, how to verify, what is open for next time.
@@ -217,6 +217,16 @@ 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 PR6 Squad Intelligence Upgrade (2026-05-28) ───────
221
+ - id: design-system-locked
222
+ rule: "Each project SHOULD declare a `design-system.yaml` at its root listing tokens (colors, spacing, fonts), allowed_components, file_globs to scan, and forbidden_patterns with suggestions. The per-project linter (core.governance.design_system_lint) detects UI/UX drift — hex literals outside the palette, inline style attributes, raw HTML where a Nuxt UI component exists, etc. Adoption is opt-in per project (no YAML at project root → no violations). v3.77.0 is advisory-only; pre-commit hook integration lands in v3.77.x once the rule sets stabilise across the operator's projects."
223
+ enforcement: "Run via `python -m core.governance.design_system_lint_cli <project_path>` (text or JSON output, optional --exit-on-violations). Example template at `docs/examples/design-system-example.yaml`."
224
+
225
+ # ─── Rule added in PR5 Squad Intelligence Upgrade (2026-05-28) ───────
226
+ - id: dna-fidelity-warn
227
+ 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."
228
+ 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."
229
+
220
230
  # ─── Rule added in PR4 Squad Intelligence Upgrade (2026-05-28) ───────
221
231
  - id: pattern-library-first
222
232
  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,197 @@
1
+ """Design System Linter — PR6 Squad Intelligence Upgrade v3.77.0.
2
+
3
+ Scans a project for forbidden patterns declared in its design-system.yaml.
4
+ Opt-in per project: no YAML at root means no violations. Advisory-only in
5
+ v3.77.0; pre-commit hook integration lands in v3.77.x.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from dataclasses import asdict, dataclass, field # noqa: F401 (asdict kept for callers)
12
+ from pathlib import Path
13
+ from typing import Iterator
14
+
15
+ import yaml
16
+
17
+
18
+ # ─── Dataclasses ──────────────────────────────────────────────────────────────
19
+
20
+ @dataclass
21
+ class DesignSystem:
22
+ """Loaded design-system.yaml for a project."""
23
+
24
+ version: int = 1
25
+ project: str = ""
26
+ tokens: dict = field(default_factory=dict)
27
+ allowed_components: list[str] = field(default_factory=list)
28
+ file_globs: list[str] = field(default_factory=list)
29
+ forbidden_patterns: list[dict] = field(default_factory=list)
30
+
31
+
32
+ @dataclass
33
+ class DesignViolation:
34
+ file: str # relative to project_path, forward slashes
35
+ line: int # 1-indexed
36
+ pattern: str # the regex string from forbidden_patterns
37
+ suggestion: str # suggestion text from forbidden_patterns
38
+ matched_text: str # the actual matched substring (cap 200 chars)
39
+
40
+
41
+ # ─── Internal helpers ─────────────────────────────────────────────────────────
42
+
43
+ def _default_file_globs() -> list[str]:
44
+ return ["**/*.vue", "**/*.tsx", "**/*.jsx"]
45
+
46
+
47
+ def _escape_glob_literal(s: str) -> str:
48
+ """Escape a literal glob segment (no ** inside)."""
49
+ return re.escape(s).replace(r"\*", "[^/]*").replace(r"\?", "[^/]")
50
+
51
+
52
+ def _glob_to_regex(glob_pattern: str) -> re.Pattern[str]:
53
+ """Translate a glob pattern (with ** support) to a compiled regex.
54
+
55
+ Gitignore convention: ``**/`` = zero-or-more ``dir/`` prefixes (including
56
+ zero), so ``**/*.vue`` matches both ``App.vue`` and ``src/App.vue``.
57
+ Bare ``*`` = any non-slash run; ``?`` = one non-slash char.
58
+ """
59
+ # Split on ** tokens, keeping them as delimiters.
60
+ tokens = re.split(r"(\*\*)", glob_pattern)
61
+ result = ""
62
+ for i, tok in enumerate(tokens):
63
+ if tok != "**":
64
+ result += _escape_glob_literal(tok)
65
+ continue
66
+ nxt = tokens[i + 1] if i + 1 < len(tokens) else ""
67
+ if nxt.startswith("/"):
68
+ # **/ → consume the slash, emit zero-or-more dir/ groups.
69
+ tokens[i + 1] = nxt[1:]
70
+ result += "(?:[^/]+/)*"
71
+ else:
72
+ # trailing ** or ** not followed by / → match any remaining path.
73
+ result += ".*"
74
+ return re.compile(f"^{result}$")
75
+
76
+
77
+ def _glob_match(pattern: str, rel_path: str) -> bool:
78
+ """Return True when rel_path (forward slashes) matches the glob pattern."""
79
+ try:
80
+ return bool(_glob_to_regex(pattern).match(rel_path))
81
+ except re.error:
82
+ return False
83
+
84
+
85
+ def _iter_matching_files(
86
+ project_path: Path, file_globs: list[str]
87
+ ) -> Iterator[Path]:
88
+ """Yield all files under project_path matching any glob in file_globs."""
89
+ seen: set[Path] = set()
90
+ for glob in file_globs:
91
+ for path in project_path.rglob("*"):
92
+ if not path.is_file():
93
+ continue
94
+ rel = path.relative_to(project_path).as_posix()
95
+ if _glob_match(glob, rel) and path not in seen:
96
+ seen.add(path)
97
+ yield path
98
+
99
+
100
+ def _is_excluded(rel_path: str, exclude_paths: list[str]) -> bool:
101
+ """Return True if rel_path matches any exclude_paths glob or is design-system.yaml."""
102
+ if rel_path == "design-system.yaml":
103
+ return True
104
+ return any(_glob_match(exc, rel_path) for exc in exclude_paths)
105
+
106
+
107
+ def _scan_file_for_pattern(
108
+ file_path: Path,
109
+ project_path: Path,
110
+ compiled_re: re.Pattern[str],
111
+ pattern: str,
112
+ suggestion: str,
113
+ ) -> Iterator[DesignViolation]:
114
+ """Read one file and yield a DesignViolation for every regex match."""
115
+ rel = file_path.relative_to(project_path).as_posix()
116
+ try:
117
+ text = file_path.read_text(encoding="utf-8", errors="replace")
118
+ except OSError:
119
+ return
120
+ for lineno, line in enumerate(text.splitlines(), start=1):
121
+ for match in compiled_re.finditer(line):
122
+ yield DesignViolation(
123
+ file=rel,
124
+ line=lineno,
125
+ pattern=pattern,
126
+ suggestion=suggestion,
127
+ matched_text=match.group(0)[:200],
128
+ )
129
+
130
+
131
+ # ─── Public API ───────────────────────────────────────────────────────────────
132
+
133
+ def load_design_system(project_path: Path) -> DesignSystem | None:
134
+ """Load design-system.yaml from project_path root.
135
+
136
+ Returns None when the file is absent or malformed.
137
+ """
138
+ yaml_path = project_path / "design-system.yaml"
139
+ if not yaml_path.is_file():
140
+ return None
141
+ try:
142
+ raw = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
143
+ except (OSError, yaml.YAMLError):
144
+ return None
145
+ if not isinstance(raw, dict):
146
+ return None
147
+ return DesignSystem(
148
+ version=int(raw.get("version") or 1),
149
+ project=str(raw.get("project") or ""),
150
+ tokens=dict(raw.get("tokens") or {}),
151
+ allowed_components=list(raw.get("allowed_components") or []),
152
+ file_globs=list(raw.get("file_globs") or []),
153
+ forbidden_patterns=list(raw.get("forbidden_patterns") or []),
154
+ )
155
+
156
+
157
+ def lint_project(project_path: Path) -> list[DesignViolation]:
158
+ """Scan project_path for design-system violations.
159
+
160
+ Returns an empty list when the project path does not exist, has no
161
+ design-system.yaml, or the YAML is malformed.
162
+ """
163
+ if not project_path.is_dir():
164
+ return []
165
+ ds = load_design_system(project_path)
166
+ if ds is None:
167
+ return []
168
+ globs = ds.file_globs if ds.file_globs else _default_file_globs()
169
+ violations: list[DesignViolation] = []
170
+ for fp_dict in ds.forbidden_patterns:
171
+ if not isinstance(fp_dict, dict):
172
+ continue
173
+ _collect_pattern_violations(project_path, globs, fp_dict, violations)
174
+ return violations
175
+
176
+
177
+ def _collect_pattern_violations(
178
+ project_path: Path,
179
+ file_globs: list[str],
180
+ fp_dict: dict,
181
+ violations: list[DesignViolation],
182
+ ) -> None:
183
+ """Compile one forbidden pattern and append matching violations in-place."""
184
+ raw_pattern = fp_dict.get("pattern", "")
185
+ suggestion = fp_dict.get("suggestion", "")
186
+ exclude_paths: list[str] = list(fp_dict.get("exclude_paths") or [])
187
+ try:
188
+ compiled = re.compile(raw_pattern)
189
+ except re.error:
190
+ return
191
+ for file_path in _iter_matching_files(project_path, file_globs):
192
+ rel = file_path.relative_to(project_path).as_posix()
193
+ if _is_excluded(rel, exclude_paths):
194
+ continue
195
+ violations.extend(
196
+ _scan_file_for_pattern(file_path, project_path, compiled, raw_pattern, suggestion)
197
+ )
@@ -0,0 +1,92 @@
1
+ """CLI for the Design System Linter (PR6 Squad Intelligence Upgrade v3.77.0).
2
+
3
+ Usage:
4
+ python -m core.governance.design_system_lint_cli <project_path> [--format text|json] [--exit-on-violations]
5
+
6
+ Examples:
7
+ python -m core.governance.design_system_lint_cli /path/to/project
8
+ python -m core.governance.design_system_lint_cli /path/to/project --format json --exit-on-violations
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import sys
16
+ from collections import defaultdict
17
+ from pathlib import Path
18
+
19
+ from core.governance.design_system_lint import (
20
+ DesignViolation,
21
+ lint_project,
22
+ )
23
+
24
+
25
+ def _build_parser() -> argparse.ArgumentParser:
26
+ parser = argparse.ArgumentParser(
27
+ prog="python -m core.governance.design_system_lint_cli",
28
+ description="Scan a project for design-system violations.",
29
+ )
30
+ parser.add_argument("project_path", help="Path to the project root containing design-system.yaml.")
31
+ parser.add_argument(
32
+ "--format",
33
+ choices=["text", "json"],
34
+ default="text",
35
+ help="Output format (default: text).",
36
+ )
37
+ parser.add_argument(
38
+ "--exit-on-violations",
39
+ action="store_true",
40
+ help="Exit with code 1 when violations are found.",
41
+ )
42
+ return parser
43
+
44
+
45
+ def _print_text(violations: list[DesignViolation]) -> None:
46
+ """Group violations by file, sorted by file then line, and pretty-print."""
47
+ print(f"{len(violations)} design-system violation(s) found:")
48
+ by_file: dict[str, list[DesignViolation]] = defaultdict(list)
49
+ for v in violations:
50
+ by_file[v.file].append(v)
51
+ for file in sorted(by_file):
52
+ for v in sorted(by_file[file], key=lambda x: x.line):
53
+ truncated = v.matched_text[:60] + ("..." if len(v.matched_text) > 60 else "")
54
+ print(f" {v.file}:{v.line} {truncated}")
55
+ print(f" → {v.suggestion}")
56
+
57
+
58
+ def _print_json(violations: list[DesignViolation]) -> None:
59
+ """Emit one JSON line per violation followed by a summary line (jsonl)."""
60
+ for v in violations:
61
+ print(json.dumps({
62
+ "file": v.file,
63
+ "line": v.line,
64
+ "pattern": v.pattern,
65
+ "suggestion": v.suggestion,
66
+ "matched_text": v.matched_text,
67
+ }))
68
+ print(json.dumps({"summary": True, "count": len(violations)}))
69
+
70
+
71
+ def main(argv: list[str] | None = None) -> int:
72
+ parser = _build_parser()
73
+ args = parser.parse_args(argv if argv is not None else sys.argv[1:])
74
+ violations = lint_project(Path(args.project_path))
75
+
76
+ if not violations:
77
+ if args.format == "json":
78
+ print(json.dumps({"violations": [], "count": 0}))
79
+ else:
80
+ print("No design-system violations.")
81
+ return 0
82
+
83
+ if args.format == "json":
84
+ _print_json(violations)
85
+ else:
86
+ _print_text(violations)
87
+
88
+ return 1 if args.exit_on_violations else 0
89
+
90
+
91
+ if __name__ == "__main__": # pragma: no cover
92
+ 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"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.75.2",
3
+ "version": "3.77.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.2"
3
+ version = "3.77.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"}