arkaos 4.44.0 → 4.46.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 (52) hide show
  1. package/THE-ARKAOS-GUIDE.md +1 -1
  2. package/VERSION +1 -1
  3. package/config/output-styles/arkaos.md +1 -1
  4. package/core/egress/__init__.py +20 -0
  5. package/core/egress/allowlist.py +123 -0
  6. package/core/egress/audit.py +127 -0
  7. package/core/egress/policy.py +261 -0
  8. package/core/egress/redact.py +46 -0
  9. package/core/governance/evidence_checks.py +116 -37
  10. package/core/harness/__init__.py +22 -0
  11. package/core/harness/drift.py +317 -0
  12. package/core/harness/json_store.py +119 -0
  13. package/core/harness/manifest.py +87 -0
  14. package/core/harness/paths.py +72 -0
  15. package/core/harness/spec.py +182 -0
  16. package/core/hooks/session_start.py +17 -0
  17. package/core/runtime/opencode_hooks.py +13 -10
  18. package/core/synapse/layers.py +85 -12
  19. package/harness/codex/AGENTS.md +1 -1
  20. package/harness/copilot/copilot-instructions.md +1 -1
  21. package/harness/cursor/rules/arkaos.mdc +2 -2
  22. package/harness/gemini/GEMINI.md +1 -1
  23. package/harness/opencode/AGENTS.md +1 -1
  24. package/harness/opencode/agents/arka-architect-gabriel.md +1 -1
  25. package/harness/opencode/agents/arka-brand-director-valentina.md +1 -1
  26. package/harness/opencode/agents/arka-cfo-helena.md +1 -1
  27. package/harness/opencode/agents/arka-chief-of-staff-afonso.md +1 -1
  28. package/harness/opencode/agents/arka-community-strategist-beatriz.md +1 -1
  29. package/harness/opencode/agents/arka-content-strategist-rafael.md +1 -1
  30. package/harness/opencode/agents/arka-conversion-strategist-ines.md +1 -1
  31. package/harness/opencode/agents/arka-coo-sofia.md +1 -1
  32. package/harness/opencode/agents/arka-copy-director-eduardo.md +1 -1
  33. package/harness/opencode/agents/arka-cqo-marta.md +1 -1
  34. package/harness/opencode/agents/arka-cto-marco.md +1 -1
  35. package/harness/opencode/agents/arka-design-ops-lead-iris.md +1 -1
  36. package/harness/opencode/agents/arka-ecom-director-ricardo.md +1 -1
  37. package/harness/opencode/agents/arka-knowledge-director-clara.md +1 -1
  38. package/harness/opencode/agents/arka-leadership-director-rodrigo.md +1 -1
  39. package/harness/opencode/agents/arka-marketing-director-luna.md +1 -1
  40. package/harness/opencode/agents/arka-ops-lead-daniel.md +1 -1
  41. package/harness/opencode/agents/arka-pm-director-carolina.md +1 -1
  42. package/harness/opencode/agents/arka-revops-lead-vicente.md +1 -1
  43. package/harness/opencode/agents/arka-saas-strategist-tiago.md +1 -1
  44. package/harness/opencode/agents/arka-sales-director-miguel.md +1 -1
  45. package/harness/opencode/agents/arka-strategy-director-tomas.md +1 -1
  46. package/harness/opencode/agents/arka-tech-director-francisca.md +1 -1
  47. package/harness/opencode/agents/arka-tech-lead-paulo.md +1 -1
  48. package/harness/opencode/agents/arka-video-producer-simao.md +1 -1
  49. package/harness/zed/.rules +1 -1
  50. package/knowledge/skills-manifest.json +1 -1
  51. package/package.json +1 -1
  52. package/pyproject.toml +1 -1
