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.
- package/README.md +1 -1
- package/VERSION +1 -1
- package/arka/SKILL.md +10 -1
- package/arka/skills/flow/SKILL.md +23 -1
- package/arka/skills/recipes/SKILL.md +69 -0
- package/arka/skills/refine/SKILL.md +81 -0
- package/config/constitution.yaml +8 -0
- package/config/mcp-policy.yaml +8 -0
- package/config/output-styles/arkaos.md +52 -0
- package/core/evals/__init__.py +4 -0
- package/core/evals/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/evals/__pycache__/record_cli.cpython-313.pyc +0 -0
- package/core/evals/__pycache__/runner_cli.cpython-313.pyc +0 -0
- package/core/evals/__pycache__/sanitizer.cpython-313.pyc +0 -0
- package/core/evals/__pycache__/verdict_labels.cpython-313.pyc +0 -0
- package/core/evals/record_cli.py +96 -0
- package/core/evals/runner_cli.py +102 -0
- package/core/evals/sanitizer.py +97 -0
- package/core/evals/verdict_labels.py +53 -13
- package/core/forge/__pycache__/complexity.cpython-313.pyc +0 -0
- package/core/forge/complexity.py +9 -0
- package/core/governance/__pycache__/evidence_checks.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/judge.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/leak_scanner.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/phantom_action_check.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/phantom_action_check.cpython-314.pyc +0 -0
- package/core/governance/evidence_checks.py +60 -4
- package/core/governance/judge.py +95 -0
- package/core/governance/leak_scanner.py +10 -0
- package/core/governance/phantom_action_check.py +58 -2
- package/core/hooks/__pycache__/post_tool_use.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/stop.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/stop.cpython-314.pyc +0 -0
- package/core/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/core/hooks/post_tool_use.py +13 -0
- package/core/hooks/stop.py +23 -0
- package/core/hooks/user_prompt_submit.py +69 -0
- package/core/knowledge/__pycache__/recipes.cpython-313.pyc +0 -0
- package/core/knowledge/__pycache__/recipes_cli.cpython-313.pyc +0 -0
- package/core/knowledge/recipes.py +237 -0
- package/core/knowledge/recipes_cli.py +99 -0
- package/core/registry/__pycache__/generator.cpython-313.pyc +0 -0
- package/core/shared/__pycache__/safe_session_id.cpython-313.pyc +0 -0
- package/core/shared/__pycache__/safe_session_id.cpython-314.pyc +0 -0
- package/core/shared/safe_session_id.py +11 -3
- package/core/synapse/__pycache__/engine.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-314.pyc +0 -0
- package/core/synapse/__pycache__/pattern_library_layer.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/recipe_layer.cpython-313.pyc +0 -0
- package/core/synapse/engine.py +6 -0
- package/core/synapse/pattern_library_layer.py +10 -2
- package/core/synapse/recipe_layer.py +114 -0
- package/core/sync/__pycache__/mcp_optimizer.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/mcp_syncer.cpython-313.pyc +0 -0
- package/core/sync/__pycache__/settings_syncer.cpython-313.pyc +0 -0
- package/core/sync/mcp_optimizer.py +4 -1
- package/core/sync/mcp_syncer.py +50 -6
- package/core/sync/settings_syncer.py +9 -2
- package/core/workflow/__pycache__/flow_enforcer.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/flow_enforcer.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/plan_approval.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/plan_approval.cpython-314.pyc +0 -0
- package/core/workflow/flow_enforcer.py +63 -0
- package/core/workflow/plan_approval.py +185 -0
- package/departments/dev/SKILL.md +10 -0
- package/departments/dev/skills/onboard/SKILL.md +6 -2
- package/departments/dev/skills/refactor-plan/SKILL.md +7 -0
- package/departments/dev/skills/scaffold/SKILL.md +6 -0
- package/departments/quality/SKILL.md +24 -0
- package/installer/doctor.js +7 -0
- package/installer/index.js +20 -0
- package/installer/output-style.js +92 -0
- package/installer/update.js +19 -0
- package/installer/worktree-baseref.js +8 -2
- package/knowledge/commands-keywords.json +4 -0
- package/knowledge/commands-registry.json +41 -3
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
- package/scripts/__pycache__/synapse-bridge.cpython-313.pyc +0 -0
package/core/sync/mcp_syncer.py
CHANGED
|
@@ -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 = {
|
|
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
|
|
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
|
|
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
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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"
|
package/departments/dev/SKILL.md
CHANGED
|
@@ -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.
|
|
48
|
-
|
|
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
|
```
|
package/installer/doctor.js
CHANGED
|
@@ -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)",
|
package/installer/index.js
CHANGED
|
@@ -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
|
+
}
|
package/installer/update.js
CHANGED
|
@@ -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...");
|