arkaos 3.71.1 → 3.73.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.71.1
1
+ 3.73.0
@@ -73,6 +73,26 @@ state the gap explicitly and propose filling it.
73
73
  Dispatch specialists via the `Agent` tool. The squad lead from Phase 3
74
74
  names them. Specialists run in parallel when work is independent.
75
75
 
76
+ **Dispatch must be announced (NON-NEGOTIABLE `dispatch-must-be-announced`).**
77
+ Immediately before each `Agent` tool call, emit on its own line:
78
+
79
+ ```
80
+ [arka:dispatch] <calling-persona> -> <specialist-id>
81
+ ```
82
+
83
+ Example before dispatching a frontend specialist from Paulo's seat:
84
+
85
+ ```
86
+ [arka:dispatch] paulo -> frontend-dev
87
+ ```
88
+
89
+ The PreToolUse specialist-enforcer (`core/workflow/specialist_enforcer.py`)
90
+ reads this marker to identify which specialist holds the floor. Without
91
+ it, the specialist's subsequent `Write`/`Edit` will block when
92
+ `hooks.specialistEnforcement=true`. The marker format mirrors
93
+ `[arka:routing]` exactly but uses the verb `dispatch` and points from
94
+ the caller to the receiver.
95
+
76
96
  ### Phase 7 — Plan and make the spec
77
97
  Run six parallel reviewers on the plan:
78
98
 
