arkaos 4.10.0 → 4.12.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 (80) hide show
  1. package/README.md +1 -1
  2. package/VERSION +1 -1
  3. package/arka/SKILL.md +10 -1
  4. package/arka/skills/flow/SKILL.md +23 -1
  5. package/arka/skills/recipes/SKILL.md +69 -0
  6. package/arka/skills/refine/SKILL.md +81 -0
  7. package/config/constitution.yaml +8 -0
  8. package/config/mcp-policy.yaml +8 -0
  9. package/config/output-styles/arkaos.md +52 -0
  10. package/core/evals/__init__.py +4 -0
  11. package/core/evals/__pycache__/__init__.cpython-313.pyc +0 -0
  12. package/core/evals/__pycache__/record_cli.cpython-313.pyc +0 -0
  13. package/core/evals/__pycache__/runner_cli.cpython-313.pyc +0 -0
  14. package/core/evals/__pycache__/sanitizer.cpython-313.pyc +0 -0
  15. package/core/evals/__pycache__/verdict_labels.cpython-313.pyc +0 -0
  16. package/core/evals/record_cli.py +96 -0
  17. package/core/evals/runner_cli.py +102 -0
  18. package/core/evals/sanitizer.py +97 -0
  19. package/core/evals/verdict_labels.py +53 -13
  20. package/core/forge/__pycache__/complexity.cpython-313.pyc +0 -0
  21. package/core/forge/complexity.py +9 -0
  22. package/core/governance/__pycache__/evidence_checks.cpython-313.pyc +0 -0
  23. package/core/governance/__pycache__/judge.cpython-313.pyc +0 -0
  24. package/core/governance/__pycache__/leak_scanner.cpython-313.pyc +0 -0
  25. package/core/governance/__pycache__/phantom_action_check.cpython-313.pyc +0 -0
  26. package/core/governance/__pycache__/phantom_action_check.cpython-314.pyc +0 -0
  27. package/core/governance/evidence_checks.py +60 -4
  28. package/core/governance/judge.py +95 -0
  29. package/core/governance/leak_scanner.py +10 -0
  30. package/core/governance/phantom_action_check.py +58 -2
  31. package/core/hooks/__pycache__/post_tool_use.cpython-313.pyc +0 -0
  32. package/core/hooks/__pycache__/stop.cpython-313.pyc +0 -0
  33. package/core/hooks/__pycache__/stop.cpython-314.pyc +0 -0
  34. package/core/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  35. package/core/hooks/post_tool_use.py +13 -0
  36. package/core/hooks/stop.py +23 -0
  37. package/core/hooks/user_prompt_submit.py +69 -0
  38. package/core/knowledge/__pycache__/recipes.cpython-313.pyc +0 -0
  39. package/core/knowledge/__pycache__/recipes_cli.cpython-313.pyc +0 -0
  40. package/core/knowledge/recipes.py +237 -0
  41. package/core/knowledge/recipes_cli.py +99 -0
  42. package/core/registry/__pycache__/generator.cpython-313.pyc +0 -0
  43. package/core/shared/__pycache__/safe_session_id.cpython-313.pyc +0 -0
  44. package/core/shared/__pycache__/safe_session_id.cpython-314.pyc +0 -0
  45. package/core/shared/safe_session_id.py +11 -3
  46. package/core/synapse/__pycache__/engine.cpython-313.pyc +0 -0
  47. package/core/synapse/__pycache__/engine.cpython-314.pyc +0 -0
  48. package/core/synapse/__pycache__/pattern_library_layer.cpython-313.pyc +0 -0
  49. package/core/synapse/__pycache__/recipe_layer.cpython-313.pyc +0 -0
  50. package/core/synapse/engine.py +6 -0
  51. package/core/synapse/pattern_library_layer.py +10 -2
  52. package/core/synapse/recipe_layer.py +114 -0
  53. package/core/sync/__pycache__/mcp_optimizer.cpython-313.pyc +0 -0
  54. package/core/sync/__pycache__/mcp_syncer.cpython-313.pyc +0 -0
  55. package/core/sync/__pycache__/settings_syncer.cpython-313.pyc +0 -0
  56. package/core/sync/mcp_optimizer.py +4 -1
  57. package/core/sync/mcp_syncer.py +50 -6
  58. package/core/sync/settings_syncer.py +9 -2
  59. package/core/workflow/__pycache__/flow_enforcer.cpython-313.pyc +0 -0
  60. package/core/workflow/__pycache__/flow_enforcer.cpython-314.pyc +0 -0
  61. package/core/workflow/__pycache__/plan_approval.cpython-313.pyc +0 -0
  62. package/core/workflow/__pycache__/plan_approval.cpython-314.pyc +0 -0
  63. package/core/workflow/flow_enforcer.py +63 -0
  64. package/core/workflow/plan_approval.py +185 -0
  65. package/departments/dev/SKILL.md +10 -0
  66. package/departments/dev/skills/onboard/SKILL.md +6 -2
  67. package/departments/dev/skills/refactor-plan/SKILL.md +7 -0
  68. package/departments/dev/skills/scaffold/SKILL.md +6 -0
  69. package/departments/quality/SKILL.md +24 -0
  70. package/installer/doctor.js +7 -0
  71. package/installer/index.js +20 -0
  72. package/installer/output-style.js +92 -0
  73. package/installer/update.js +19 -0
  74. package/installer/worktree-baseref.js +8 -2
  75. package/knowledge/commands-keywords.json +4 -0
  76. package/knowledge/commands-registry.json +41 -3
  77. package/package.json +1 -1
  78. package/pyproject.toml +1 -1
  79. package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
  80. package/scripts/__pycache__/synapse-bridge.cpython-313.pyc +0 -0
