arkaos 3.75.2 → 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 +1 -1
- package/arka/skills/flow/SKILL.md +12 -0
- package/config/constitution.yaml +5 -0
- package/config/hooks/post-tool-use.sh +27 -0
- package/config/hooks/stop.sh +57 -0
- package/core/governance/__pycache__/activation_tracker.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/agent_activation_cli.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/dna_fidelity.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/dna_fidelity_cli.cpython-313.pyc +0 -0
- package/core/governance/activation_tracker.py +178 -0
- package/core/governance/agent_activation_cli.py +107 -0
- package/core/governance/dna_fidelity.py +246 -0
- package/core/governance/dna_fidelity_cli.py +149 -0
- package/core/synapse/__pycache__/pattern_library_layer.cpython-313.pyc +0 -0
- package/departments/dev/agents/tech-lead.yaml +18 -0
- package/departments/quality/agents/copy-director.yaml +23 -0
- package/departments/quality/agents/cqo.yaml +20 -0
- package/departments/quality/agents/tech-director.yaml +21 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
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.
|
package/config/constitution.yaml
CHANGED
|
@@ -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
|
package/config/hooks/stop.sh
CHANGED
|
@@ -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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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())
|
|
Binary file
|
|
@@ -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