@@ -0,0 +1,169 @@
1
+ # ============================================================================
2
+ # ArkaOS — Agent File Ownership
3
+ #
4
+ # Maps file path patterns to the specialist agents (Tier 2) who own writes
5
+ # to those files. Squad leads (Tier 1) MUST dispatch the specialist via
6
+ # the Agent tool before writing to owned files — otherwise the PreToolUse
7
+ # hook blocks the write.
8
+ #
9
+ # Read by: core/workflow/specialist_enforcer.py
10
+ # Hook: config/hooks/pre-tool-use.sh (between KB-gate and flow-gate)
11
+ #
12
+ # Bypass: emit `[arka:specialist-bypass <reason>]` in the same assistant
13
+ # message immediately before the Write/Edit. Bypass is logged to
14
+ # ~/.arkaos/telemetry/specialist-dispatch.jsonl for accountability.
15
+ #
16
+ # Feature flag: hooks.specialistEnforcement in ~/.arkaos/config.json
17
+ # (defaults to false during rollout — promote to true after telemetry
18
+ # shows the rule set is stable).
19
+ # ============================================================================
20
+
21
+ version: 1
22
+
23
+ # Squad leads (Tier 1) — orchestrate, dispatch specialists, do not implement.
24
+ # The routing tag `[arka:routing] <dept> -> <name>` identifies which lead
25
+ # currently holds the floor. Lead names are normalized to lowercase by
26
+ # the enforcer before comparison.
27
+ leads:
28
+ - paulo # dev — Tech Lead
29
+ - luna # mkt — Marketing Lead
30
+ - valentina # brand — Brand Lead
31
+ - helena # fin — CFO (also c_suite)
32
+ - tomas # strat — Strategy Lead
33
+ - ricardo # ecom — E-commerce Lead
34
+ - clara # kb — Knowledge Lead
35
+ - daniel # ops — Operations Lead
36
+ - carolina # pm — Project Management Lead
37
+ - tiago # saas — SaaS Lead
38
+ - ines # landing — Landing Lead
39
+ - rafael # content — Content Lead
40
+ - beatriz # community — Community Lead
41
+ - miguel # sales — Sales Lead
42
+ - rodrigo # leadership — People Lead
43
+ - sofia # org — COO (also c_suite)
44
+
45
+ # C-Suite (Tier 0) — veto power, may write anywhere without dispatch.
46
+ # Listed here so the enforcer treats them as always-authorized.
47
+ c_suite:
48
+ - marco # CTO
49
+ - marta # CQO
50
+ - eduardo # Copy Director
51
+ - francisca # Tech Director
52
+ - sofia # COO
53
+ - helena # CFO
54
+
55
+ # Ownership rules — evaluated top-to-bottom, FIRST match wins.
56
+ # Patterns use glob syntax (fnmatch + ** for recursive directory).
57
+ # `owners` lists the specialist agent IDs (kebab-case) authorized to write.
58
+ # A persona currently holding the routing floor must match one of `owners`
59
+ # (case-insensitive). Leads always fail unless they appear in `owners`.
60
+ ownership:
61
+ # ─── Frontend ────────────────────────────────────────────────────────
62
+ - pattern: "**/*.vue"
63
+ owners: [frontend-dev]
64
+ reason: "Vue templates require frontend specialist"
65
+
66
+ - pattern: "**/*.tsx"
67
+ owners: [frontend-dev]
68
+ reason: "React TSX requires frontend specialist"
69
+
70
+ - pattern: "**/*.jsx"
71
+ owners: [frontend-dev]
72
+ reason: "React JSX requires frontend specialist"
73
+
74
+ - pattern: "**/components/**"
75
+ owners: [frontend-dev]
76
+ reason: "Component layer is frontend specialist territory"
77
+
78
+ # ─── Backend (PHP/Laravel) ───────────────────────────────────────────
79
+ - pattern: "**/app/Services/**"
80
+ owners: [senior-dev, backend-dev]
81
+ reason: "Service layer requires backend specialist"
82
+
83
+ - pattern: "**/app/Repositories/**"
84
+ owners: [senior-dev, backend-dev]
85
+ reason: "Repository layer requires backend specialist"
86
+
87
+ - pattern: "**/app/Http/Controllers/**"
88
+ owners: [senior-dev, backend-dev]
89
+ reason: "Controllers require backend specialist"
90
+
91
+ - pattern: "**/database/migrations/**"
92
+ owners: [dba, backend-dev]
93
+ reason: "Migrations require DBA review"
94
+
95
+ # ─── Security-sensitive ──────────────────────────────────────────────
96
+ - pattern: "**/.env*"
97
+ owners: [security-eng]
98
+ reason: "Environment files require security specialist"
99
+
100
+ - pattern: "**/auth/**"
101
+ owners: [security-eng, backend-dev]
102
+ reason: "Auth code requires security review"
103
+
104
+ - pattern: "core/security/**"
105
+ owners: [security-eng]
106
+ reason: "Core security module is security specialist territory"
107
+
108
+ - pattern: "config/hooks/**"
109
+ owners: [security-eng, devops-eng]
110
+ reason: "Hooks affect runtime behavior — security + devops review required"
111
+
112
+ # ─── DevOps / Infrastructure ─────────────────────────────────────────
113
+ - pattern: ".github/workflows/**"
114
+ owners: [devops-eng]
115
+ reason: "CI/CD workflows are devops specialist territory"
116
+
117
+ - pattern: "**/Dockerfile*"
118
+ owners: [devops-eng]
119
+ reason: "Container builds require devops specialist"
120
+
121
+ - pattern: "infrastructure/**"
122
+ owners: [devops-eng]
123
+ reason: "Infrastructure-as-code requires devops specialist"
124
+
125
+ # ─── Core architecture ──────────────────────────────────────────────
126
+ - pattern: "core/workflow/**/*.py"
127
+ owners: [architect, senior-dev]
128
+ reason: "Workflow engine requires architecture review"
129
+
130
+ - pattern: "core/agents/**/*.py"
131
+ owners: [architect, senior-dev]
132
+ reason: "Agent schema/loader requires architecture review"
133
+
134
+ - pattern: "core/governance/**/*.py"
135
+ owners: [architect, security-eng]
136
+ reason: "Governance code requires architecture + security review"
137
+
138
+ - pattern: "docs/adr/**"
139
+ owners: [architect]
140
+ reason: "ADRs are architect's responsibility"
141
+
142
+ # ─── Open-access (any persona may write) ─────────────────────────────
143
+ # Tests are written by the specialist who owns the code under test.
144
+ # qa-eng owns test STRATEGY, not individual test files.
145
+ - pattern: "**/tests/**"
146
+ owners: ["*"]
147
+ reason: "Tests written by specialist who owns code under test"
148
+
149
+ - pattern: "**/*.md"
150
+ owners: ["*"]
151
+ reason: "Markdown docs are open to any persona"
152
+
153
+ # Lead-allowed files — leads + c-suite ALWAYS allowed without dispatch.
154
+ # Used for cross-cutting files that touch many specialists.
155
+ lead_allowed:
156
+ - "CHANGELOG.md"
157
+ - "VERSION"
158
+ - "package.json"
159
+ - "package-lock.json"
160
+ - "pyproject.toml"
161
+ - "uv.lock"
162
+ - "README.md"
163
+ - "CLAUDE.md"
164
+ - "CONSTITUTION.md"
165
+ - "config/agent-ownership.yaml" # bootstrap — this file
166
+ - "config/constitution.yaml"
167
+ - "**/*.yaml" # squad/workflow configs
168
+ - "**/*.yml"
169
+ - "knowledge/**" # KB curation
@@ -121,6 +121,11 @@ enforcement_levels:
121
121
  rule: "ArkaOS learns from user corrections via hybrid mechanism: implicit auto-detection with confidence scoring for typical corrections (default), explicit Marta-led confirmation for high-leverage rules (NON-NEGOTIABLE candidates) or rules that contradict existing memory. Marta is the owner of the learning loop. Memory rules carry a confidence field that climbs as the rule is applied without correction."
