arkaos 4.41.0 → 4.42.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.
Files changed (44) hide show
  1. package/THE-ARKAOS-GUIDE.md +1 -1
  2. package/VERSION +1 -1
  3. package/config/claude-agents/marta-cqo.md +10 -2
  4. package/core/evals/record_cli.py +179 -24
  5. package/core/evals/runner_cli.py +5 -2
  6. package/core/evals/verdict_labels.py +19 -6
  7. package/core/governance/aggregate_guard.py +537 -0
  8. package/core/governance/redo_counter.py +13 -8
  9. package/core/governance/reviewer_ledger.py +60 -12
  10. package/departments/quality/SKILL.md +5 -2
  11. package/harness/codex/AGENTS.md +1 -1
  12. package/harness/copilot/copilot-instructions.md +1 -1
  13. package/harness/cursor/rules/arkaos.mdc +2 -2
  14. package/harness/gemini/GEMINI.md +1 -1
  15. package/harness/opencode/AGENTS.md +1 -1
  16. package/harness/opencode/agents/arka-architect-gabriel.md +1 -1
  17. package/harness/opencode/agents/arka-brand-director-valentina.md +1 -1
  18. package/harness/opencode/agents/arka-cfo-helena.md +1 -1
  19. package/harness/opencode/agents/arka-chief-of-staff-afonso.md +1 -1
  20. package/harness/opencode/agents/arka-community-strategist-beatriz.md +1 -1
  21. package/harness/opencode/agents/arka-content-strategist-rafael.md +1 -1
  22. package/harness/opencode/agents/arka-conversion-strategist-ines.md +1 -1
  23. package/harness/opencode/agents/arka-coo-sofia.md +1 -1
  24. package/harness/opencode/agents/arka-copy-director-eduardo.md +1 -1
  25. package/harness/opencode/agents/arka-cqo-marta.md +1 -1
  26. package/harness/opencode/agents/arka-cto-marco.md +1 -1
  27. package/harness/opencode/agents/arka-design-ops-lead-iris.md +1 -1
  28. package/harness/opencode/agents/arka-ecom-director-ricardo.md +1 -1
  29. package/harness/opencode/agents/arka-knowledge-director-clara.md +1 -1
  30. package/harness/opencode/agents/arka-leadership-director-rodrigo.md +1 -1
  31. package/harness/opencode/agents/arka-marketing-director-luna.md +1 -1
  32. package/harness/opencode/agents/arka-ops-lead-daniel.md +1 -1
  33. package/harness/opencode/agents/arka-pm-director-carolina.md +1 -1
  34. package/harness/opencode/agents/arka-revops-lead-vicente.md +1 -1
  35. package/harness/opencode/agents/arka-saas-strategist-tiago.md +1 -1
  36. package/harness/opencode/agents/arka-sales-director-miguel.md +1 -1
  37. package/harness/opencode/agents/arka-strategy-director-tomas.md +1 -1
  38. package/harness/opencode/agents/arka-tech-director-francisca.md +1 -1
  39. package/harness/opencode/agents/arka-tech-lead-paulo.md +1 -1
  40. package/harness/opencode/agents/arka-video-producer-simao.md +1 -1
  41. package/harness/zed/.rules +1 -1
  42. package/knowledge/skills-manifest.json +1 -1
  43. package/package.json +1 -1
  44. package/pyproject.toml +1 -1
@@ -1,6 +1,6 @@
1
1
  # The ArkaOS Guide
2
2
 
3
- > v4.41.0 — 89 agents, 17 departments, 332 skills, 297 commands, 18 ADRs.
3
+ > v4.42.0 — 89 agents, 17 departments, 332 skills, 297 commands, 18 ADRs.
4
4
  > One file, everything you need to start. Generated by `scripts/guide_gen.py` — never hand-edited.
5
5
 
6
6
  ## What it is
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.41.0
1
+ 4.42.0
@@ -46,8 +46,16 @@ evidence report, never from model size.
46
46
  through this agent bypasses the department SKILL's step 6: write your