@@ -8,22 +8,26 @@ from __future__ import annotations
8
8
 
9
9
  import copy
10
10
  import json
11
+ import shutil
11
12
  from pathlib import Path
12
13
 
13
14
  from core.sync.schema import McpSyncResult, Project
14
15
 
15
16
  # Maps stack item names to registry categories
16
17
  _STACK_TO_CATEGORIES: dict[str, list[str]] = {
17
- "laravel": ["laravel"],
18
- "nuxt": ["nuxt"],
19
- "vue": ["nuxt"],
20
- "next": ["react"],
21
- "react": ["react"],
18
+ "laravel": ["laravel", "grounding-code"],
19
+ "nuxt": ["nuxt", "grounding-code"],
20
+ "vue": ["nuxt", "grounding-code"],
21
+ "next": ["react", "grounding-code"],
22
+ "react": ["react", "grounding-code"],
23
+ "python": ["grounding-code"],
22
24
  "shopify": ["ecommerce"],
23
25
  }
24
26
 
25
27
  # Keys that are registry metadata — stripped when writing .mcp.json
26
- _REGISTRY_META_KEYS = {"category", "description", "required_env"}
28
+ _REGISTRY_META_KEYS = {
29
+ "category", "description", "required_env", "requires_binary",
30
+ }
27
31
 
28
32
 
29
33
  # ---------------------------------------------------------------------------