122
122
  enforcement: "Correction signals detected via absolute-language keywords ('sempre', 'nunca', 'no exceptions') and correction magnitude; [arka:learned-rule confidence=X] tag emitted when rule auto-saved; explicit Marta confirmation question fired when rule is high-leverage or conflicts with existing memory."
123
123
 
124
+ # ─── Rule added in PR1 Squad Intelligence Upgrade (2026-05-28) ───────
125
+ - id: dispatch-must-be-announced
126
+ rule: "When a squad lead dispatches a specialist via the Agent tool, the lead MUST emit `[arka:dispatch] <from> -> <to>` on a line of its own immediately before the dispatch call. The marker identifies the specialist to the PreToolUse specialist-enforcer so writes from the specialist pass `owner-match` instead of falling through as `no-routing-tag` or blocking the lead. The format is identical to `[arka:routing]` but uses the verb `dispatch` and points from the calling persona to the receiving specialist (e.g., `[arka:dispatch] paulo -> frontend-dev`)."
127
+ enforcement: "PreToolUse hook (config/hooks/pre-tool-use.sh) reads the dispatch marker via core.workflow.specialist_enforcer._resolve_persona. Dispatch tag overrides routing tag because it is more specific. Without it, lead writes to specialist-owned files are blocked when hooks.specialistEnforcement=true. See ADR docs/adr/2026-05-28-specialist-dispatch-subagent-blindspot.md for the architectural constraint this rule mitigates."
128
+
124
129
  quality_gate:
125
130
  description: "Mandatory pre-delivery review. Nothing ships without APPROVED verdict."
126
131
  trigger: "After the last execution phase, before delivery to user"