47
47
  final QGVerdict JSON to a temp file and run
48
48
  `~/.arkaos/bin/arka-py -m core.evals.record_cli --file <f> --kind qg
49
- --department <dept> --deliverable "<title>"`. It fails LOUDLY on
50
- invalid JSON if it fails, fix the JSON and re-run; never skip.
49
+ --session-id <session> --department <dept> --deliverable "<title>"`.
50
+ It fails LOUDLY for three distinct reasons, each with its own
51
+ remedy: invalid JSON (fix the JSON and re-run), a missing
52
+ --session-id (pass the session id — the anti-self-approval guard
53
+ reads that session's reviewer ledger), or a guard refusal (the
54
+ ledger cannot support your aggregate: quorum, digest, blocker
55
+ coverage, or an APPROVED verdict standing over a rejecting
56
+ reviewer — read the stderr reasons and fix the REVIEW, not the
57
+ JSON; if the reason names AGGREGATE.json or the session id, fix
58
+ that instead). Never skip.
51
59
  Every review feeds `~/.arkaos/telemetry/qg-verdicts.jsonl`, redo
52
60
  verdicts included (a REJECTED→APPROVED pair is two labels).
53
61
 
@@ -1,11 +1,30 @@
1
1
  """Record a QGVerdict or gate JudgeVerdict as an eval label.
2
2
 
3
- Reads the reviewer's verdict JSON from stdin (or --file) and appends it
4
- to the matching label corpus (``--kind qg`` → qg-verdicts.jsonl,
5
- ``--kind judge`` → judge-verdicts.jsonl). Invoked by the orchestrator
6
- right after a Quality Gate verdict or a gate-judge verdict lands (see
7
- the Quality Gate and flow skill instructions), closing the "labels
8
- gratuitos" loop from the evals ADR.
3
+ Reads the verdict JSON from stdin (or --file) and appends it to the
4
+ matching label corpus (``--kind qg``/``--kind reviewer`` →
5
+ qg-verdicts.jsonl, ``--kind judge`` → judge-verdicts.jsonl). Invoked by
6
+ the orchestrator right after a verdict lands (see the Quality Gate and
7
+ flow skill instructions), closing the "labels gratuitos" loop from the
8
+ evals ADR.
9
+
10
+ ``--kind qg`` is the AGGREGATE path and runs the anti-self-approval
11
+ guard (PR-B3): ``--session-id`` is mandatory and validated, the
12
+ session's reviewer ledger must hold at least two hook-captured
13
+ reviewer verdicts, no evidence_digest present on both sides may
14
+ disagree (an absent digest warns — PR-B4 makes dispatch populate it),
15
+ no CONFIRMED reviewer blocker may disappear silently, and an APPROVED
16
+ aggregate may not stand over a rejecting reviewer. The label records
17
+ ONLY once AGGREGATE.json is verifiably on disk — an
18
+ accepted-but-unwritten aggregate is a refusal, not a success.
19
+
20
+ ``--kind reviewer`` records an individual reviewer's verdict as a
21
+ label and cross-references the ledger by ``verdict_digest`` — it never
22
+ writes ledger records (only the hook capture may; anything the
23
+ orchestrator could write itself would let it fabricate its own
24
+ quorum), and it refuses aggregator identities: the aggregator's
25
+ verdict enters through the guarded ``--kind qg`` path, never as a
26
+ "reviewer" label. For reviewer identities it remains an unguarded
27
+ label path — the cross-reference is provenance, not admission.
9
28
 
10
29
  Unlike the underlying writer (telemetry contract: never raises), this
11
30
  explicit CLI fails LOUDLY on invalid verdict JSON — a malformed label
@@ -18,6 +37,7 @@ import argparse
18
37
  import json
19
38
  import sys
20
39
  from pathlib import Path
40
+ from typing import TYPE_CHECKING
21
41
 
22
42
  from pydantic import ValidationError
23
43
 
@@ -25,51 +45,186 @@ from core.evals.verdict_labels import record_judge_label, record_verdict_label
25
45
  from core.governance.judge import JudgeVerdict
26
46
  from core.governance.qg_verdict import QGVerdict
27
47
 
48
+ if TYPE_CHECKING: # the runtime import stays lazy (guard pulls pydantic)
49
+ from core.governance.aggregate_guard import GuardResult
28
50
 
29
- def main(argv: list[str] | None = None) -> int:
51
+
52
+ def _build_parser() -> argparse.ArgumentParser:
30
53
  parser = argparse.ArgumentParser(description=__doc__)
31
54
  parser.add_argument("--file", help="verdict JSON file (default: stdin)")
32
55
  parser.add_argument(
33
56
  "--kind",
34
- choices=("qg", "judge"),
57
+ choices=("qg", "reviewer", "judge"),
35
58
  default="qg",
36
- help="qg = QGVerdict (default); judge = gate JudgeVerdict (PR2)",
59
+ help=(
60
+ "qg = aggregate QGVerdict, guarded (default); reviewer = "
61
+ "individual reviewer QGVerdict; judge = gate JudgeVerdict"
62
+ ),
37
63
  )
38
64
  parser.add_argument("--deliverable", default="")
39
65
  parser.add_argument("--department", default="")
40
66
  parser.add_argument("--eval-task-id", default="")
41
67
  parser.add_argument("--session-id", default="")
42
- args = parser.parse_args(argv)
68
+ parser.add_argument("--round", default="", dest="round_label")
69
+ parser.add_argument("--head", default="")
70
+ return parser
43
71
 
44
- raw = (
45
- Path(args.file).read_text(encoding="utf-8")
46
- if args.file
47
- else sys.stdin.read()
48
- )
72
+
73
+ def main(argv: list[str] | None = None) -> int:
74
+ args = _build_parser().parse_args(argv)
75
+ raw = _read_raw(args)
76
+ if raw is None:
77
+ return 1
49
78
  if args.kind == "judge":
50
79
  return _record_judge(raw, args)
51
- return _record_qg(raw, args)
80
+ verdict = _parse_qg(raw)
81
+ if verdict is None:
82
+ return 1
83
+ if args.kind == "reviewer":
84
+ return _record_reviewer(verdict, args)
85
+ return _record_aggregate(verdict, args)
86
+
87
+
88
+ def _read_raw(args: argparse.Namespace) -> str | None:
89
+ """Verdict text, or None after a LOUD error (never a traceback)."""
90
+ if not args.file:
91
+ return sys.stdin.read()
92
+ try:
93
+ return Path(args.file).read_text(encoding="utf-8")
94
+ except (OSError, UnicodeDecodeError) as exc:
95
+ print(f"error: cannot read --file — {exc}", file=sys.stderr)
96
+ return None
52
97
 
53
98
 
54
- def _record_qg(raw: str, args: argparse.Namespace) -> int:
99
+ def _parse_qg(raw: str) -> QGVerdict | None:
55
100
  try:
56
- verdict = QGVerdict.model_validate(json.loads(raw))
101
+ return QGVerdict.model_validate(json.loads(raw))
57
102
  except (json.JSONDecodeError, ValidationError) as exc:
58
103
  print(f"error: invalid QGVerdict JSON — {exc}", file=sys.stderr)
104
+ return None
105
+
106
+
107
+ def _refuse(reasons: list[str]) -> int:
108
+ for reason in reasons:
109
+ print(f"refused: {reason}", file=sys.stderr)
110
+ print(
111
+ "error: aggregate refused — nothing recorded (anti-self-"
112
+ "approval guard, PR-B3)",
113
+ file=sys.stderr,
114
+ )
115
+ return 1
116
+
117
+
118
+ def _drive_redo(verdict: QGVerdict, session_id: str) -> str:
119
+ from core.governance import redo_counter
120
+
121
+ if verdict.verdict == "REJECTED":
122
+ return redo_counter.record_rejected(session_id).to_message()
123
+ redo_counter.reset(session_id)
124
+ return ""
125
+
126
+
127
+ def _record_aggregate(verdict: QGVerdict, args: argparse.Namespace) -> int:
128
+ from core.governance.aggregate_guard import check_aggregate, write_aggregate
129
+
130
+ if not args.session_id:
131
+ print(
132
+ "error: --kind qg records an AGGREGATE and requires "
133
+ "--session-id (the guard reads that session's reviewer ledger)",
134
+ file=sys.stderr,
135
+ )
136
+ return 1
137
+ dumped = verdict.model_dump()
138
+ result = check_aggregate(dumped, args.session_id)
139
+ if not result.ok:
140
+ return _refuse(result.reasons)
141
+ aggregate_path = write_aggregate(args.session_id, dumped, result)
142
+ if aggregate_path is None:
143
+ # An accepted-but-unwritten aggregate recorded anyway would be
144
+ # the incident shape again, one filesystem error away.
145
+ return _refuse([
146
+ "AGGREGATE.json could not be written and verified on disk "
147
+ "— a label without its artifact is the incident shape"
148
+ ])
149
+ _record_label(verdict, args)
150
+ print(json.dumps(_accepted_payload(verdict, args, aggregate_path, result)))
151
+ return 0
152
+
153
+
154
+ def _accepted_payload(
155
+ verdict: QGVerdict,
156
+ args: argparse.Namespace,
157
+ aggregate_path: Path,
158
+ result: GuardResult,
159
+ ) -> dict:
160
+ return {
161
+ "recorded": True,
162
+ "verdict": verdict.verdict,
163
+ "eval_task_id": args.eval_task_id,
164
+ "aggregate": str(aggregate_path),
165
+ "reviewers": result.reviewers,
166
+ "warnings": result.warnings,
167
+ "redo": _drive_redo(verdict, args.session_id),
168
+ }
169
+
170
+
171
+ def _record_reviewer(verdict: QGVerdict, args: argparse.Namespace) -> int:
172
+ from core.governance.reviewer_ledger import AGGREGATOR_IDS
173
+
174
+ if verdict.reviewer in AGGREGATOR_IDS:
175
+ print(
176
+ f"error: {verdict.reviewer!r} is an aggregator identity — "
177
+ "an aggregator's verdict is recorded via --kind qg, which "
178
+ "is guarded; recording it as a 'reviewer' label would repeat "
179
+ "the 2026-07-20 incident under a different flag",
180
+ file=sys.stderr,
181
+ )
59
182
  return 1
183
+ _record_label(verdict, args)
184
+ print(json.dumps({
185
+ "recorded": True,
186
+ "verdict": verdict.verdict,
187
+ "reviewer": verdict.reviewer,
188
+ "ledger_match": _ledger_match(verdict, args.session_id),
189
+ }))
190
+ return 0
191
+
192
+
193
+ def _ledger_match(verdict: QGVerdict, session_id: str) -> str:
194
+ """Name of the hook-captured record carrying this exact verdict."""
195
+ if not session_id:
196
+ return ""
197
+ try:
198
+ from core.governance.aggregate_guard import _session_records
199
+ from core.governance.qg_digest import verdict_digest
200
+
201
+ target = verdict_digest(verdict.model_dump())
202
+ for name, record in _session_records(session_id):
203
+ their = record.get("verdict")
204
+ if not isinstance(their, dict):
205
+ continue
206
+ try: # digest the NORMALIZED dump — raw dicts differ on defaults
207
+ normalized = QGVerdict.model_validate(their).model_dump()
208
+ except ValidationError:
209
+ continue
210
+ if verdict_digest(normalized) == target:
211
+ return name
212
+ except Exception: # cross-reference is provenance, never a blocker
213
+ return ""
214
+ return ""
215
+
216
+
217
+ def _record_label(verdict: QGVerdict, args: argparse.Namespace) -> None:
60
218
  record_verdict_label(
61
219
  verdict,
62
220
  deliverable=args.deliverable,
63
221
  department=args.department,
64
222
  eval_task_id=args.eval_task_id,
65
223
  session_id=args.session_id,
224
+ kind=args.kind,
225
+ round_label=args.round_label,
226
+ head=args.head,
66
227
  )
67
- print(json.dumps({
68
- "recorded": True,
69
- "verdict": verdict.verdict,
70
- "eval_task_id": args.eval_task_id,
71
- }))
72
- return 0
73
228
 
74
229
 
75
230
  def _record_judge(raw: str, args: argparse.Namespace) -> int:
@@ -65,8 +65,11 @@ def _dispatch_prompt(task: EvalTask) -> str:
65
65
  f"O deliverable será julgado pelo Quality Gate contra estas "
66
66
  f"propriedades verificáveis:\n{properties}{rubric}\n\n"
67
67
  f"Após o verdict, regista o label com:\n"
68
- f" arka-py -m core.evals.record_cli --eval-task-id {task.id} "
69
- f"--department {task.department} < verdict.json"
68
+ f" arka-py -m core.evals.record_cli --kind qg "
69
+ f"--session-id <session> --eval-task-id {task.id} "
70
+ f"--department {task.department} < verdict.json\n"
71
+ f"(--session-id é obrigatório no caminho do agregado — o guard "
72
+ f"anti-self-approval lê o ledger de reviewers dessa sessão)"
70
73
  )
71
74
 
72
75
 
@@ -17,7 +17,7 @@ from __future__ import annotations
17
17
 
18
18
  import json
19
19
  import os
20
- from datetime import datetime, timezone
20
+ from datetime import UTC, datetime
21
21
  from pathlib import Path
22
22
  from typing import Any
23
23
 
@@ -49,20 +49,33 @@ def record_verdict_label(
49
49
  department: str = "",
50
50
  eval_task_id: str = "",
51
51
  session_id: str = "",
52
+ kind: str = "",
53
+ round_label: str = "",
54
+ head: str = "",
52
55
  ) -> None:
53
- """Append one labeled QG example. Never raises (telemetry contract)."""
56
+ """Append one labeled QG example. Never raises (telemetry contract).
57
+
58
+ ``kind``/``round_label``/``head`` live on the envelope, not the
59
+ schema: QGVerdict drops unknown fields, so without them the corpus
60
+ could not distinguish rounds of one PR (QG r13 register).
61
+ ``round_label`` persists under the ``round`` key — the parameter
62
+ avoids shadowing the builtin.
63
+ """
54
64
  try:
55
65
  entry: dict[str, Any] = {
56
- "ts": datetime.now(timezone.utc).isoformat(),
66
+ "ts": datetime.now(UTC).isoformat(),
57
67
  "deliverable": str(deliverable or ""),
58
68
  "department": str(department or ""),
59
69
  "eval_task_id": str(eval_task_id or ""),
60
70
  "session_id": str(session_id or ""),
71
+ "kind": str(kind or ""),
72
+ "round": str(round_label or ""),
73
+ "head": str(head or ""),
61
74
  **verdict.model_dump(),
62
75
  }
63
76
  with _locked_append(_labels_path()) as fh:
64
77
  fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
65
- except Exception: # noqa: BLE001 — telemetry must never raise
78
+ except Exception:
66
79
  return
67
80
 
68
81
 
@@ -75,7 +88,7 @@ def record_judge_label(
75
88
  """Append one labeled gate-judge example. Never raises."""
76
89
  try:
77
90
  entry: dict[str, Any] = {
78
- "ts": datetime.now(timezone.utc).isoformat(),
91
+ "ts": datetime.now(UTC).isoformat(),
79
92
  "deliverable": str(deliverable or ""),
80
93
  "department": str(department or ""),
81
94
  "session_id": str(session_id or ""),
@@ -83,7 +96,7 @@ def record_judge_label(
83
96
  }
84
97
  with _locked_append(_judge_labels_path()) as fh:
85
98
  fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
86
- except Exception: # noqa: BLE001 — telemetry must never raise
99
+ except Exception:
87
100
  return
88
101
 
89
102