@@ -0,0 +1,87 @@
1
+ """Ownership manifest schema for ``~/.arkaos/ownership.json`` (PR-C1).
2
+
3
+ The manifest is the C2 manager's record of WHICH harness surfaces
4
+ ArkaOS claims and UNDER WHICH policy. C1 ships only the schema and a
5
+ tolerant loader — nothing here writes the file, and the default
6
+ manifest is DERIVED from ``core.harness.spec`` so the two can never
7
+ disagree about which surfaces exist.
8
+
9
+ Policy vocabulary (decided in the 2026-07-26 campaign plan, C2):
10
+
11
+ - ``own`` — ArkaOS replaces the surface wholesale on assert.
12
+ - ``own-subset`` — ArkaOS guarantees its own entries exist; operator
13
+ entries on the same surface are never removed.
14
+ - ``seed`` — written only when absent; an operator edit is
15
+ ADOPTED (drift reports it, assert never reverts it).
16
+ - ``operator`` — ArkaOS never touches the surface.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from enum import StrEnum
22
+ from pathlib import Path
23
+
24
+ from pydantic import BaseModel, Field, ValidationError
25
+
26
+ from core.harness import json_store
27
+ from core.harness import spec as harness_spec
28
+
29
+
30
+ class OwnershipPolicy(StrEnum):
31
+ """How ArkaOS treats one harness surface."""
32
+
33
+ OWN = "own"
34
+ OWN_SUBSET = "own-subset"
35
+ SEED = "seed"
36
+ OPERATOR = "operator"
37
+
38
+
39
+ class SurfaceRecord(BaseModel):
40
+ """One claimed surface and the policy it is held under."""
41
+
42
+ surface: str = Field(description="Stable surface id, e.g. settings:hooks")
43
+ policy: OwnershipPolicy
44
+ last_asserted: str | None = Field(
45
+ default=None,
46
+ description="ISO-8601 timestamp of the last C2 assert; C1 never sets it",
47
+ )
48
+ content_sha256: str | None = Field(
49
+ default=None,
50
+ description="Digest of the ArkaOS-owned content at last assert",
51
+ )
52
+
53
+
54
+ class OwnershipManifest(BaseModel):
55
+ """The full ownership manifest for one runtime."""
56
+
57
+ version: int = 1
58
+ runtime: str = "claude-code"
59
+ surfaces: list[SurfaceRecord] = Field(default_factory=list)
60
+
61
+ @classmethod
62
+ def default(cls, runtime: str = "claude-code") -> OwnershipManifest:
63
+ """Manifest derived from the runtime spec's surface table."""
64
+ runtime_spec = harness_spec.spec_for(runtime)
65
+ return cls(
66
+ runtime=runtime,
67
+ surfaces=[
68
+ SurfaceRecord(surface=s.surface, policy=OwnershipPolicy(s.policy))
69
+ for s in runtime_spec.surfaces
70
+ ],
71
+ )
72
+
73
+
74
+ def load_manifest(path: Path) -> tuple[OwnershipManifest | None, str | None]:
75
+ """Tolerant manifest load: ``(manifest, None)`` or ``(None, error)``.
76
+
77
+ Error vocabulary extends ``json_store.LoadResult`` with
78
+ ``invalid-schema`` — a file that parses as JSON but does not
79
+ validate is reported, never raised.
80
+ """
81
+ result = json_store.load_json(path)
82
+ if not result.ok:
83
+ return None, result.error
84
+ try:
85
+ return OwnershipManifest.model_validate(result.data), None
86
+ except ValidationError:
87
+ return None, "invalid-schema"
@@ -0,0 +1,72 @@
1
+ """Canonical harness paths, resolved at call time.
2
+
3
+ Every path here takes an explicit override — ``home`` for the
4
+ user-tree paths (defaulting to ``Path.home()``), ``root`` for
5
+ ``hooks_dir`` — resolved at CALL time, so tests and the C2 manager can
6
+ operate on an alternate tree without monkeypatching. Nothing here is a
7
+ module-level constant: paths captured at import bind to whatever root
8
+ the importing process started with, which is exactly the split-root
9
+ failure mode PR-A2 closed (``ARKAOS_ROOT`` in the shell pointing one
10
+ way, ``.repo-path`` another).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from pathlib import Path
16
+
17
+ from core.hooks._shared import resolve_arkaos_root
18
+
19
+
20
+ def claude_home(home: Path | None = None) -> Path:
21
+ """``~/.claude`` — the Claude Code configuration tree."""
22
+ return (home or Path.home()) / ".claude"
23
+
24
+
25
+ def claude_settings_path(home: Path | None = None) -> Path:
26
+ """``~/.claude/settings.json`` — the executed harness surface."""
27
+ return claude_home(home) / "settings.json"
28
+
29
+
30
+ def claude_global_md_path(home: Path | None = None) -> Path:
31
+ """``~/.claude/CLAUDE.md`` — global instructions (C5 managed block)."""
32
+ return claude_home(home) / "CLAUDE.md"
33
+
34
+
35
+ def arkaos_home(home: Path | None = None) -> Path:
36
+ """``~/.arkaos`` — ArkaOS user-mutable data."""
37
+ return (home or Path.home()) / ".arkaos"
38
+
39
+
40
+ def ownership_manifest_path(home: Path | None = None) -> Path:
41
+ """``~/.arkaos/ownership.json`` — the C2 ownership manifest."""
42
+ return arkaos_home(home) / "ownership.json"
43
+
44
+
45
+ def audit_dir(home: Path | None = None) -> Path:
46
+ """``~/.arkaos/audit`` — mutation audit trails."""
47
+ return arkaos_home(home) / "audit"
48
+
49
+
50
+ def harness_audit_log_path(home: Path | None = None) -> Path:
51
+ """``~/.arkaos/audit/harness-mutations.jsonl`` (written by C2)."""
52
+ return audit_dir(home) / "harness-mutations.jsonl"
53
+
54
+
55
+ def hard_deny_extension_path(home: Path | None = None) -> Path:
56
+ """``~/.arkaos/hard-deny.json`` — operator hard-deny extensions."""
57
+ return arkaos_home(home) / "hard-deny.json"
58
+
59
+
60
+ def arkaos_config_path(home: Path | None = None) -> Path:
61
+ """``~/.arkaos/config.json`` — feature flags (hardEnforcement, ...)."""
62
+ return arkaos_home(home) / "config.json"
63
+
64
+
65
+ def hooks_dir(root: str | None = None) -> Path:
66
+ """``<arkaos-root>/config/hooks`` — the deployed hook scripts.
67
+
68
+ The root goes through the PR-A2 validated chain
69
+ (``core.hooks._shared.resolve_arkaos_root``) at call time; passing
70
+ ``root`` overrides it for tests.
71
+ """
72
+ return Path(root or resolve_arkaos_root()) / "config" / "hooks"
@@ -0,0 +1,182 @@
1
+ """Desired-state harness spec, keyed by runtime (PR-C1).
2
+
3
+ Until now the desired state of the operator's harness lived only inside
4
+ installer JavaScript: ``installer/hard-deny.js`` (the curated hard-deny
5
+ rules) and ``installer/adapters/claude-code.js`` (which hook events get
6
+ registered, with which scripts and timeouts). This module is the Python
7
+ port that ``drift`` (C1) and the ``ClaudeConfigManager`` (C2) read.
8
+
9
+ PARITY IS PINNED, NOT PROMISED: ``tests/python/test_harness_spec.py``
10
+ parses both JS sources and asserts this module matches them — rule
11
+ list in full, same content and order, hook registrations by
12
+ event/script/timeout/matcher. Editing either side without the other
13
+ fails the suite. The installer files remain the EXECUTED path until C4
14
+ flips ownership.
15
+
16
+ ``config/settings-template.json`` is NOT a source here — the template
17
+ is not the executed path (PR-B5 lesson: it declared agent-provision for
18
+ months while the adapter never registered it).
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from dataclasses import dataclass
24
+
25
+ # Port of DEFAULT_HARD_DENY_RULES in installer/hard-deny.js — same
26
+ # rules, same order. Parity-pinned by test_harness_spec.py.
27
+ HARD_DENY_RULES: tuple[str, ...] = (
28
+ # Destructive git operations
29
+ "Bash(git push --force*)",
30
+ "Bash(git push -f*)",
31
+ "Bash(git reset --hard*)",
32
+ "Bash(git clean -fd*)",
33
+ "Bash(git branch -D*)",
34
+ # Filesystem destruction
35
+ "Bash(rm -rf /*)",
36
+ "Bash(rm -rf ~/*)",
37
+ "Bash(rm -rf ~)",
38
+ "Bash(rm -rf .*)",
39
+ # Secrets and credential paths
40
+ "Read(~/.ssh/*)",
41
+ "Read(~/.ssh/**)",
42
+ "Read(~/.aws/credentials)",
43
+ "Read(~/.aws/config)",
44
+ "Read(~/.gnupg/*)",
45
+ "Read(~/.gnupg/**)",
46
+ "Read(~/.npmrc)",
47
+ "Read(~/.docker/config.json)",
48
+ "Read(~/.config/gh/*)",
49
+ "Read(/etc/shadow)",
50
+ "Read(/etc/sudoers)",
51
+ # Writes to credential / config dirs
52
+ "Write(~/.ssh/*)",
53
+ "Write(~/.aws/credentials)",
54
+ "Write(~/.gnupg/*)",
55
+ "Write(~/.npmrc)",
56
+ # Process / privilege escalation
57
+ "Bash(sudo *)",
58
+ "Bash(su -*)",
59
+ "Bash(chmod 777*)",
60
+ "Bash(curl * | sh*)", # arka:sec-ok(curl-pipe-shell): deny-rule literal
61
+ "Bash(curl * | bash*)", # arka:sec-ok(curl-pipe-shell): deny-rule literal
62
+ "Bash(wget * | sh*)",
63
+ "Bash(wget * | bash*)",
64
+ )
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class HookRegistration:
69
+ """One hook entry the adapter writes into ``settings.json``.
70
+
71
+ ``script`` is the basename without extension — the adapter picks
72
+ ``.sh`` / ``.ps1`` / ``.cjs`` per platform and fastpath, so drift
73
+ matching accepts any of the three.
74
+ """
75
+
76
+ event: str
77
+ script: str
78
+ timeout: int
79
+ matcher: str | None = None
80
+ posix_only: bool = False
81
+ conditional: bool = False
82
+
83
+
84
+ # Port of the settings.hooks registrations in
85
+ # installer/adapters/claude-code.js — parity-pinned.
86
+ _CLAUDE_CODE_HOOKS: tuple[HookRegistration, ...] = (
87
+ HookRegistration("SessionStart", "session-start", 15),
88
+ HookRegistration("UserPromptSubmit", "user-prompt-submit", 20),
89
+ HookRegistration("PostToolUse", "post-tool-use", 5),
90
+ HookRegistration("PostToolUseFailure", "post-tool-use", 5),
91
+ HookRegistration("PreToolUse", "pre-tool-use", 10),
92
+ HookRegistration(
93
+ "PreToolUse",
94
+ "agent-provision",
95
+ 10,
96
+ matcher="Task",
97
+ posix_only=True,
98
+ conditional=True,
99
+ ),
100
+ HookRegistration("Stop", "stop", 5),
101
+ HookRegistration("SubagentStop", "subagent-stop", 10),
102
+ HookRegistration("SessionEnd", "session-end", 15),
103
+ HookRegistration("PreCompact", "pre-compact", 30),
104
+ HookRegistration("CwdChanged", "cwd-changed", 5),
105
+ )
106
+
107
+
108
+ @dataclass(frozen=True)
109
+ class SurfaceSpec:
110
+ """One harness surface ArkaOS claims, with its default policy.
111
+
112
+ ``policy`` values are the ``manifest.OwnershipPolicy`` vocabulary;
113
+ kept as strings here so ``spec`` stays import-light (manifest
114
+ imports spec, not the reverse).
115
+ """
116
+
117
+ surface: str
118
+ policy: str
119
+ detail: str
120
+
121
+
122
+ _CLAUDE_CODE_SURFACES: tuple[SurfaceSpec, ...] = (
123
+ SurfaceSpec(
124
+ "settings:hooks",
125
+ "own-subset",
126
+ "ArkaOS hook entries per event; user hooks on the same events stay",
127
+ ),
128
+ SurfaceSpec(
129
+ "settings:autoMode.hard_deny",
130
+ "own-subset",
131
+ "Curated deny rules merged in; operator rules are never removed",
132
+ ),
133
+ SurfaceSpec(
134
+ "settings:statusLine",
135
+ "seed",
136
+ "Branded status bar; an operator replacement is adopted once "
137
+ "C2 owns the surface",
138
+ ),
139
+ SurfaceSpec(
140
+ "settings:worktree",
141
+ "seed",
142
+ "worktree.baseRef default; an operator change is adopted once "
143
+ "C2 owns the surface",
144
+ ),
145
+ )
146
+
147
+
148
+ @dataclass(frozen=True)
149
+ class RuntimeSpec:
150
+ """The desired harness state for one runtime."""
151
+
152
+ runtime: str
153
+ hook_registrations: tuple[HookRegistration, ...]
154
+ hard_deny_rules: tuple[str, ...]
155
+ surfaces: tuple[SurfaceSpec, ...]
156
+
157
+
158
+ _RUNTIME_SPECS: dict[str, RuntimeSpec] = {
159
+ "claude-code": RuntimeSpec(
160
+ runtime="claude-code",
161
+ hook_registrations=_CLAUDE_CODE_HOOKS,
162
+ hard_deny_rules=HARD_DENY_RULES,
163
+ surfaces=_CLAUDE_CODE_SURFACES,
164
+ ),
165
+ }
166
+
167
+ SUPPORTED_RUNTIMES: tuple[str, ...] = tuple(sorted(_RUNTIME_SPECS))
168
+
169
+
170
+ def spec_for(runtime: str) -> RuntimeSpec:
171
+ """The :class:`RuntimeSpec` for ``runtime``.
172
+
173
+ Raises ``ValueError`` for an unknown runtime — a silent empty spec
174
+ would make drift report a perfectly-owned harness for a runtime
175
+ this package knows nothing about.
176
+ """
177
+ try:
178
+ return _RUNTIME_SPECS[runtime]
179
+ except KeyError:
180
+ raise ValueError(
181
+ f"unknown runtime {runtime!r}; supported: {', '.join(SUPPORTED_RUNTIMES)}"
182
+ ) from None
@@ -76,6 +76,22 @@ _EVIDENCE_CONTRACT = (
76
76
  " 10 lines."
77
77
  )