@@ -116,6 +116,83 @@ print(json.dumps({
116
116
  }
117
117
  }
118
118
 
119
+ # --- Specialist-dispatch gate (between KB-gate and flow-gate) ---
120
+ # Blocks Tier-1 leads from writing to specialist-owned files without
121
+ # dispatching first. Only fires for file-mutation tools.
122
+ $specialistPy = Join-Path $env:ARKAOS_ROOT "core/workflow/specialist_enforcer.py"
123
+ if ((Test-Path $specialistPy) -and ($toolName -in @("Write","Edit","MultiEdit","NotebookEdit"))) {
124
+ $toolInputJson = "{}"
125
+ if ($inp.tool_input) {
126
+ $toolInputJson = ($inp.tool_input | ConvertTo-Json -Compress -Depth 10)
127
+ }
128
+ $env:TOOL_NAME = $toolName
129
+ $env:TRANSCRIPT_PATH = $transcriptPath
130
+ $env:SESSION_ID = $sessionId
131
+ $env:CWD = $cwd
132
+ $env:TOOL_INPUT_JSON = $toolInputJson
133
+
134
+ $spScript = @'
135
+ import json
136
+ import os
137
+ import sys
138
+
139
+ sys.path.insert(0, os.environ["ARKAOS_ROOT"])
140
+ try:
141
+ from core.workflow.specialist_enforcer import evaluate, record_telemetry
142
+ except Exception:
143
+ print(json.dumps({"allow": True, "reason": "specialist-import-failed"}))
144
+ sys.exit(0)
145
+
146
+ try:
147
+ tool_input = json.loads(os.environ.get("TOOL_INPUT_JSON", "{}"))
148
+ except json.JSONDecodeError:
149
+ tool_input = {}
150
+
151
+ decision = evaluate(
152
+ tool_name=os.environ.get("TOOL_NAME", ""),
153
+ transcript_path=os.environ.get("TRANSCRIPT_PATH", ""),
154
+ session_id=os.environ.get("SESSION_ID", ""),
155
+ cwd=os.environ.get("CWD", ""),
156
+ tool_input=tool_input,
157
+ )
158
+ try:
159
+ record_telemetry(
160
+ session_id=os.environ.get("SESSION_ID", ""),
161
+ tool=os.environ.get("TOOL_NAME", ""),
162
+ decision=decision,
163
+ cwd=os.environ.get("CWD", ""),
164
+ target_file=str(tool_input.get("file_path", "")),
165
+ )
166
+ except Exception:
167
+ pass
168
+ print(json.dumps({
169
+ "allow": decision.allow,
170
+ "reason": decision.reason,
171
+ "stderr_msg": decision.to_stderr_message(),
172
+ }))
173
+ '@
174
+
175
+ $spDecisionJson = $spScript | & $python.Source -
176
+ if (-not [string]::IsNullOrWhiteSpace($spDecisionJson)) {
177
+ try {
178
+ $spDecision = $spDecisionJson | ConvertFrom-Json
179
+ } catch { $spDecision = $null }
180
+
181
+ if ($spDecision -and -not $spDecision.allow) {
182
+ [Console]::Error.WriteLine($spDecision.stderr_msg)
183
+ $denyOut = @{
184
+ hookSpecificOutput = @{
185
+ hookEventName = "PreToolUse"
186
+ permissionDecision = "deny"
187
+ permissionDecisionReason = $spDecision.stderr_msg
188
+ }
189
+ } | ConvertTo-Json -Compress -Depth 5
190
+ Write-Output $denyOut
191
+ exit 2
192
+ }
193
+ }
194
+ }
195
+
119
196
  # --- Fast allow: not a flow-gated tool ---
120
197
  if ($toolName -ne "Write" -and $toolName -ne "Edit" -and $toolName -ne "MultiEdit") {
121
198
  exit 0
@@ -120,6 +120,86 @@ PY
120
120
  fi
121
121
  fi
122
122
 
123
+ # ─── Specialist-dispatch gate (between KB-gate and flow-gate) ──────────
124
+ # Blocks Tier-1 leads from writing to specialist-owned files without
125
+ # dispatching first. Only fires for file-mutation tools. Independent
126
+ # feature flag (`hooks.specialistEnforcement`) — fails open if the
127
+ # Python module is absent or raises.
128
+ if [ -f "$ARKAOS_ROOT/core/workflow/specialist_enforcer.py" ]; then
129
+ case "$TOOL_NAME" in
130
+ Write|Edit|MultiEdit|NotebookEdit)
131
+ TOOL_INPUT_JSON_SP="{}"
132
+ if command -v jq &>/dev/null; then
133
+ TOOL_INPUT_JSON_SP=$(echo "$input" | jq -c '.tool_input // {}' 2>/dev/null)
134
+ fi
135
+ SP_DECISION_JSON=$(TOOL_NAME="$TOOL_NAME" \
136
+ TRANSCRIPT_PATH="$TRANSCRIPT_PATH" \
137
+ SESSION_ID="$SESSION_ID" \
138
+ CWD="$CWD" \
139
+ TOOL_INPUT_JSON="$TOOL_INPUT_JSON_SP" \
140
+ ARKAOS_ROOT="$ARKAOS_ROOT" \
141
+ python3 - <<'PY' 2>/dev/null
142
+ import json
143
+ import os
144
+ import sys
145
+
146
+ sys.path.insert(0, os.environ["ARKAOS_ROOT"])
147
+ try:
148
+ from core.workflow.specialist_enforcer import evaluate, record_telemetry
149
+ except Exception:
150
+ print(json.dumps({"allow": True, "reason": "specialist-import-failed"}))
151
+ sys.exit(0)
152
+
153
+ try:
154
+ tool_input = json.loads(os.environ.get("TOOL_INPUT_JSON", "{}"))
155
+ except json.JSONDecodeError:
156
+ tool_input = {}
157
+
158
+ decision = evaluate(
159
+ tool_name=os.environ.get("TOOL_NAME", ""),
160
+ transcript_path=os.environ.get("TRANSCRIPT_PATH", ""),
161
+ session_id=os.environ.get("SESSION_ID", ""),
162
+ cwd=os.environ.get("CWD", ""),
163
+ tool_input=tool_input,
164
+ )
165
+ try:
166
+ record_telemetry(
167
+ session_id=os.environ.get("SESSION_ID", ""),
168
+ tool=os.environ.get("TOOL_NAME", ""),
169
+ decision=decision,
170
+ cwd=os.environ.get("CWD", ""),
171
+ target_file=str(tool_input.get("file_path", "")),
172
+ )
173
+ except Exception:
174
+ pass
175
+ print(json.dumps({
176
+ "allow": decision.allow,
177
+ "reason": decision.reason,
178
+ "stderr_msg": decision.to_stderr_message(),
179
+ }))
180
+ PY
181
+ )
182
+
183
+ if [ -n "$SP_DECISION_JSON" ]; then
184
+ SP_ALLOW=$(echo "$SP_DECISION_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('allow', True))" 2>/dev/null)
185
+ if [ "$SP_ALLOW" != "True" ] && [ "$SP_ALLOW" != "true" ]; then
186
+ SP_STDERR=$(echo "$SP_DECISION_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('stderr_msg',''))" 2>/dev/null)
187
+ echo "$SP_STDERR" >&2
188
+ STDERR_MSG="$SP_STDERR" python3 - <<'PY'
189
+ import json, os
190
+ print(json.dumps({"hookSpecificOutput": {
191
+ "hookEventName": "PreToolUse",
192
+ "permissionDecision": "deny",
193
+ "permissionDecisionReason": os.environ.get("STDERR_MSG", ""),
194
+ }}))
195
+ PY
196
+ exit 2
197
+ fi
198
+ fi
199
+ ;;
200
+ esac
201
+ fi
202
+
123
203
  # ─── Fast allow: not a flow-gated tool ──────────────────────────────────
124
204
  # PR11 v2.33.0 expanded the gated set to include all EFFECT tools.
125
205
  # Bash is special — handled per-command by the Python enforcer via
@@ -0,0 +1,117 @@
1
+ """Specialist-dispatch telemetry summarizer (PR1 — Squad Intelligence Upgrade).
2
+
3
+ Reads ``~/.arkaos/telemetry/specialist-dispatch.jsonl`` (the JSONL stream
4
+ the PreToolUse specialist-enforcer appends to on every gated decision)
5
+ and produces compact summaries for ``/arka status`` and tuning.
6
+
7
+ Mirrors the pattern of ``core.governance.enforcement_telemetry`` so
8
+ periods, malformed-line tolerance, and zero-division safety behave the
9
+ same way across telemetry surfaces. Read-only.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ from collections import Counter
16
+ from dataclasses import dataclass, field
17
+ from datetime import datetime, timedelta, timezone
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ DEFAULT_PATH: Path = (
22
+ Path.home() / ".arkaos" / "telemetry" / "specialist-dispatch.jsonl"
23
+ )
24
+ _VALID_PERIODS: frozenset[str] = frozenset({"today", "week", "month", "all"})
25
+ _TOP_N: int = 5
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class SpecialistSummary:
30
+ """Aggregated specialist-dispatch telemetry over a time slice."""
31
+ period: str
32
+ total_calls: int
33
+ blocked_calls: int
34
+ block_rate: float
35
+ bypass_used: int
36
+ top_blocked_personas: list[tuple[str, int]] = field(default_factory=list)
37
+ top_owners_required: list[tuple[str, int]] = field(default_factory=list)
38
+ corrupt_line_count: int = 0
39
+
40
+
41
+ def summarise(period: str, *, path: Path | None = None) -> SpecialistSummary:
42
+ if period not in _VALID_PERIODS:
43
+ raise ValueError(f"invalid period: {period!r}")
44
+ src = path or DEFAULT_PATH
45
+ cutoff = _period_cutoff(period)
46
+ entries, corrupt = _read_jsonl(src, cutoff)
47
+ return _build_summary(period, entries, corrupt)
48
+
49
+
50
+ def _period_cutoff(period: str, now: datetime | None = None) -> datetime | None:
51
+ ref = now or datetime.now(timezone.utc)
52
+ if period == "today":
53
+ return ref.replace(hour=0, minute=0, second=0, microsecond=0)
54
+ if period == "week":
55
+ return ref - timedelta(days=7)
56
+ if period == "month":
57
+ return ref - timedelta(days=30)
58
+ return None
59
+
60
+
61
+ def _read_jsonl(
62
+ src: Path, cutoff: datetime | None
63
+ ) -> tuple[list[dict[str, Any]], int]:
64
+ if not src.exists():
65
+ return [], 0
66
+ entries: list[dict[str, Any]] = []
67
+ corrupt = 0
68
+ try:
69
+ with src.open("r", encoding="utf-8", errors="replace") as fh:
70
+ for line in fh:
71
+ if not line.strip():
72
+ continue
73
+ try:
74
+ entry = json.loads(line)
75
+ except json.JSONDecodeError:
76
+ corrupt += 1
77
+ continue
78
+ if cutoff is not None:
79
+ ts_raw = entry.get("ts", "")
80
+ try:
81
+ ts = datetime.fromisoformat(ts_raw)
82
+ except (TypeError, ValueError):
83
+ corrupt += 1
84
+ continue
85
+ if ts < cutoff:
86
+ continue
87
+ entries.append(entry)
88
+ except OSError:
89
+ return entries, corrupt
90
+ return entries, corrupt
91
+
92
+
93
+ def _build_summary(
94
+ period: str, entries: list[dict[str, Any]], corrupt: int
95
+ ) -> SpecialistSummary:
96
+ total = len(entries)
97
+ blocked = sum(1 for e in entries if e.get("allow") is False)
98
+ bypass = sum(1 for e in entries if e.get("bypass_used") is True)
99
+ rate = (blocked / total) if total else 0.0
100
+ persona_counter: Counter[str] = Counter()
101
+ owner_counter: Counter[str] = Counter()
102
+ for e in entries:
103
+ if e.get("allow") is False:
104
+ persona = e.get("current_persona") or "unknown"
105
+ persona_counter[persona] += 1
106
+ for owner in e.get("required_owners", []) or []:
107
+ owner_counter[owner] += 1
108
+ return SpecialistSummary(
109
+ period=period,
110
+ total_calls=total,
111
+ blocked_calls=blocked,
112
+ block_rate=rate,
113
+ bypass_used=bypass,
114
+ top_blocked_personas=persona_counter.most_common(_TOP_N),
115
+ top_owners_required=owner_counter.most_common(_TOP_N),
116
+ corrupt_line_count=corrupt,
117
+ )
@@ -0,0 +1,51 @@
1
+ """CLI front-end for specialist-dispatch telemetry.
2
+
3
+ Usage:
4
+ python -m core.governance.specialist_telemetry_cli today
5
+ python -m core.governance.specialist_telemetry_cli week
6
+ python -m core.governance.specialist_telemetry_cli month
7
+ python -m core.governance.specialist_telemetry_cli all
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import sys
13
+
14
+ from core.governance.specialist_telemetry import summarise
15
+
16
+
17
+ def _format_human(summary) -> str:
18
+ pct = f"{summary.block_rate * 100:.1f}%"
19
+ lines = [
20
+ f"Specialist Dispatch Telemetry — {summary.period}",
21
+ f" Total calls: {summary.total_calls}",
22
+ f" Blocked: {summary.blocked_calls} ({pct})",
23
+ f" Bypasses used: {summary.bypass_used}",
24
+ ]
25
+ if summary.top_blocked_personas:
26
+ lines.append(" Top blocked personas:")
27
+ for persona, count in summary.top_blocked_personas:
28
+ lines.append(f" - {persona}: {count}")
29
+ if summary.top_owners_required:
30
+ lines.append(" Top owners required:")
31
+ for owner, count in summary.top_owners_required:
32
+ lines.append(f" - {owner}: {count}")
33
+ if summary.corrupt_line_count:
34
+ lines.append(f" Corrupt lines: {summary.corrupt_line_count}")
35
+ return "\n".join(lines)
36
+
37
+
38
+ def main(argv: list[str] | None = None) -> int:
39
+ args = argv if argv is not None else sys.argv[1:]
40
+ period = args[0] if args else "today"
41
+ try:
42
+ summary = summarise(period)
43
+ except ValueError as exc:
44
+ print(f"error: {exc}", file=sys.stderr)
45
+ return 2
46
+ print(_format_human(summary))
47
+ return 0
48
+
49
+
50
+ if __name__ == "__main__": # pragma: no cover
51
+ sys.exit(main())