@@ -69,6 +73,39 @@ def resolve_mcps_for_stack(
69
73
  return result
70
74
 
71
75
 
76
+ def _split_missing_binaries(
77
+ target: list[tuple[str, dict]],
78
+ ) -> tuple[list[tuple[str, dict]], list[str]]:
79
+ """Drop entries whose `requires_binary` is not on PATH.
80
+
81
+ An MCP entry pointing at an absent binary is a dead server that
82
+ errors on every session start — the sync never writes one. The
83
+ skipped names are reported (mcps_deferred) so the operator knows
84
+ installing the binary + re-sync activates it (Interaction Reform
85
+ PR6, codebase-memory-mcp).
86
+ """
87
+ available: list[tuple[str, dict]] = []
88
+ missing: list[str] = []
89
+ for name, config in target:
90
+ binary = config.get("requires_binary", "")
91
+ if binary and shutil.which(binary) is None:
92
+ missing.append(name)
93
+ continue
94
+ available.append((name, config))
95
+ return available, missing
96
+
97
+
98
+ def _missing_binary_warnings(registry: dict, missing: list[str]) -> list[str]:
99
+ return [
100
+ (
101
+ f"{name}: requires binary "
102
+ f"'{registry.get(name, {}).get('requires_binary', '?')}' not on "
103
+ f"PATH — install it and re-run /arka update to activate"
104
+ )
105
+ for name in missing
106
+ ]
107
+
108
+
72
109
  def sync_project_mcp(
73
110
  project: Project, registry: dict, home_path: str
74
111
  ) -> McpSyncResult:
@@ -97,6 +134,7 @@ def _do_sync_mcp(
97
134
 
98
135
  current = _read_current_mcps(mcp_file)
99
136
  target_raw = resolve_mcps_for_stack(registry, project.stack)
137
+ target_raw, missing_binary = _split_missing_binaries(target_raw)
100
138
  target = _resolve_placeholders(target_raw, home_path, project.path)
101
139
  registry_names = set(registry.keys())
102
140
 
@@ -104,11 +142,15 @@ def _do_sync_mcp(
104
142
  current, target, registry_names, registry
105
143
  )
106
144
 
145
+ binary_warnings = _missing_binary_warnings(registry, missing_binary)
146
+
107
147
  if not added and not removed and not updated:
108
148
  return McpSyncResult(
109
149
  path=project.path,
110
150
  status="unchanged",
111
151
  final_mcp_list=sorted(current.keys()),
152
+ mcps_deferred=sorted(missing_binary),
153
+ optimizer_warnings=binary_warnings,
112
154
  )
113
155
 
114
156
  merged = _build_merged(current, target, registry_names, registry)
@@ -122,6 +164,8 @@ def _do_sync_mcp(
122
164
  mcps_removed=sorted(removed),
123
165
  mcps_updated=sorted(updated),
124
166
  mcps_preserved=sorted(preserved),
167
+ mcps_deferred=sorted(missing_binary),
168
+ optimizer_warnings=binary_warnings,
125
169
  final_mcp_list=sorted(merged.keys()),
126
170
  )
127
171
 
@@ -88,10 +88,14 @@ def _read_current_settings(settings_file: Path) -> dict:
88
88
 
89
89
 
90
90
  def _is_already_correct(current: dict, target_servers: list[str]) -> bool:
91
- """Return True when servers and flag already match the target."""
91
+ """Return True when servers, flag and outputStyle already match."""
92
92
  current_servers = sorted(current.get("enabledMcpjsonServers", []))
93
93
  flag = current.get("enableAllProjectMcpServers", False)
94
- return current_servers == target_servers and flag is True
94
+ return (
95
+ current_servers == target_servers
96
+ and flag is True
97
+ and bool(current.get("outputStyle"))
98
+ )
95
99
 
96
100
 
97
101
  def _compute_diff(
@@ -112,6 +116,9 @@ def _build_merged_settings(current: dict, target_servers: list[str]) -> dict:
112
116
  merged["permissions"] = _DEFAULT_PERMISSIONS
113
117
  merged["enabledMcpjsonServers"] = target_servers
114
118
  merged["enableAllProjectMcpServers"] = True
119
+ # Interaction Reform PR1: seed-if-absent — an explicit operator
120
+ # choice (any value, including "default") is never overridden.
121
+ merged.setdefault("outputStyle", "ArkaOS")
115
122
  return merged
116
123
 
117
124
 
@@ -224,6 +224,9 @@ class Decision:
224
224
  phase_observed: str | None = None
225
225
  bypass_used: bool = False
226
226
  warning: str = ""
227
+ # Interaction Reform PR3 — plan-approval telemetry (WARN phase):
228
+ # "approved" | "missing" | "no-plan" | "unknown" | "" (not gated).
229
+ approval_state: str = ""
227
230
 
228
231
  def to_stderr_message(self) -> str:
229
232
  if self.allow:
@@ -369,6 +372,46 @@ def _scan_markers(messages: list[str]) -> tuple[str | None, str | None]:
369
372
  return marker_found, phase_observed
370
373
 
371
374
 
375
+ _APPROVAL_EXEMPT_REASONS: frozenset[str] = frozenset({
376
+ "tool-not-gated", "feature-flag-off", "env-bypass",
377
+ "classifier-did-not-match",
378
+ })
379
+
380
+
381
+ def _annotate_plan_approval(decision: Decision, session_id: str) -> Decision:
382
+ """PR3 WARN phase: annotate gated allows with plan-approval state.
383
+
384
+ Telemetry only — no deny path exists here (hard enforcement is PR4,
385
+ gated on this telemetry's false-positive rate). The stderr warning
386
+ fires only for the interesting case: a plan was PRESENTED and the
387
+ effect tool ran without the approval landing.
388
+ """
389
+ if not decision.allow or decision.reason in _APPROVAL_EXEMPT_REASONS:
390
+ return decision
391
+ if decision.marker_found == "trivial" or "trivial" in decision.reason:
392
+ return decision
393
+ try:
394
+ from core.workflow import plan_approval
395
+ if plan_approval.is_approved(session_id):
396
+ decision.approval_state = "approved"
397
+ elif plan_approval.is_presented(session_id):
398
+ decision.approval_state = "missing"
399
+ warn = (
400
+ "[arka:warn] no-plan-approval-warn — effect tool ran with a "
401
+ "presented Gate-2 plan and no user approval on record "
402
+ "(telemetry only in this release)."
403
+ )
404
+ decision.warning = (
405
+ f"{decision.warning}\n{warn}".strip()
406
+ if decision.warning else warn
407
+ )
408
+ else:
409
+ decision.approval_state = "no-plan"
410
+ except Exception: # noqa: BLE001 — annotation must never break a hook
411
+ decision.approval_state = "unknown"
412
+ return decision
413
+
414
+
372
415
  def evaluate(
373
416
  tool_name: str,
374
417
  transcript_path: str,
@@ -389,7 +432,27 @@ def evaluate(
389
432
  ``messages`` (PR-6 hook consolidation): pre-parsed assistant messages.
390
433
  When None (default) the transcript is read from ``transcript_path`` —
391
434
  backward compatible with all existing callers.
435
+
436
+ Interaction Reform PR3: every gated allow is annotated with the
437
+ plan-approval state (``approval_state``) for the WARN-phase
438
+ telemetry that gates PR4's hard G2 enforcement.
392
439
  """
440
+ return _annotate_plan_approval(
441
+ _evaluate_flow(
442
+ tool_name, transcript_path, session_id, cwd, tool_input, messages
443
+ ),
444
+ session_id,
445
+ )
446
+
447
+
448
+ def _evaluate_flow(
449
+ tool_name: str,
450
+ transcript_path: str,
451
+ session_id: str = "",
452
+ cwd: str = "",
453
+ tool_input: dict | None = None,
454
+ messages: list[str] | None = None,
455
+ ) -> Decision:
393
456
  is_gated = tool_name in EFFECT_TOOLS_ALWAYS
394
457
  if not is_gated and tool_name == "Bash":
395
458
  bash_cmd = ""
@@ -0,0 +1,185 @@
1
+ """Per-session plan-approval state machine (Interaction Reform PR3).
2
+
3
+ The flow contract says Gate 2 "waits for EXPLICIT user approval", but
4
+ nothing executable ever verified it — the enforcer only checked marker
5
+ PRESENCE, so a bare ``[arka:routing]`` unlocked writes. This module
6
+ tracks the missing signal:
7
+
8
+ none → presented → approved
9
+
10
+ - **presented** — the Stop hook observed the turn ending at Gate 2
11
+ (plan shown, no Gate 3 yet).
12
+ - **approved** — the UserPromptSubmit hook classified the NEXT user
13
+ message as an approval, or PostToolUse saw a successful
14
+ ``ExitPlanMode`` (the native plan-mode approve button IS approval).
15
+
16
+ A new ``presented`` invalidates an older ``approved`` (plan B after an
17
+ approved plan A requires fresh approval): approval is only live when
18
+ ``approved_ts >= presented_ts``.
19
+
20
+ Same persistence contract as ``flow_authorization``: CWE-22-safe state
21
+ files under ``arkaos_temp_dir`` (override ``ARKA_PLAN_APPROVAL_DIR``),
22
+ 12h TTL, never raises — hooks must not break a turn. In PR3 the
23
+ enforcer only WARNS on missing approval (telemetry to measure false
24
+ positives across the install base); hard deny is PR4, gated on that
25
+ telemetry.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import json
31
+ import os
32
+ import re
33
+ import time
34
+ from dataclasses import asdict, dataclass
35
+ from pathlib import Path
36
+
37
+ from core.shared.safe_session_id import safe_session_id
38
+ from core.shared.temp_paths import arkaos_temp_dir
39
+
40
+ DEFAULT_TTL_SECONDS = 12 * 60 * 60
41
+
42
+ _APPROVE_RE = re.compile(
43
+ r"\b(aprovo|aprovado|avan[çc]a(?:m|mos)?|segue em frente|for[çc]a"
44
+ r"|manda|bora|go ahead|approved?|proceed|ship it|lgtm)\b",
45
+ re.IGNORECASE,
46
+ )
47
+ _REJECT_RE = re.compile(
48
+ r"\b(n[ãa]o|para|espera|aguarda|cancela?|stop|reject|hold)\b",
49
+ re.IGNORECASE,
50
+ )
51
+ _SHORT_YES_RE = re.compile(r"\b(sim|ok|okay|yes|yep|sure|dale)\b", re.IGNORECASE)
52
+
53
+
54
+ def _state_dir() -> Path | None:
55
+ """Resolve the state dir, creating it. None when the mkdir fails.
56
+
57
+ The module's never-raises contract must hold in its OWN internals:
58
+ a read-only override dir or a full disk returns None (callers treat
59
+ it as "no state") instead of propagating an OSError into a hook
60
+ (QG 2026-07-09, PR4 prerequisite #3).
61
+ """
62
+ override = os.environ.get("ARKA_PLAN_APPROVAL_DIR", "").strip()
63
+ try:
64
+ if override:
65
+ path = Path(override)
66
+ path.mkdir(parents=True, exist_ok=True)
67
+ return path
68
+ return arkaos_temp_dir("arkaos-plan-approval")
69
+ except OSError:
70
+ return None
71
+
72
+
73
+ def _state_file(session_id: str) -> Path | None:
74
+ safe = safe_session_id(session_id)
75
+ if safe is None:
76
+ return None
77
+ state_dir = _state_dir()
78
+ if state_dir is None:
79
+ return None
80
+ return state_dir / f"{safe}.json"
81
+
82
+
83
+ @dataclass
84
+ class ApprovalState:
85
+ state: str = "none" # none | presented | approved
86
+ presented_ts: float = 0.0
87
+ approved_ts: float = 0.0
88
+ source: str = "" # text | exit-plan-mode
89
+ excerpt: str = ""
90
+
91
+
92
+ def _read(session_id: str) -> ApprovalState:
93
+ path = _state_file(session_id)
94
+ if path is None or not path.exists():
95
+ return ApprovalState()
96
+ try:
97
+ data = json.loads(path.read_text(encoding="utf-8"))
98
+ return ApprovalState(**{
99
+ k: data.get(k, getattr(ApprovalState(), k))
100
+ for k in ApprovalState().__dict__
101
+ })
102
+ except Exception: # noqa: BLE001 — corrupt state resets, never raises
103
+ return ApprovalState()
104
+
105
+
106
+ def _write(session_id: str, state: ApprovalState) -> None:
107
+ path = _state_file(session_id)
108
+ if path is None:
109
+ return
110
+ try:
111
+ path.write_text(
112
+ json.dumps(asdict(state)) + "\n", encoding="utf-8"
113
+ )
114
+ except OSError:
115
+ return
116
+
117
+
118
+ def mark_presented(session_id: str) -> None:
119
+ """The turn ended at Gate 2: a plan is on the table."""
120
+ state = _read(session_id)
121
+ state.state = "presented"
122
+ state.presented_ts = time.time()
123
+ _write(session_id, state)
124
+
125
+
126
+ def mark_approved(
127
+ session_id: str, source: str = "text", excerpt: str = ""
128
+ ) -> None:
129
+ state = _read(session_id)
130
+ state.state = "approved"
131
+ state.approved_ts = time.time()
132
+ state.source = source
133
+ state.excerpt = excerpt[:120]
134
+ _write(session_id, state)
135
+
136
+
137
+ def mark_rejected(session_id: str) -> None:
138
+ _write(session_id, ApprovalState())
139
+
140
+
141
+ def get_state(session_id: str) -> ApprovalState:
142
+ return _read(session_id)
143
+
144
+
145
+ def is_presented(session_id: str) -> bool:
146
+ state = _read(session_id)
147
+ return (
148
+ state.state in ("presented", "approved")
149
+ and time.time() - state.presented_ts < DEFAULT_TTL_SECONDS
150
+ )
151
+
152
+
153
+ def is_approved(session_id: str) -> bool:
154
+ """Approval is live only when it POSTdates the latest presented plan."""
155
+ state = _read(session_id)
156
+ if state.state != "approved" or not state.approved_ts:
157
+ return False
158
+ if state.approved_ts + 0.001 < state.presented_ts:
159
+ return False
160
+ return time.time() - state.approved_ts < DEFAULT_TTL_SECONDS
161
+
162
+
163
+ def classify_reply(text: str, has_creation_verb: bool = False) -> str:
164
+ """Classify a user message that follows a presented plan.
165
+
166
+ Returns "approve" | "reject" | "other". A short bare "sim/ok/yes"
167
+ counts as approval ONLY when unambiguous: <= 5 tokens, no creation
168
+ verb (a new instruction, not consent), and no question mark.
169
+ """
170
+ stripped = (text or "").strip()
171
+ if not stripped:
172
+ return "other"
173
+ if _REJECT_RE.search(stripped):
174
+ return "reject"
175
+ if _APPROVE_RE.search(stripped) and not has_creation_verb:
176
+ return "approve"
177
+ tokens = stripped.split()
178
+ if (
179
+ len(tokens) <= 5
180
+ and _SHORT_YES_RE.search(stripped)
181
+ and not has_creation_verb
182
+ and "?" not in stripped
183
+ ):
184
+ return "approve"
185
+ return "other"
@@ -70,6 +70,16 @@ Phase 7: QUALITY GATE → Marta + Eduardo (copy) + Francisca (tech) → APPROVED
70
70
  Phase 8: DOCUMENTATION → ADR + docs to Obsidian
71
71
  ```
72
72
 
73
+ ## Codebase grounding (codebase-memory MCP)
74
+
75
+ When the `codebase-memory` MCP is active in the project, consult it
76
+ BEFORE touching complex code — in Phase 1 RESEARCH and in every
77
+ Diagnose step: `get_architecture` for the structural map,
78
+ `search_graph`/`get_code_snippet` to locate the real implementations,
79
+ `trace_path` for impact analysis before a refactor. The graph is a
80
+ PRIOR, not ground truth: verify with Read before editing. When the MCP
81
+ is absent the phases run as before (Grep/Read exploration).
82
+
73
83
  ## Focused Workflow (4 Phases)
74
84
 
75
85
  For `/dev debug`, `/dev refactor`, `/dev db`, `/dev performance`, `/dev pipeline`: Diagnose → Implement with tests → Rita validates → Quality Gate (Marta).
@@ -44,8 +44,12 @@ The onboard flow is a 12-step pipeline, split across two reference files:
44
44
  8. Generate `PROJECT.md` in project root
45
45
  9. Register in `$ARKA_OS/projects/<name>/`
46
46
  10. Apply MCP profile — generate `.mcp.json` + `.claude/settings.local.json`
47
- 11. Create Obsidian docsHome, Architecture, Projects MOC, Ecosystem MOC
48
- 12. Report summary + next steps
47
+ 11. Index the codebase graph when the `codebase-memory-mcp` binary
48
+ exists, run `codebase-memory-mcp cli index_repository
49
+ '{"repo_path": "<abs project path>"}'` so the consistency layer is
50
+ warm from day one; skip silently when the binary is absent
51
+ 12. Create Obsidian docs — Home, Architecture, Projects MOC, Ecosystem MOC
52
+ 13. Report — summary + next steps (mention the graph index status)
49
53
 
50
54
  The `ecosystem` subcommands (list/create/add) and error handling also live in mcp-config.md.
51
55
 
@@ -26,6 +26,13 @@ allowed-tools: [Read, Write, Edit, Bash, Grep, Glob, Agent, WebFetch, WebSearch]
26
26
 
27
27
  Plan a refactoring: identify code smells, select refactoring patterns, ensure test safety net.
28
28
 
29
+ When the `codebase-memory` MCP is active, ground the plan in the real
30
+ structure BEFORE proposing moves: `get_architecture` for the module
31
+ map, `trace_path` for every call-site the refactor touches (impact
32
+ analysis), `search_graph` to find duplicated implementations worth
33
+ consolidating. The graph is a prior — verify with Read before planning
34
+ an edit.
35
+
29
36
  ## Output
30
37
 
31
38
  Refactoring plan with patterns, risk assessment, and test verification steps
@@ -109,6 +109,12 @@ Profile mapping:
109
109
 
110
110
  This generates `.mcp.json` and `.claude/settings.local.json`.
111
111
 
112
+ After the profile is applied, warm the consistency layer: when the
113
+ `codebase-memory-mcp` binary exists, run
114
+ `codebase-memory-mcp cli index_repository '{"repo_path": "<abs path>"}'`
115
+ so the graph is available from the first feature. Skip silently when
116
+ the binary is absent (the sync defers the MCP entry until installed).
117
+
112
118
  ### Step 6: Generate PROJECT.md
113
119
 
114
120
  Create `PROJECT.md` in the project root with:
@@ -23,6 +23,13 @@ override it with narrative.
23
23
  Any Department Workflow:
24
24
  ...
25
25
  Phase N-1: QUALITY GATE
26
+ 0. Output-judge (constitution gate-judges, MEDIUM/HIGH work only):
27
+ one judge dispatched via the Agent tool with
28
+ JUDGE_VERDICT_JSON_SCHEMA (core.governance.judge), frontier
29
+ model, over deliverable + diff + evidence report. REVISE loops
30
+ the work back before the personas run (max 2); PASS findings
31
+ become reviewer input. Record it:
32
+ arka-py -m core.evals.record_cli --kind judge
26
33
  1. Run the evidence engine over the project/diff:
27
34
  ~/.arkaos/bin/arka-py -m core.governance.evidence_checks <project_dir> \
28
35
  [--changed-files f1,f2] [--test-command '...'] --json
@@ -38,6 +45,23 @@ Any Department Workflow:
38
45
  explicit justification in the verdict notes; otherwise REJECTED.
39
46
  4. If ANY reviewer rejects → work loops back with the blockers list
40
47
  5. If ALL approve → Marta issues final APPROVED verdict
48
+ 6. Record the label (evals ADR 2026-07-09): pipe Marta's final
49
+ QGVerdict JSON to
50
+ `arka-py -m core.evals.record_cli --department <dept>
51
+ [--deliverable <title>] [--eval-task-id <id>] [--session-id <id>]`
52
+ — every verdict feeds the eval/distillation corpus
53
+ (~/.arkaos/telemetry/qg-verdicts.jsonl). Applies to EVERY review,
54
+ not only eval runs; --eval-task-id only when the review judged a
55
+ config/evals task.
56
+ 7. Recipe promotion (Interaction Reform PR7, APPROVED reusable
57
+ features only): when the deliverable is a feature worth reusing
58
+ across projects (auth flow, payment integration, a standard UI
59
+ pattern…), PROPOSE to the operator "promote this to a recipe?".
60
+ On confirmation, capture it —
61
+ `arka-py -m core.knowledge.recipes_cli capture --spec <spec.json>`
62
+ (spec = {recipe, narrative, files}). Capture is fail-closed:
63
+ every field and file is sanitized first, refused without a
64
+ redaction config. Never silent — always operator-confirmed.
41
65
  Phase N: DELIVERY
42
66
  → Only reaches user after APPROVED from all three
43
67
  ```
@@ -208,6 +208,13 @@ const checks = [
208
208
  check: () => graphifyDoctor().installed,
209
209
  fix: () => graphifyDoctor().hint || "Run: uv tool install graphifyy (or pipx install graphifyy)",
210
210
  },
211
+ {
212
+ name: "codebase-memory",
213
+ description: "codebase-memory-mcp binary present (default consistency layer for code stacks)",
214
+ severity: "warn",
215
+ check: () => commandExists("codebase-memory-mcp"),
216
+ fix: () => "Install codebase-memory-mcp (see mcps/registry.json entry for the one-liner), then /arka update to activate per project",
217
+ },
211
218
  {
212
219
  name: "magic-api-key",
213
220
  description: "Magic API key configured (frontend UI/UX — Magic MCP)",
@@ -372,6 +372,26 @@ export async function install({ runtime, path, force, skipSystem, withOllama })
372
372
  console.log(` Warning: could not seed worktree.baseRef (${err.message})`);
373
373
  }
374
374
 
375
+ // Interaction Reform PR1 — install the ArkaOS output style and seed
376
+ // it as the Claude Code default. File copy is unconditional (making
377
+ // the style available is not a preference); the settings seed is
378
+ // if-absent only — an explicit operator choice is never overridden.
379
+ try {
380
+ const { installOutputStyles, seedOutputStyleDefault } = await import("./output-style.js");
381
+ const styleResult = installOutputStyles({
382
+ sourceDir: join(ARKAOS_ROOT, "config", "output-styles"),
383
+ });
384
+ if (styleResult.copied > 0) {
385
+ console.log(` ${styleResult.copied} output style(s) installed (~/.claude/output-styles/).`);
386
+ }
387
+ const seedResult = seedOutputStyleDefault({ runtime });
388
+ if (!seedResult.skipped && seedResult.action === "created") {
389
+ console.log(` outputStyle default set to "${seedResult.value}".`);
390
+ }
391
+ } catch (err) {
392
+ console.log(` Warning: could not install output style (${err.message})`);
393
+ }
394
+
375
395
  // PR43 v2.62.0 — auto-install default Claude Code plugins. Only runs
376
396
  // when runtime is Claude Code AND the `claude` CLI is available.
377
397
  // Idempotent: skips plugins already in installed_plugins.json.
@@ -0,0 +1,92 @@
1
+ // ArkaOS output style install + default seed (Interaction Reform PR1).
2
+ //
3
+ // The ArkaOS output style used to exist only as a hand-placed file in
4
+ // ~/.claude/output-styles/ — orphaned from the installer, so fresh
5
+ // installs never got the branded personality and /config showed the
6
+ // generic default. This module makes the repo the source of truth:
7
+ //
8
+ // 1. Copies config/output-styles/*.md → ~/.claude/output-styles/
9
+ // (always — the file being available is not a preference).
10
+ // 2. Seeds `"outputStyle": "ArkaOS"` in ~/.claude/settings.json
11
+ // ONLY when the key is absent. An operator who explicitly chose
12
+ // another style (including "default") is never overridden — the
13
+ // same seed-if-absent contract as worktree-baseref.js and
14
+ // config-seed.js.
15
+ //
16
+ // Never raises — failures are non-fatal install warnings.
17
+
18
+ import {
19
+ copyFileSync,
20
+ existsSync,
21
+ mkdirSync,
22
+ readdirSync,
23
+ readFileSync,
24
+ renameSync,
25
+ writeFileSync,
26
+ } from "node:fs";
27
+ import { homedir } from "node:os";
28
+ import { join } from "node:path";
29
+
30
+
31
+ export const DEFAULT_OUTPUT_STYLE = "ArkaOS";
32
+
33
+
34
+ export function installOutputStyles({
35
+ sourceDir,
36
+ home = homedir(),
37
+ } = {}) {
38
+ if (!sourceDir || !existsSync(sourceDir)) {
39
+ return { skipped: "source-not-found", copied: 0 };
40
+ }
41
+ try {
42
+ const destDir = join(home, ".claude", "output-styles");
43
+ mkdirSync(destDir, { recursive: true });
44
+ let copied = 0;
45
+ for (const name of readdirSync(sourceDir)) {
46
+ if (!name.endsWith(".md")) continue;
47
+ copyFileSync(join(sourceDir, name), join(destDir, name));
48
+ copied += 1;
49
+ }
50
+ return { skipped: null, copied };
51
+ } catch {
52
+ return { skipped: "copy-failed", copied: 0 };
53
+ }
54
+ }
55
+
56
+
57
+ export function seedOutputStyleDefault({
58
+ runtime = "claude-code",
59
+ home = homedir(),
60
+ defaultValue = DEFAULT_OUTPUT_STYLE,
61
+ } = {}) {
62
+ if (runtime !== "claude-code") {
63
+ return { skipped: "runtime-not-claude-code", action: null };
64
+ }
65
+ const settingsPath = join(home, ".claude", "settings.json");
66
+ if (!existsSync(settingsPath)) {
67
+ return { skipped: "claude-settings-not-found", action: null };
68
+ }
69
+ let settings;
70
+ try {
71
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
72
+ } catch {
73
+ return { skipped: "settings-not-parseable", action: null };
74
+ }
75
+ if (typeof settings !== "object" || settings === null) {
76
+ return { skipped: "settings-not-object", action: null };
77
+ }
78
+ if (typeof settings.outputStyle === "string" && settings.outputStyle) {
79
+ return { skipped: null, action: "noop", value: settings.outputStyle };
80
+ }
81
+ settings.outputStyle = defaultValue;
82
+ const tmp = `${settingsPath}.tmp-${process.pid}`;
83
+ try {
84
+ writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
85
+ renameSync(tmp, settingsPath);
86
+ } catch {
87
+ // Read-only ~/.claude, disk full… — "never raises" must be true of
88
+ // THIS module, not delegated to caller try/catch (QG blocker M1).
89
+ return { skipped: "write-failed", action: null };
90
+ }
91
+ return { skipped: null, action: "created", value: defaultValue };
92
+ }
@@ -174,6 +174,25 @@ export async function update() {
174
174
  copyFileSync(statuslineSrc, join(installDir, "config", statuslineFile));
175
175
  console.log(" ✓ Statusline updated");
176
176
  }
177
+ // Interaction Reform PR1 — refresh the ArkaOS output style file and
178
+ // seed the default (if-absent; explicit operator choice preserved).
179
+ try {
180
+ const { installOutputStyles, seedOutputStyleDefault } = await import("./output-style.js");
181
+ const styleResult = installOutputStyles({
182
+ sourceDir: join(ARKAOS_ROOT, "config", "output-styles"),
183
+ });
184
+ if (styleResult.copied > 0) {
185
+ console.log(" ✓ Output style updated");
186
+ }
187
+ const seedResult = seedOutputStyleDefault({
188
+ runtime: manifest.runtime || "claude-code",
189
+ });
190
+ if (!seedResult.skipped && seedResult.action === "created") {
191
+ console.log(` ✓ outputStyle default set to "${seedResult.value}"`);
192
+ }
193
+ } catch (err) {
194
+ console.log(` ⚠ Output style update failed (${err.message})`);
195
+ }
177
196
 
178
197
  // ── 3. Update hooks ──
179
198
  console.log(" [3/8] Updating hooks...");