78
78
 
79
+ # PR-A4: routing was fulfilled in PROSE — the model announced the squad
80
+ # and never invoked the Skill tool, so the department's workflows and
81
+ # contracts never entered context (the label without the payload). This
82
+ # contract defines fulfilment; enforcement stays with A5a/A5b,
83
+ # telemetry-gated.
84
+ _SKILL_ROUTING_CONTRACT = (
85
+ "\n\n[ARKA:SKILL-CONTRACT] Routing IS tool invocation. A department"
86
+ " route is fulfilled by calling the department's hub skill — e.g."
87
+ " Skill(arka-dev), Skill(arka-brand), Skill(arka-content) — BEFORE"
88
+ " substantive work; announcing the squad in prose without invoking"
89
+ " the skill is not routing; it is the label without the payload."
90
+ "\nNever derive a hub name by concatenation — [arka:skill-hint] tags"
91
+ " name the exact call to make. The [arka:trivial] bypass is"
92
+ " unchanged."
93
+ )
94
+
79
95
  _META_TAG_CONTRACT = (
80
96
  "\n\n[ARKA:META-TAG] Every substantive response ends with a single line:"
81
97
  "\n [arka:meta] kb=N research=X persona=Y gap=Z critic=W"
@@ -348,6 +364,7 @@ def build_context(cwd: str) -> str:
348
364
  """
349
365
  parts = [
350
366
  _EVIDENCE_CONTRACT,
367
+ _SKILL_ROUTING_CONTRACT,
351
368
  _META_TAG_CONTRACT,
352
369
  _authority_brief(cwd),
353
370
  _model_fabric(),
@@ -31,7 +31,7 @@ import os
31
31
  import re
32
32
  import subprocess
33
33
  import sys
34
- from datetime import datetime, timezone
34
+ from datetime import UTC, datetime
35
35
  from pathlib import Path
36
36
 
37
37
  _TELEMETRY_DIR = Path.home() / ".arkaos" / "telemetry"
@@ -135,7 +135,10 @@ def _load_commands_registry() -> list[dict]:
135
135
 
136
136
  def _routing_lines(prompt: str, session_id: str, cwd: str) -> list[str]:
137
137
  """UserPromptSubmit routing parity (core/hooks/user_prompt_submit.py):
138
- [ARKA:ROUTE] on every prompt + L1 [dept:X] + L5 [hint:/cmd] +
138
+ [ARKA:ROUTE] on every prompt + L1 [dept:X] + L5 [arka:skill-hint]
139
+ Skill(<hub>) -> /cmd (PR-A4 imperative form; hub resolved by
140
+ layers._hub_skill, never by slug concatenation — a /mkt hint
141
+ renders Skill(arka-marketing)) +
139
142
  [ARKA:WORKFLOW-REQUIRED] on creation/implementation prompts. The
140
143
  directive strings and the verb regex are IMPORTED from the claude
141
144
  hook — one source of truth, zero drift."""
@@ -170,7 +173,7 @@ def _routing_lines(prompt: str, session_id: str, cwd: str) -> list[str]:
170
173
  hints = CommandHintsLayer(commands=commands).compute(ctx)
171
174
  if hints.tag:
172
175
  lines.append(hints.tag)
173
- except Exception: # noqa: BLE001 — routing hints are a bonus, never a blocker
176
+ except Exception:
174
177
  pass
175
178
  if prompt[0] not in ("/", "!") and _WF_VERB_RE.search(prompt):
176
179
  lines.append(_WORKFLOW_DIRECTIVE.strip())
@@ -285,7 +288,7 @@ def _action_post_tool(payload: dict) -> dict:
285
288
  _append_jsonl(
286
289
  _TELEMETRY_DIR / "opencode-tools.jsonl",
287
290
  {
288
- "ts": datetime.now(timezone.utc).isoformat(),
291
+ "ts": datetime.now(UTC).isoformat(),
289
292
  "session": session_id,
290
293
  "tool": mapped,
291
294
  "ok": bool(payload.get("ok", True)),
@@ -319,7 +322,7 @@ def _enqueue_turn_capture(session_id: str, text: str, cwd: str) -> None:
319
322
  proc.stdin.close()
320
323
  except (BrokenPipeError, OSError):
321
324
  pass
322
- except Exception: # noqa: BLE001 — a hook bridge never breaks a turn
325
+ except Exception:
323
326
  pass
324
327
 
325
328
 
@@ -347,7 +350,7 @@ def _action_idle(payload: dict) -> dict:
347
350
  _append_jsonl(
348
351
  _TELEMETRY_DIR / "opencode-compliance.jsonl",
349
352
  {
350
- "ts": datetime.now(timezone.utc).isoformat(),
353
+ "ts": datetime.now(UTC).isoformat(),
351
354
  "session": session_id,
352
355
  "kb_cite": result.reason,
353
356
  "meta_tag": "[arka:meta]" in text,
@@ -388,7 +391,7 @@ def _handoff_line(session_id: str, project: str) -> str:
388
391
  try:
389
392
  _handoff_marker(session_id).parent.mkdir(parents=True, exist_ok=True)
390
393
  _handoff_marker(session_id).write_text(
391
- json.dumps({"shown_at": datetime.now(timezone.utc).isoformat()}),
394
+ json.dumps({"shown_at": datetime.now(UTC).isoformat()}),
392
395
  encoding="utf-8",
393
396
  )
394
397
  except OSError:
@@ -440,7 +443,7 @@ def _action_memory(payload: dict) -> dict:
440
443
  handoff = _handoff_line(session_id, project)
441
444
  if handoff:
442
445
  lines.insert(0, handoff)
443
- except Exception: # noqa: BLE001 — memory is a bonus, never a blocker
446
+ except Exception:
444
447
  return {"context": []}
445
448
  return {"context": lines[:6]}
446
449
 
@@ -460,7 +463,7 @@ def _action_compact(payload: dict) -> dict:
460
463
  current = state.get_state()
461
464
  if current:
462
465
  context.append(f"Workflow activo: {json.dumps(current)[:400]}")
463
- except Exception: # noqa: BLE001 — fail-open
466
+ except Exception:
464
467
  pass
465
468
  return {"context": context}
466
469
 
@@ -488,7 +491,7 @@ def main() -> int:
488
491
  else:
489
492
  try:
490
493
  result = handler(payload)
491
- except Exception as exc: # noqa: BLE001 — fail-open, never block
494
+ except Exception as exc:
492
495
  result = {"error": f"{type(exc).__name__}: {exc}"}
493
496
  sys.stdout.write(json.dumps(result, ensure_ascii=False) + "\n")
494
497
  return 0
@@ -386,8 +386,89 @@ class BranchLayer(Layer):
386
386
  # --- L5: Command Hints ---
387
387
 
388
388
 
389
+ # PR-A4: department slug -> deployed hub-skill directory. Both slug
390
+ # vocabularies reach this layer — the registry department field (which
391
+ # says "leadership") and the command-prefix slugs (which say "lead") —
392
+ # and both must resolve. Registry values need no key: identity already
393
+ # yields the right hub (leadership -> arka-leadership). The keys exist
394
+ # for the four prefix slugs whose hub directory differs (mkt, fin,
395
+ # strat, lead — the same alias set
396
+ # tests/python/test_skill_routing_integrity.py pins) plus the two
397
+ # orchestrator entries ("arka" and "do" both belong to the main
398
+ # orchestrator skill; /do is the universal router in arka/SKILL.md).
399
+ # An unknown department falls back to the identity form — a
400
+ # wrong-but-plausible hint beats a crashed layer.
401
+ _DEPT_HUB_EXCEPTIONS = {
402
+ "mkt": "arka-marketing",
403
+ "fin": "arka-finance",
404
+ "strat": "arka-strategy",
405
+ "lead": "arka-leadership",
406
+ "arka": "arka",
407
+ "do": "arka",
408
+ }
409
+
410
+
411
+ def _hub_skill(department: str) -> str:
412
+ return _DEPT_HUB_EXCEPTIONS.get(department, f"arka-{department}")
413
+
414
+
415
+ def _hint_department(cmd: dict) -> str:
416
+ """Department for the hint's hub skill.
417
+
418
+ Falls back to the command prefix ("/dev feature" -> "dev",
419
+ "/arka-do" -> "arka") for registry entries that predate the
420
+ department field — an approximate hub beats no hint.
421
+ """
422
+ dept = str(cmd.get("department", ""))
423
+ if dept:
424
+ return dept
425
+ prefix = str(cmd.get("command", "")).lstrip("/").split(" ")[0]
426
+ return prefix.split("-")[0]
427
+
428
+
429
+ def _score_commands(
430
+ commands: list[dict], text: str
431
+ ) -> list[tuple[int, str, str]]:
432
+ """Keyword-score the registry commands against the prompt text,
433
+ best score first; zero-score commands never qualify."""
434
+ scored = []
435
+ for cmd in commands:
436
+ keywords = cmd.get("keywords", [])
437
+ score = sum(1 for kw in keywords if kw.lower() in text)
438
+ if score > 0:
439
+ scored.append((
440
+ score,
441
+ cmd.get("command", ""),
442
+ _hint_department(cmd),
443
+ ))
444
+ scored.sort(reverse=True)
445
+ return scored
446
+
447
+
448
+ def _hint_tag(top: list[tuple[int, str, str]]) -> str:
449
+ """Render the imperative hint tag for the top-scored commands.
450
+
451
+ EACH hint pairs the command with ITS OWN department's hub — the
452
+ top-2 can legitimately span two departments ("standup color
453
+ palette" matches /brand colors AND /pm story on the live registry),
454
+ and a hint whose hub belongs to the other command is a dead
455
+ instruction (QG A4, Francisca B1).
456
+ """
457
+ return " ".join(
458
+ f"[arka:skill-hint] Skill({_hub_skill(dept)}) -> {command}"
459
+ for _, command, dept in top
460
+ )
461
+
462
+
389
463
  class CommandHintsLayer(Layer):
390
- """L5: Matching commands from the registry for non-explicit requests."""
464
+ """L5: Matching commands from the registry for non-explicit requests.
465
+
466
+ PR-A4: hints are IMPERATIVE — `[arka:skill-hint] Skill(arka-dev) ->
467
+ /dev feature <description>` names the exact tool call that fulfils
468
+ the route. The old `[hint:/cmd]` form was declarative and routinely
469
+ ignored: the model announced the squad in prose and the skill's
470
+ content never entered context.
471
+ """
391
472
 
392
473
  def __init__(self, commands: list[dict] | None = None) -> None:
393
474
  self._commands = commands or []
@@ -429,18 +510,10 @@ class CommandHintsLayer(Layer):
429
510
  cached=False,
430
511
  )
431
512
 
432
- # Score commands by keyword match
433
- scored = []
434
- for cmd in self._commands:
435
- keywords = cmd.get("keywords", [])
436
- score = sum(1 for kw in keywords if kw.lower() in text)
437
- if score > 0:
438
- scored.append((score, cmd.get("command", "")))
439
-
440
- scored.sort(reverse=True)
441
- hints = [cmd for _, cmd in scored[:2]]
513
+ top = _score_commands(self._commands, text)[:2]
514
+ hints = [command for _, command, _ in top]
442
515
 
443
- tags = " ".join(f"[hint:{h}]" for h in hints)
516
+ tags = _hint_tag(top)
444
517
  ms = int((time.time() - start) * 1000)
445
518
  return LayerResult(
446
519
  layer_id=self.id,
@@ -1,6 +1,6 @@
1
1
  # ArkaOS — The Operating System for AI Agent Teams
2
2
 
3
- > v4.44.0 — 89 agents, 17 departments, 332 skills. Generated by `scripts/harness_gen.py`; do not edit.
3
+ > v4.46.0 — 89 agents, 17 departments, 332 skills. Generated by `scripts/harness_gen.py`; do not edit.
4
4
 
5
5
  You are operating within ArkaOS. Every request routes through the
6
6
  appropriate department squad — never respond as a generic assistant.
@@ -1,6 +1,6 @@
1
1
  # ArkaOS — The Operating System for AI Agent Teams
2
2
 
3
- > v4.44.0 — 89 agents, 17 departments, 332 skills. Generated by `scripts/harness_gen.py`; do not edit.
3
+ > v4.46.0 — 89 agents, 17 departments, 332 skills. Generated by `scripts/harness_gen.py`; do not edit.
4
4
 
5
5
  You are operating within ArkaOS. Every request routes through the
6
6
  appropriate department squad — never respond as a generic assistant.
@@ -1,11 +1,11 @@
1
1
  ---
2
- description: ArkaOS v4.44.0 agent-team contract
2
+ description: ArkaOS v4.46.0 agent-team contract
3
3
  alwaysApply: true
4
4
  ---
5
5
 
6
6
  # ArkaOS — The Operating System for AI Agent Teams
7
7
 
8
- > v4.44.0 — 89 agents, 17 departments, 332 skills. Generated by `scripts/harness_gen.py`; do not edit.
8
+ > v4.46.0 — 89 agents, 17 departments, 332 skills. Generated by `scripts/harness_gen.py`; do not edit.
9
9
 
10
10
  You are operating within ArkaOS. Every request routes through the
11
11
  appropriate department squad — never respond as a generic assistant.
@@ -1,6 +1,6 @@
1
1
  # ArkaOS — The Operating System for AI Agent Teams
2
2
 
3
- > v4.44.0 — 89 agents, 17 departments, 332 skills. Generated by `scripts/harness_gen.py`; do not edit.
3
+ > v4.46.0 — 89 agents, 17 departments, 332 skills. Generated by `scripts/harness_gen.py`; do not edit.
4
4
 
5
5
  You are operating within ArkaOS. Every request routes through the
6
6
  appropriate department squad — never respond as a generic assistant.
@@ -1,6 +1,6 @@
1
1
  # ArkaOS — The Operating System for AI Agent Teams
2
2
 
3
- > v4.44.0 — 89 agents, 17 departments, 332 skills. Generated by `scripts/harness_gen.py`; do not edit.
3
+ > v4.46.0 — 89 agents, 17 departments, 332 skills. Generated by `scripts/harness_gen.py`; do not edit.
4
4
 
5
5
  You are operating within ArkaOS. Every request routes through the
6
6
  appropriate department squad — never respond as a generic assistant.
@@ -3,7 +3,7 @@ description: "Software Architect — ArkaOS /dev department"
3
3
  mode: subagent
4
4
  ---
5
5
 
6
- You are Gabriel, Software Architect of the ArkaOS /dev department (v4.44.0; generated by scripts/harness_gen.py — do not edit).
6
+ You are Gabriel, Software Architect of the ArkaOS /dev department (v4.46.0; generated by scripts/harness_gen.py — do not edit).
7
7
 
8
8
  Expertise: system design, system visualization via dev/diagram (architecture + dataflow diagrams delivered as browser artifacts), domain modeling (event storming, bounded contexts), design patterns (GoF, PoEAA), business / domain analysis, API design.
9
9
 
@@ -3,7 +3,7 @@ description: "Creative Director — ArkaOS /brand department"
3
3
  mode: subagent
4
4
  ---
5
5
 
6
- You are Valentina, Creative Director of the ArkaOS /brand department (v4.44.0; generated by scripts/harness_gen.py — do not edit).
6
+ You are Valentina, Creative Director of the ArkaOS /brand department (v4.46.0; generated by scripts/harness_gen.py — do not edit).
7
7
 
8
8
  Expertise: brand identity creation, reference-video visual analysis via dev/watch (complete frames + transcript — motion and art direction judged on evidence, never on screenshots), visual design direction, UX/UI strategy, design systems, brand voice & tone.
9
9
 
@@ -3,7 +3,7 @@ description: "Chief Financial Officer — ArkaOS /fin department"
3
3
  mode: subagent
4
4
  ---
5
5
 
6
- You are Helena, Chief Financial Officer of the ArkaOS /fin department (v4.44.0; generated by scripts/harness_gen.py — do not edit).
6
+ You are Helena, Chief Financial Officer of the ArkaOS /fin department (v4.46.0; generated by scripts/harness_gen.py — do not edit).
7
7
 
8
8
  Expertise: financial planning & analysis, valuation & investment, unit economics & SaaS metrics, risk management & ERM, fundraising & cap tables, cash flow management.
9
9
 
@@ -3,7 +3,7 @@ description: "Chief of Staff & Governance Lead — ArkaOS /org department"
3
3
  mode: subagent
4
4
  ---
5
5
 
6
- You are Afonso, Chief of Staff & Governance Lead of the ArkaOS /org department (v4.44.0; generated by scripts/harness_gen.py — do not edit).
6
+ You are Afonso, Chief of Staff & Governance Lead of the ArkaOS /org department (v4.46.0; generated by scripts/harness_gen.py — do not edit).
7
7
 
8
8
  Expertise: meeting cadence (daily/weekly/quarterly/annual), OKR & CFR orchestration cross-department, decision records & RACI, premortem / blameless postmortem rituals, governance, board & founder-CEO succession, strategic alignment & single-threaded leadership.
9
9