arkaos 2.19.2 → 2.20.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 2.19.2
1
+ 2.20.0-beta.1
package/arka/SKILL.md CHANGED
@@ -12,10 +12,45 @@ allowed-tools: [Read, Write, Edit, Bash, Grep, Glob, Agent, WebFetch, WebSearch]
12
12
  > **The Operating System for AI Agent Teams**
13
13
  > 65 agents. 17 departments. 244+ skills. Multi-runtime. Dashboard. Knowledge RAG.
14
14
 
15
- ## ⛔ Enforcement contract (read before responding)
15
+ ## ⛔ Mandatory 13-phase flow (NON-NEGOTIABLE)
16
16
 
17
- This overrides every default. If the UserPromptSubmit hook injected a
18
- `[ARKA:WORKFLOW-REQUIRED]` tag, you MUST, on the first line of your reply:
17
+ Every non-trivial request runs the canonical flow. Full spec:
18
+ `arka/skills/flow/SKILL.md`. Constitution rule: `mandatory-flow`.
19
+
20
+ ```
21
+ 1. Input (verbatim)
22
+ 2. Get context (profile, repo, git, cwd tag, session digests)
23
+ 3. Decide route -> emit [arka:routing] <dept> -> <lead>
24
+ 4. Call hierarchy (Tier 0 when strategic/cross-dept/security/financial)
25
+ 5. Research (Obsidian + vector DB, cite sources or declare gap)
26
+ 6. Call team (dispatch specialists via Agent tool)
27
+ 7. Plan with six parallel reviewers:
28
+ positive analyst / devil's advocate / Q&A / KB research /
29
+ best-solution validator / pessimistic analyst
30
+ 8. Present plan (save to Obsidian + vector DB + ~/.arkaos/plans/)
31
+ 9. Wait for EXPLICIT approval (silence is not approval)
32
+ 10. TODO list (atomic, ordered, independently verifiable)
33
+ 11. Per-todo loop:
34
+ team call -> complete -> QA (all tests, E2E, Playwright)
35
+ -> Security review -> Quality Gate (Marta+Eduardo+Francisca, Opus)
36
+ -> Document (Obsidian + vector DB)
37
+ 12. Loop until TODO is exhausted
38
+ 13. Detailed summary (what was done, where, how to verify, what is open)
39
+ ```
40
+
41
+ Before every step, emit `[arka:phase:N] <label>` on its own line.
42
+
43
+ **Trivial bypass** (the only bypass): single-file edit under 10 lines
44
+ with an imperative verb. Emit `[arka:trivial] <reason>` as the first
45
+ line and proceed directly.
46
+
47
+ No task type, no context, no runtime setting overrides this flow.
48
+
49
+ ## Enforcement contract
50
+
51
+ If the UserPromptSubmit hook injected `[ARKA:WORKFLOW-REQUIRED]`, or if
52
+ the SessionStart systemMessage shows `[ARKA:MANDATORY-FLOW]`, the flow
53
+ above is the contract. The first non-trivial line of your reply MUST be:
19
54
 
20
55
  ```
21
56
  [arka:routing] <department-slug> -> <lead-agent>
@@ -0,0 +1,160 @@
1
+ ---
2
+ name: arka-flow
3
+ description: >
4
+ ArkaOS canonical mandatory workflow. 13 phases. This is the default
5
+ execution contract for every user request inside an ArkaOS-managed
6
+ context. Not optional. Not overridable.
7
+ allowed-tools: [Read, Write, Edit, Bash, Grep, Glob, Agent, WebFetch, WebSearch]
8
+ ---
9
+
10
+ # ArkaOS — Mandatory Workflow
11
+
12
+ > This flow runs on **every** user request inside an ArkaOS-managed context.
13
+ > There is no "simple mode". There is no "skip the workflow this time".
14
+ > The only exception is a single-file <10-line trivial edit, which may emit
15
+ > `[arka:trivial] <reason>` and bypass. Everything else runs the 13 phases.
16
+
17
+ ## The 13 phases (strict sequence)
18
+
19
+ ### Phase 1 — Input
20
+ Receive the user request verbatim. Do not paraphrase before Phase 2.
21
+
22
+ ### Phase 2 — Get context
23
+ Read the active context. Sources, in order:
24
+ - `~/.arkaos/profile.json` (who, what company, what language)
25
+ - Current working directory + `.claude/CLAUDE.md` + `.claude/rules/`
26
+ - Git branch and recent commits
27
+ - `cwd-changed` tag from the hook (ecosystem, stack, descriptor)
28
+ - Most recent `~/.arkaos/sessions/` digest if present
29
+
30
+ ### Phase 3 — Decide context and route
31
+ State the target department explicitly:
32
+
33
+ ```
34
+ [arka:routing] <department-slug> -> <lead-agent>
35
+ ```
36
+
37
+ Mapping (full list in `arka/SKILL.md`): dev→Paulo, brand→Valentina,
38
+ kb→Clara, mkt→Luna, content→Rafael, landing→Ines, ecom→Ricardo,
39
+ saas→Tiago, sales→Miguel, pm→Carolina, ops→Daniel, strat→Tomas,
40
+ fin→Helena, lead→Rodrigo, org→Sofia, community→Beatriz.
41
+
42
+ ### Phase 4 — Call hierarchy
43
+ Escalate to Tier 0 (C-Suite) for review when the request is strategic,
44
+ cross-department, security-sensitive, or financial. Tier 0 = Marco (CTO),
45
+ Helena (CFO), Sofia (COO), Marta (CQO), Eduardo (Copy Director),
46
+ Francisca (Tech & UX Director). Otherwise, squad lead owns.
47
+
48
+ ### Phase 5 — Understand and research the context
49
+ Query the knowledge base:
50
+ - `mcp__obsidian__search_notes` for prior work on the topic
51
+ - Vector DB semantic search when installed
52
+ - Prior session digests at `~/.arkaos/session-digests/`
53
+ - Relevant Forge plans at `~/.arkaos/plans/`
54
+
55
+ Cite the sources found. If the KB has nothing and the ask is non-trivial,
56
+ state the gap explicitly and propose filling it.
57
+
58
+ ### Phase 6 — Call team
59
+ Dispatch specialists via the `Agent` tool. The squad lead from Phase 3
60
+ names them. Specialists run in parallel when work is independent.
61
+
62
+ ### Phase 7 — Plan and make the spec
63
+ Run six parallel reviewers on the plan:
64
+
65
+ | Reviewer | Question it owns |
66
+ |---|---|
67
+ | Positive analyst | Why this solution is the right one |
68
+ | Devil's advocate | Strongest case against the chosen path |
69
+ | Q&A / input collector | What is still unknown and must be answered |
70
+ | Obsidian + DB researcher | What the knowledge base already says |
71
+ | Best-solution validator | Is there a better option we have not tried |
72
+ | Pessimistic analyst | What breaks, at what scale, in what scenario |
73
+
74
+ Synthesise into a spec. Reference the Conclave (`arka-conclave`) or
75
+ the Forge (`arka-forge`) when complexity warrants.
76
+
77
+ ### Phase 8 — Present the plan
78
+ Save the spec to:
79
+ - Obsidian (`docs/superpowers/specs/` or vault equivalent)
80
+ - Vector DB (when available via cache or KB cache)
81
+ - Session cache at `~/.arkaos/plans/`
82
+
83
+ Print the plan inline for the user.
84
+
85
+ ### Phase 9 — Wait for approval
86
+ Two branches:
87
+
88
+ - **Approve** → Phase 10
89
+ - **More input** → loop to Phase 7
90
+
91
+ Approval must be explicit. Silence is not approval.
92
+
93
+ ### Phase 10 — TODO list
94
+ Break the approved plan into atomic, ordered items. Persist to the
95
+ task tracker. Each item must be independently verifiable.
96
+
97
+ ### Phase 11 — Per-todo loop
98
+ For each item, in order:
99
+
100
+ 1. Organise a call with all team members relevant to that item.
101
+ 2. Complete the todo.
102
+ 3. **QA** — all tests, end-to-end, Playwright browser tests when the
103
+ item touches UI, report saved to Obsidian + vector DB + cache.
104
+ - Fail → back to the todo. Do not advance.
105
+ 4. **Security review** — Tier 0 security specialist checks for flaws,
106
+ injection, missing auth, data exposure.
107
+ - Fail → back to the todo.
108
+ 5. **Quality Gate** — Marta (CQO) orchestrates the right specialists
109
+ for the area. If a specialist is missing, stop and advise the user
110
+ to create one via `/arka personas` + provide the knowledge.
111
+ - Fail → back to the todo.
112
+ 6. Document — save the completed work to Obsidian + vector DB.
113
+ 7. Next todo.
114
+
115
+ ### Phase 12 — Loop until TODO list is fully done
116
+ Do not skip items. Do not batch QA or Security across multiple
117
+ items — each item runs the full gate chain.
118
+
119
+ ### Phase 13 — Detailed summary
120
+ When the TODO list is exhausted, emit a final summary: what was done,
121
+ where it lives, how to verify, what is open for next time.
122
+
123
+ ## Visibility requirements
124
+
125
+ Every phase MUST emit a visibility tag the user can see:
126
+
127
+ ```
128
+ [arka:phase:2] get-context
129
+ [arka:phase:3] route -> dev -> Paulo
130
+ [arka:phase:7] plan+6-reviewers
131
+ [arka:phase:11.3] qa -> all pass
132
+ [arka:phase:11.5] quality-gate -> approved
133
+ ```
134
+
135
+ No silent phases. No compound steps.
136
+
137
+ ## Hard no-go list
138
+
139
+ - No Write / Edit before Phase 7 completes for the affected item.
140
+ - No Agent dispatch before Phase 6.
141
+ - No "pushing to master" without passing Phase 11.4 and 11.5 on every
142
+ changed item.
143
+ - No `[arka:trivial]` claim when the change spans more than one file or
144
+ exceeds 10 lines in total.
145
+ - No skipping Phase 9 (approval). The user is the gate, not a hint.
146
+
147
+ ## Related skills
148
+
149
+ - `/arka-forge` — complexity-aware planning when the request is large.
150
+ - `/arka-conclave` — 20-advisor deliberation for strategic decisions.
151
+ - `/arka-spec` — spec gate for Phase 7.
152
+ - `/arka-quality` — Quality Gate orchestration for Phase 11.5.
153
+
154
+ ## Non-negotiable
155
+
156
+ The UserPromptSubmit hook classifies every turn. When it injects
157
+ `[ARKA:WORKFLOW-REQUIRED]`, this flow is the contract. The session-start
158
+ hook embeds it as `systemMessage` so it sits at system-prompt priority
159
+ from turn 1. CLAUDE.md references it. Constitution rule
160
+ `mandatory-flow` codifies it. There is no override.
@@ -66,6 +66,24 @@ enforcement_levels:
66
66
  rule: "Forge plans must pass critic validation and governance check before approval"
67
67
  enforcement: "Plan Critic validates constitution compliance; PostToolUse monitors execution"
68
68
 
69
+ - id: mandatory-flow
70
+ rule: "Every non-trivial request executes the 13-phase canonical flow defined in arka/skills/flow/SKILL.md. No task type, no context, no runtime setting can opt out. The only bypass is [arka:trivial] for single-file edits under 10 lines."
71
+ enforcement: "UserPromptSubmit hook classifies the turn; SessionStart systemMessage embeds the flow at system-prompt priority; arka/SKILL.md references it as default; violation is a constitution breach, not a style issue."
72
+ phases:
73
+ - "1. Input (verbatim)"
74
+ - "2. Get context (profile, repo, KB, session digests)"
75
+ - "3. Decide context and route ([arka:routing] tag)"
76
+ - "4. Call hierarchy (Tier 0 when strategic/cross-dept)"
77
+ - "5. Understand and research (Obsidian + vector DB)"
78
+ - "6. Call team (squad + specialists via Agent)"
79
+ - "7. Plan with six parallel reviewers"
80
+ - "8. Present plan (save to Obsidian + vector DB + cache)"
81
+ - "9. Wait for explicit approval"
82
+ - "10. TODO list"
83
+ - "11. Per-todo loop: team call -> complete -> QA -> Security -> Quality Gate -> Document"
84
+ - "12. Loop until TODO exhausted"
85
+ - "13. Detailed summary"
86
+
69
87
  quality_gate:
70
88
  description: "Mandatory pre-delivery review. Nothing ships without APPROVED verdict."
71
89
  trigger: "After the last execution phase, before delivery to user"
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================================
3
+ # ArkaOS v2 — Shared Workflow Classifier
4
+ #
5
+ # Decides whether a user prompt triggers the mandatory 13-phase flow.
6
+ # Used by: user-prompt-submit.sh, pre-tool-use.sh, stop.sh.
7
+ #
8
+ # Contract:
9
+ # arka_wf_classify "<prompt text>" → echoes "true" or "false", exits 0.
10
+ # arka_wf_mark_required "<session_id>" → writes marker file.
11
+ # arka_wf_is_required "<session_id>" → exits 0 if required, 1 otherwise.
12
+ # arka_wf_clear_required "<session_id>" → removes marker file.
13
+ #
14
+ # Markers live under /tmp/arkaos-wf-required/<session_id>.
15
+ # Python path for mark/clear: delegates to flow_enforcer.py when available,
16
+ # otherwise falls back to touching the marker file directly.
17
+ # ============================================================================
18
+
19
+ ARKA_WF_REQUIRED_DIR="${ARKA_WF_REQUIRED_DIR:-/tmp/arkaos-wf-required}"
20
+
21
+ # Reject any session_id outside [A-Za-z0-9._-]{1,128}. Protects the marker
22
+ # directory from path-traversal writes (CWE-22). Must stay in sync with
23
+ # core/workflow/flow_enforcer.py::_safe_session_id.
24
+ arka_wf_safe_session_id() {
25
+ local session_id="${1:-}"
26
+ [ -z "$session_id" ] && return 1
27
+ [ ${#session_id} -gt 128 ] && return 1
28
+ case "$session_id" in
29
+ *[!A-Za-z0-9._-]*) return 1 ;;
30
+ esac
31
+ return 0
32
+ }
33
+
34
+ # Verb + noun patterns shared with the original inline classifier in
35
+ # user-prompt-submit.sh. Keep in sync when adding new intent verbs.
36
+ ARKA_WF_VERB_PATTERN='(criar?|crie[ms]?|cria[mr]?|adicionar?|adiciona[mr]?|implementar?|implementa[mr]?|desenvolver?|desenvolve[mr]?|construir?|constru[ií]a?[mr]?|fazer?|faz[ae][mr]?|refactor(izar?)?|corrigir?|corrige[mr]?|consertar?|conserta[mr]?|create[sd]?|creating|build(s|ing)?|add(s|ed|ing)?|implement(s|ed|ing)?|develop(s|ed|ing)?|fix(es|ed|ing)?|refactor(s|ed|ing)?|make[sd]?|making)'
37
+
38
+ # Classify: returns "true" if the prompt looks like a creation/
39
+ # implementation/modification request, "false" otherwise.
40
+ # Skips: explicit slash commands (already routed) and bang shells.
41
+ arka_wf_classify() {
42
+ local text="${1:-}"
43
+ [ -z "$text" ] && { echo "false"; return 0; }
44
+
45
+ local first_char
46
+ first_char=$(printf '%s' "$text" | head -c 1)
47
+ if [ "$first_char" = "/" ] || [ "$first_char" = "!" ]; then
48
+ echo "false"
49
+ return 0
50
+ fi
51
+
52
+ if echo "$text" | grep -qiE "\b${ARKA_WF_VERB_PATTERN}\b"; then
53
+ echo "true"
54
+ else
55
+ echo "false"
56
+ fi
57
+ }
58
+
59
+ # Mark that the flow is required for this session. Safe no-op if session_id
60
+ # is empty or fails the allowlist check.
61
+ arka_wf_mark_required() {
62
+ local session_id="${1:-}"
63
+ arka_wf_safe_session_id "$session_id" || return 0
64
+ mkdir -p "$ARKA_WF_REQUIRED_DIR" 2>/dev/null
65
+ date -u +"%Y-%m-%dT%H:%M:%SZ" > "$ARKA_WF_REQUIRED_DIR/$session_id" 2>/dev/null
66
+ }
67
+
68
+ # Test whether flow is required. Exit code 0 = required, 1 = not required.
69
+ arka_wf_is_required() {
70
+ local session_id="${1:-}"
71
+ arka_wf_safe_session_id "$session_id" || return 1
72
+ [ -f "$ARKA_WF_REQUIRED_DIR/$session_id" ]
73
+ }
74
+
75
+ # Clear the requirement marker. Safe no-op if absent or unsafe.
76
+ arka_wf_clear_required() {
77
+ local session_id="${1:-}"
78
+ arka_wf_safe_session_id "$session_id" || return 0
79
+ rm -f "$ARKA_WF_REQUIRED_DIR/$session_id" 2>/dev/null
80
+ return 0
81
+ }
82
+
83
+ # When invoked directly (not sourced), expose a simple CLI for ad-hoc use.
84
+ if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
85
+ case "${1:-}" in
86
+ classify) arka_wf_classify "${2:-}" ;;
87
+ mark) arka_wf_mark_required "${2:-}" ;;
88
+ is-required) arka_wf_is_required "${2:-}" && echo "true" || echo "false" ;;
89
+ clear) arka_wf_clear_required "${2:-}" ;;
90
+ *)
91
+ echo "Usage: $0 {classify <text>|mark <session_id>|is-required <session_id>|clear <session_id>}" >&2
92
+ exit 64
93
+ ;;
94
+ esac
95
+ fi
@@ -0,0 +1,117 @@
1
+ # ============================================================================
2
+ # ArkaOS v2 — PreToolUse Hook (Windows PowerShell)
3
+ #
4
+ # Parity with config/hooks/pre-tool-use.sh. Blocks Write/Edit/MultiEdit when
5
+ # the mandatory 13-phase flow is required for the session AND the assistant
6
+ # has not emitted a flow marker in its last 3 messages of the transcript.
7
+ #
8
+ # Delegates the decision to core/workflow/flow_enforcer.py.
9
+ #
10
+ # Exit 0 = allow (silent). Exit 2 = deny + structured hookSpecificOutput JSON.
11
+ # ============================================================================
12
+
13
+ $ErrorActionPreference = "SilentlyContinue"
14
+
15
+ # --- Read stdin JSON ---
16
+ $inputJson = [Console]::In.ReadToEnd()
17
+ if ([string]::IsNullOrWhiteSpace($inputJson)) { exit 0 }
18
+
19
+ try {
20
+ $inp = $inputJson | ConvertFrom-Json
21
+ } catch {
22
+ exit 0
23
+ }
24
+
25
+ $toolName = [string]$inp.tool_name
26
+ $transcriptPath = [string]$inp.transcript_path
27
+ $sessionId = [string]$inp.session_id
28
+ $cwd = [string]$inp.cwd
29
+
30
+ # --- Fast allow: not a gated tool ---
31
+ if ($toolName -ne "Write" -and $toolName -ne "Edit" -and $toolName -ne "MultiEdit") {
32
+ exit 0
33
+ }
34
+
35
+ # --- Resolve ARKAOS_ROOT ---
36
+ if ([string]::IsNullOrWhiteSpace($env:ARKAOS_ROOT)) {
37
+ $repoPathFile = Join-Path $HOME ".arkaos/.repo-path"
38
+ if (Test-Path $repoPathFile) {
39
+ $env:ARKAOS_ROOT = (Get-Content $repoPathFile -Raw).Trim()
40
+ } elseif (Test-Path (Join-Path $HOME ".arkaos")) {
41
+ $env:ARKAOS_ROOT = (Join-Path $HOME ".arkaos")
42
+ } else {
43
+ $env:ARKAOS_ROOT = if ($env:ARKA_OS) { $env:ARKA_OS } else { Join-Path $HOME ".claude/skills/arkaos" }
44
+ }
45
+ }
46
+
47
+ $enforcerPy = Join-Path $env:ARKAOS_ROOT "core/workflow/flow_enforcer.py"
48
+ if (-not (Test-Path $enforcerPy)) { exit 0 }
49
+
50
+ $python = Get-Command python3 -ErrorAction SilentlyContinue
51
+ if (-not $python) { $python = Get-Command python -ErrorAction SilentlyContinue }
52
+ if (-not $python) { exit 0 }
53
+
54
+ # --- Delegate to Python enforcer ---
55
+ $env:TOOL_NAME = $toolName
56
+ $env:TRANSCRIPT_PATH = $transcriptPath
57
+ $env:SESSION_ID = $sessionId
58
+ $env:CWD = $cwd
59
+
60
+ $pyScript = @'
61
+ import json
62
+ import os
63
+ import sys
64
+
65
+ sys.path.insert(0, os.environ["ARKAOS_ROOT"])
66
+ try:
67
+ from core.workflow.flow_enforcer import evaluate, record_telemetry
68
+ except Exception:
69
+ print(json.dumps({"allow": True, "reason": "enforcer-import-failed"}))
70
+ sys.exit(0)
71
+
72
+ decision = evaluate(
73
+ tool_name=os.environ.get("TOOL_NAME", ""),
74
+ transcript_path=os.environ.get("TRANSCRIPT_PATH", ""),
75
+ session_id=os.environ.get("SESSION_ID", ""),
76
+ cwd=os.environ.get("CWD", ""),
77
+ )
78
+ try:
79
+ record_telemetry(
80
+ session_id=os.environ.get("SESSION_ID", ""),
81
+ tool=os.environ.get("TOOL_NAME", ""),
82
+ decision=decision,
83
+ cwd=os.environ.get("CWD", ""),
84
+ )
85
+ except Exception:
86
+ pass
87
+ print(json.dumps({
88
+ "allow": decision.allow,
89
+ "reason": decision.reason,
90
+ "stderr_msg": decision.to_stderr_message(),
91
+ }))
92
+ '@
93
+
94
+ $decisionJson = $pyScript | & $python.Source -
95
+ if ([string]::IsNullOrWhiteSpace($decisionJson)) { exit 0 }
96
+
97
+ try {
98
+ $decision = $decisionJson | ConvertFrom-Json
99
+ } catch {
100
+ exit 0
101
+ }
102
+
103
+ if ($decision.allow) { exit 0 }
104
+
105
+ # --- Deny path ---
106
+ [Console]::Error.WriteLine($decision.stderr_msg)
107
+
108
+ $denyOut = @{
109
+ hookSpecificOutput = @{
110
+ hookEventName = "PreToolUse"
111
+ permissionDecision = "deny"
112
+ permissionDecisionReason = $decision.stderr_msg
113
+ }
114
+ } | ConvertTo-Json -Compress -Depth 5
115
+
116
+ Write-Output $denyOut
117
+ exit 2
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================================
3
+ # ArkaOS v2 — PreToolUse Hook (Flow Enforcement Gate)
4
+ #
5
+ # Blocks Write/Edit/MultiEdit when the mandatory 13-phase flow is required
6
+ # for the session AND the assistant has not emitted a flow marker
7
+ # (`[arka:routing]`, `[arka:trivial]`, or `[arka:phase:`) in its last
8
+ # 3 messages of the transcript.
9
+ #
10
+ # Delegates the decision to core/workflow/flow_enforcer.py (single source
11
+ # of truth, pytest-covered). This shell script is a thin wrapper — anti
12
+ # pattern `duplicated-security-logic` compliance.
13
+ #
14
+ # Timeout: 10s.
15
+ # Allow semantics: no stdout, exit 0.
16
+ # Deny semantics: hookSpecificOutput.permissionDecision=deny JSON on stdout,
17
+ # `[ARKA:ENFORCEMENT] ...` on stderr, exit 2.
18
+ # ============================================================================
19
+
20
+ input=$(cat)
21
+
22
+ # ─── Extract fields (docs: session_id, transcript_path, cwd, tool_name)
23
+ TOOL_NAME=""
24
+ TRANSCRIPT_PATH=""
25
+ SESSION_ID=""
26
+ CWD=""
27
+ if command -v jq &>/dev/null; then
28
+ TOOL_NAME=$(echo "$input" | jq -r '.tool_name // ""' 2>/dev/null)
29
+ TRANSCRIPT_PATH=$(echo "$input" | jq -r '.transcript_path // ""' 2>/dev/null)
30
+ SESSION_ID=$(echo "$input" | jq -r '.session_id // ""' 2>/dev/null)
31
+ CWD=$(echo "$input" | jq -r '.cwd // ""' 2>/dev/null)
32
+ fi
33
+
34
+ # ─── Fast allow: not a gated tool
35
+ case "$TOOL_NAME" in
36
+ Write|Edit|MultiEdit) ;;
37
+ *) exit 0 ;;
38
+ esac
39
+
40
+ # ─── Resolve ARKAOS_ROOT (same rules as user-prompt-submit.sh) ──────────
41
+ if [ -z "${ARKAOS_ROOT:-}" ]; then
42
+ if [ -f "$HOME/.arkaos/.repo-path" ]; then
43
+ ARKAOS_ROOT=$(cat "$HOME/.arkaos/.repo-path")
44
+ elif [ -d "$HOME/.arkaos" ]; then
45
+ ARKAOS_ROOT="$HOME/.arkaos"
46
+ else
47
+ ARKAOS_ROOT="${ARKA_OS:-$HOME/.claude/skills/arkaos}"
48
+ fi
49
+ fi
50
+
51
+ # ─── Degrade gracefully if Python or module is unavailable ──────────────
52
+ if ! command -v python3 &>/dev/null; then
53
+ exit 0
54
+ fi
55
+ if [ ! -f "$ARKAOS_ROOT/core/workflow/flow_enforcer.py" ]; then
56
+ exit 0
57
+ fi
58
+
59
+ # ─── Delegate to Python enforcer ────────────────────────────────────────
60
+ DECISION_JSON=$(TOOL_NAME="$TOOL_NAME" \
61
+ TRANSCRIPT_PATH="$TRANSCRIPT_PATH" \
62
+ SESSION_ID="$SESSION_ID" \
63
+ CWD="$CWD" \
64
+ ARKAOS_ROOT="$ARKAOS_ROOT" \
65
+ python3 - <<'PY' 2>/dev/null
66
+ import json
67
+ import os
68
+ import sys
69
+
70
+ sys.path.insert(0, os.environ["ARKAOS_ROOT"])
71
+ try:
72
+ from core.workflow.flow_enforcer import evaluate, record_telemetry
73
+ except Exception:
74
+ print(json.dumps({"allow": True, "reason": "enforcer-import-failed"}))
75
+ sys.exit(0)
76
+
77
+ decision = evaluate(
78
+ tool_name=os.environ.get("TOOL_NAME", ""),
79
+ transcript_path=os.environ.get("TRANSCRIPT_PATH", ""),
80
+ session_id=os.environ.get("SESSION_ID", ""),
81
+ cwd=os.environ.get("CWD", ""),
82
+ )
83
+ try:
84
+ record_telemetry(
85
+ session_id=os.environ.get("SESSION_ID", ""),
86
+ tool=os.environ.get("TOOL_NAME", ""),
87
+ decision=decision,
88
+ cwd=os.environ.get("CWD", ""),
89
+ )
90
+ except Exception:
91
+ pass
92
+ print(json.dumps({
93
+ "allow": decision.allow,
94
+ "reason": decision.reason,
95
+ "stderr_msg": decision.to_stderr_message(),
96
+ }))
97
+ PY
98
+ )
99
+
100
+ if [ -z "$DECISION_JSON" ]; then
101
+ exit 0
102
+ fi
103
+
104
+ ALLOW=$(echo "$DECISION_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('allow', True))" 2>/dev/null)
105
+
106
+ if [ "$ALLOW" = "True" ] || [ "$ALLOW" = "true" ]; then
107
+ exit 0
108
+ fi
109
+
110
+ # ─── Deny path: structured hookSpecificOutput + stderr fallback ─────────
111
+ REASON=$(echo "$DECISION_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('reason',''))" 2>/dev/null)
112
+ STDERR_MSG=$(echo "$DECISION_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('stderr_msg',''))" 2>/dev/null)
113
+
114
+ # Emit stderr (visible to the model per Claude Code hook spec) ─────────
115
+ echo "$STDERR_MSG" >&2
116
+
117
+ # Emit structured deny JSON (preferred path when runtime understands it).
118
+ # STDERR_MSG is passed via env var and read inside a single-quoted heredoc
119
+ # so no shell interpolation occurs inside the Python source — this closes
120
+ # the command-injection surface flagged by Francisca's tech review.
121
+ STDERR_MSG="$STDERR_MSG" python3 - <<'PY'
122
+ import json
123
+ import os
124
+
125
+ out = {
126
+ "hookSpecificOutput": {
127
+ "hookEventName": "PreToolUse",
128
+ "permissionDecision": "deny",
129
+ "permissionDecisionReason": os.environ.get("STDERR_MSG", ""),
130
+ }
131
+ }
132
+ print(json.dumps(out))
133
+ PY
134
+
135
+ exit 2
@@ -89,6 +89,17 @@ MSG+="ArkaOS v${VERSION} | 65 agents | 17 departments | 244+ skills"
89
89
  [ -n "$_FORGE_LINE" ] && MSG+="\\n${_FORGE_LINE}"
90
90
  MSG+="${DRIFT}"
91
91
 
92
+ # ─── Mandatory Flow Contract (top-of-session, highest priority) ─────────
93
+ MSG+="\\n\\n[ARKA:MANDATORY-FLOW] NON-NEGOTIABLE. Every non-trivial request runs the 13-phase canonical flow (constitution rule mandatory-flow; source arka/skills/flow/SKILL.md):"
94
+ MSG+="\\n 1.Input 2.Get-context 3.Route ([arka:routing] <dept> -> <lead>) 4.Call-hierarchy"
95
+ MSG+="\\n 5.Research (Obsidian+vector DB, cite or declare gap) 6.Call-team (Agent tool)"
96
+ MSG+="\\n 7.Plan with 6 parallel reviewers (positive / devils-advocate / Q&A / KB-research / best-solution / pessimistic)"
97
+ MSG+="\\n 8.Present+save (Obsidian + vector DB + ~/.arkaos/plans/) 9.Wait-approval (silence != approval)"
98
+ MSG+="\\n 10.TODO list 11.Per-todo: team-call -> complete -> QA -> Security -> Quality Gate (Marta+Eduardo+Francisca Opus) -> Document"
99
+ MSG+="\\n 12.Loop until TODO exhausted 13.Detailed summary"
100
+ MSG+="\\nEmit [arka:phase:N] before each step. Bypass ONLY via [arka:trivial] <reason> for single-file edits under 10 lines."
101
+ MSG+="\\nNo runtime setting, no user convenience, no task type can opt out. This contract overrides generic-assistant defaults."
102
+
92
103
  # --- Session Memory Resume Context ---
93
104
  if command -v python3 &>/dev/null && [ -n "$REPO" ]; then
94
105
  _SESSION_CTX=$(cd "$REPO" && python3 -c "
@@ -0,0 +1,112 @@
1
+ # ============================================================================
2
+ # ArkaOS v2 — Stop Hook (Windows PowerShell, WARN mode v1)
3
+ #
4
+ # Parity with config/hooks/stop.sh. Observes whether a flow-required session
5
+ # closed with [arka:phase:13] or [arka:trivial]. Logs to telemetry; never
6
+ # blocks in v1.
7
+ # ============================================================================
8
+
9
+ $ErrorActionPreference = "SilentlyContinue"
10
+
11
+ $inputJson = [Console]::In.ReadToEnd()
12
+ if ([string]::IsNullOrWhiteSpace($inputJson)) { exit 0 }
13
+
14
+ try {
15
+ $inp = $inputJson | ConvertFrom-Json
16
+ } catch {
17
+ exit 0
18
+ }
19
+
20
+ $sessionId = [string]$inp.session_id
21
+ $transcriptPath = [string]$inp.transcript_path
22
+ $stopHookActive = [string]$inp.stop_hook_active
23
+ $cwd = [string]$inp.cwd
24
+
25
+ if ($stopHookActive -eq "true") { exit 0 }
26
+
27
+ $wfMarker = Join-Path "/tmp/arkaos-wf-required" $sessionId
28
+ if ([string]::IsNullOrWhiteSpace($sessionId) -or -not (Test-Path $wfMarker)) {
29
+ exit 0
30
+ }
31
+
32
+ if ([string]::IsNullOrWhiteSpace($env:ARKAOS_ROOT)) {
33
+ $repoPathFile = Join-Path $HOME ".arkaos/.repo-path"
34
+ if (Test-Path $repoPathFile) {
35
+ $env:ARKAOS_ROOT = (Get-Content $repoPathFile -Raw).Trim()
36
+ } elseif (Test-Path (Join-Path $HOME ".arkaos")) {
37
+ $env:ARKAOS_ROOT = (Join-Path $HOME ".arkaos")
38
+ } else {
39
+ $env:ARKAOS_ROOT = if ($env:ARKA_OS) { $env:ARKA_OS } else { Join-Path $HOME ".claude/skills/arkaos" }
40
+ }
41
+ }
42
+
43
+ $enforcerPy = Join-Path $env:ARKAOS_ROOT "core/workflow/flow_enforcer.py"
44
+ if (-not (Test-Path $enforcerPy)) { exit 0 }
45
+
46
+ $python = Get-Command python3 -ErrorAction SilentlyContinue
47
+ if (-not $python) { $python = Get-Command python -ErrorAction SilentlyContinue }
48
+ if (-not $python) { exit 0 }
49
+
50
+ $env:SESSION_ID_VAL = $sessionId
51
+ $env:TRANSCRIPT_PATH_VAL = $transcriptPath
52
+ $env:CWD_VAL = $cwd
53
+
54
+ $pyScript = @'
55
+ import json
56
+ import os
57
+ import re
58
+ import sys
59
+ from datetime import datetime, timezone
60
+
61
+ sys.path.insert(0, os.environ["ARKAOS_ROOT"])
62
+ try:
63
+ from core.workflow.flow_enforcer import (
64
+ _load_last_assistant_messages,
65
+ TELEMETRY_PATH,
66
+ clear_flow_required,
67
+ )
68
+ except Exception:
69
+ sys.exit(0)
70
+
71
+ session_id = os.environ.get("SESSION_ID_VAL", "")
72
+ transcript_path = os.environ.get("TRANSCRIPT_PATH_VAL", "")
73
+ cwd = os.environ.get("CWD_VAL", "")
74
+
75
+ messages = _load_last_assistant_messages(transcript_path, n=1)
76
+ last = messages[-1] if messages else ""
77
+
78
+ phase13 = bool(re.search(r"\[arka:phase:13\]", last, re.IGNORECASE))
79
+ trivial = bool(re.search(r"\[arka:trivial\]", last, re.IGNORECASE))
80
+
81
+ entry = {
82
+ "ts": datetime.now(timezone.utc).isoformat(),
83
+ "session_id": session_id,
84
+ "cwd": cwd,
85
+ "event": "stop-hook-flow-check",
86
+ "closing_marker_found": phase13 or trivial,
87
+ "phase13": phase13,
88
+ "trivial": trivial,
89
+ "mode": "warn",
90
+ }
91
+
92
+ try:
93
+ TELEMETRY_PATH.parent.mkdir(parents=True, exist_ok=True)
94
+ with TELEMETRY_PATH.open("a", encoding="utf-8") as fh:
95
+ fh.write(json.dumps(entry) + "\n")
96
+ except Exception:
97
+ pass
98
+
99
+ try:
100
+ clear_flow_required(session_id)
101
+ except Exception:
102
+ pass
103
+ '@
104
+
105
+ $pyScript | & $python.Source - | Out-Null
106
+
107
+ # Belt-and-braces marker cleanup (safe even if the Python block crashed).
108
+ if ($sessionId -match '^[A-Za-z0-9._-]{1,128}$') {
109
+ Remove-Item -LiteralPath $wfMarker -ErrorAction SilentlyContinue
110
+ }
111
+
112
+ exit 0
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================================
3
+ # ArkaOS v2 — Stop Hook (Flow Completion Validator, WARN mode v1)
4
+ #
5
+ # When the classifier marked the session as flow-required, this hook checks
6
+ # whether the final assistant message contains [arka:phase:13] or
7
+ # [arka:trivial]. If absent, a structured warning is appended to
8
+ # ~/.arkaos/telemetry/enforcement.jsonl. The hook NEVER blocks in v1.
9
+ #
10
+ # Promotion to strict mode is planned for v2.21.0 after ≥ 2 weeks of clean
11
+ # telemetry. Until then, this hook is observation only.
12
+ #
13
+ # Timeout: 5s | Always exit 0.
14
+ # ============================================================================
15
+
16
+ input=$(cat)
17
+
18
+ SESSION_ID=""
19
+ TRANSCRIPT_PATH=""
20
+ STOP_HOOK_ACTIVE=""
21
+ CWD=""
22
+ if command -v jq &>/dev/null; then
23
+ SESSION_ID=$(echo "$input" | jq -r '.session_id // ""' 2>/dev/null)
24
+ TRANSCRIPT_PATH=$(echo "$input" | jq -r '.transcript_path // ""' 2>/dev/null)
25
+ STOP_HOOK_ACTIVE=$(echo "$input" | jq -r '.stop_hook_active // ""' 2>/dev/null)
26
+ CWD=$(echo "$input" | jq -r '.cwd // ""' 2>/dev/null)
27
+ fi
28
+
29
+ # Prevent infinite loops when Stop hook was triggered by its own decision.
30
+ if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
31
+ exit 0
32
+ fi
33
+
34
+ # Only evaluate sessions where the classifier flagged a creation intent.
35
+ WF_MARKER="/tmp/arkaos-wf-required/$SESSION_ID"
36
+ if [ -z "$SESSION_ID" ] || [ ! -f "$WF_MARKER" ]; then
37
+ exit 0
38
+ fi
39
+
40
+ # Resolve ARKAOS_ROOT
41
+ if [ -z "${ARKAOS_ROOT:-}" ]; then
42
+ if [ -f "$HOME/.arkaos/.repo-path" ]; then
43
+ ARKAOS_ROOT=$(cat "$HOME/.arkaos/.repo-path")
44
+ elif [ -d "$HOME/.arkaos" ]; then
45
+ ARKAOS_ROOT="$HOME/.arkaos"
46
+ else
47
+ ARKAOS_ROOT="${ARKA_OS:-$HOME/.claude/skills/arkaos}"
48
+ fi
49
+ fi
50
+
51
+ if ! command -v python3 &>/dev/null; then
52
+ exit 0
53
+ fi
54
+ if [ ! -f "$ARKAOS_ROOT/core/workflow/flow_enforcer.py" ]; then
55
+ exit 0
56
+ fi
57
+
58
+ # Reuse the last-messages reader to check for a closing phase marker.
59
+ SESSION_ID_VAL="$SESSION_ID" \
60
+ TRANSCRIPT_PATH_VAL="$TRANSCRIPT_PATH" \
61
+ CWD_VAL="$CWD" \
62
+ ARKAOS_ROOT_VAL="$ARKAOS_ROOT" \
63
+ python3 - <<'PY' 2>/dev/null
64
+ import json
65
+ import os
66
+ import re
67
+ import sys
68
+ from datetime import datetime, timezone
69
+ from pathlib import Path
70
+
71
+ sys.path.insert(0, os.environ["ARKAOS_ROOT_VAL"])
72
+ try:
73
+ from core.workflow.flow_enforcer import (
74
+ _load_last_assistant_messages,
75
+ TELEMETRY_PATH,
76
+ clear_flow_required,
77
+ )
78
+ except Exception:
79
+ sys.exit(0)
80
+
81
+ session_id = os.environ.get("SESSION_ID_VAL", "")
82
+ transcript_path = os.environ.get("TRANSCRIPT_PATH_VAL", "")
83
+ cwd = os.environ.get("CWD_VAL", "")
84
+
85
+ # Only inspect the very last assistant message for closing markers.
86
+ messages = _load_last_assistant_messages(transcript_path, n=1)
87
+ last = messages[-1] if messages else ""
88
+
89
+ phase13 = bool(re.search(r"\[arka:phase:13\]", last, re.IGNORECASE))
90
+ trivial = bool(re.search(r"\[arka:trivial\]", last, re.IGNORECASE))
91
+ closing_ok = phase13 or trivial
92
+
93
+ entry = {
94
+ "ts": datetime.now(timezone.utc).isoformat(),
95
+ "session_id": session_id,
96
+ "cwd": cwd,
97
+ "event": "stop-hook-flow-check",
98
+ "closing_marker_found": closing_ok,
99
+ "phase13": phase13,
100
+ "trivial": trivial,
101
+ "mode": "warn",
102
+ }
103
+
104
+ try:
105
+ TELEMETRY_PATH.parent.mkdir(parents=True, exist_ok=True)
106
+ with TELEMETRY_PATH.open("a", encoding="utf-8") as fh:
107
+ fh.write(json.dumps(entry) + "\n")
108
+ except Exception:
109
+ pass
110
+
111
+ # Clean up the session marker once Stop has evaluated.
112
+ try:
113
+ clear_flow_required(session_id)
114
+ except Exception:
115
+ pass
116
+ PY
117
+
118
+ # Belt-and-braces: remove the marker at shell level in case the Python
119
+ # block above crashed before reaching clear_flow_required(). Session_id
120
+ # is already validated by the Python helper; this shell remove is scoped
121
+ # to the exact marker path and is idempotent.
122
+ case "$SESSION_ID" in
123
+ *[!A-Za-z0-9._-]*|"") ;; # reject unsafe/empty session ids
124
+ *) rm -f "$WF_MARKER" 2>/dev/null ;;
125
+ esac
126
+
127
+ exit 0
@@ -83,14 +83,23 @@ mkdir -p "$CACHE_DIR" 2>/dev/null
83
83
 
84
84
  # ─── Extract user input from hook JSON ───────────────────────────────────
85
85
  user_input=""
86
+ SESSION_ID=""
86
87
  if command -v jq &>/dev/null; then
87
88
  user_input=$(echo "$input" | jq -r '.userInput // .message // ""' 2>/dev/null)
89
+ SESSION_ID=$(echo "$input" | jq -r '.session_id // ""' 2>/dev/null)
88
90
  fi
89
91
  # Fallback: try to get the raw text
90
92
  if [ -z "$user_input" ]; then
91
93
  user_input=$(echo "$input" | head -c 2000)
92
94
  fi
93
95
 
96
+ # ─── Load shared workflow classifier ─────────────────────────────────────
97
+ _CLASSIFIER_LIB="$(dirname "$0")/_lib/workflow-classifier.sh"
98
+ if [ -f "$_CLASSIFIER_LIB" ]; then
99
+ # shellcheck disable=SC1090
100
+ . "$_CLASSIFIER_LIB"
101
+ fi
102
+
94
103
  # ─── Try Python Synapse bridge first ────────────────────────────────────
95
104
  python_result=""
96
105
  BRIDGE_SCRIPT="${ARKAOS_ROOT}/scripts/synapse-bridge.py"
@@ -276,35 +285,50 @@ When [knowledge:N chunks] is present, cite at least one source.
276
285
  If [knowledge:N chunks] is absent on a non-trivial ArkaOS topic, query Obsidian first."
277
286
 
278
287
  # ─── Workflow Classifier (hard enforcement for creation/implementation) ──
279
- # Classifies the user prompt. If it looks like a creation/implementation/
280
- # modification request that is NOT already routed with an explicit /prefix,
281
- # emits a directive that the agent MUST acknowledge with [arka:routing]
282
- # BEFORE using any write tool. Trivial quick questions pass through
283
- # untouched. Explicit slash commands pass through untouched.
288
+ # Uses the shared _lib/workflow-classifier.sh. When a creation/implementation
289
+ # verb is detected, the session is marked as flow-required so PreToolUse
290
+ # can block Write/Edit/MultiEdit until the agent emits [arka:routing] or
291
+ # [arka:trivial]. Explicit slash commands and bang shells pass through.
284
292
  _WORKFLOW_DIRECTIVE=""
285
- if [ -n "$user_input" ]; then
286
- # Skip: explicit slash command (already routed)
287
- _FIRST_CHAR=$(echo "$user_input" | head -c 1)
288
- if [ "$_FIRST_CHAR" != "/" ] && [ "$_FIRST_CHAR" != "!" ]; then
289
- # Match creation/implementation verbs in EN and PT (case-insensitive).
290
- _VERB_PATTERN='(criar?|crie[ms]?|cria[mr]?|adicionar?|adiciona[mr]?|implementar?|implementa[mr]?|desenvolver?|desenvolve[mr]?|construir?|constru[ií]a?[mr]?|fazer?|faz[ae][mr]?|refactor(izar?)?|corrigir?|corrige[mr]?|consertar?|conserta[mr]?|create[sd]?|creating|build(s|ing)?|add(s|ed|ing)?|implement(s|ed|ing)?|develop(s|ed|ing)?|fix(es|ed|ing)?|refactor(s|ed|ing)?|make[sd]?|making)'
291
- _NOUN_PATTERN='(feature|funcionalidade|skill|squad|agent[e]?|workflow|endpoint|api|component[e]?|module|m[oó]dulo|page|p[aá]gina|hook|pipeline|integration|integra[cç][aã]o|dashboard|report|report[eó]|script|test[es]?)'
292
- if echo "$user_input" | grep -qiE "\b${_VERB_PATTERN}\b"; then
293
- _WORKFLOW_DIRECTIVE="
294
- [ARKA:WORKFLOW-REQUIRED] Your user request matched a CREATION/IMPLEMENTATION pattern.
295
- You MUST, before using any Write, Edit, Bash with side-effects, or Agent tool:
296
- 1. Output on the first line: [arka:routing] <department-slug> -> <lead-agent>
297
- (e.g. [arka:routing] dev -> Paulo, [arka:routing] brand -> Valentina,
298
- [arka:routing] kb -> Clara, [arka:routing] mkt -> Luna)
299
- 2. State the workflow name and phase count in one short sentence.
300
- 3. Begin phase 1 (spec or plan) BEFORE any code is written.
301
- 4. Run the Quality Gate (Marta + Eduardo + Francisca, Opus) before claiming done.
302
- Trivial override: if the request is a single-file edit under 10 lines AND the user
303
- used an imperative like 'rename X', 'fix typo', you MAY emit [arka:trivial] <reason>
304
- and proceed directly. Anything else requires routing. This is enforced, not advisory.
305
- Skipping routing violates constitution rules squad-routing, arka-supremacy,
306
- spec-driven, mandatory-qa, sequential-validation."
293
+ if [ -n "$user_input" ] && command -v arka_wf_classify &>/dev/null; then
294
+ if [ "$(arka_wf_classify "$user_input")" = "true" ]; then
295
+ # Mark session as flow-required (consumed by pre-tool-use.sh and stop.sh)
296
+ if command -v arka_wf_mark_required &>/dev/null; then
297
+ arka_wf_mark_required "$SESSION_ID"
307
298
  fi
299
+ _WORKFLOW_DIRECTIVE="
300
+ [ARKA:WORKFLOW-REQUIRED] Your user request matched a CREATION/IMPLEMENTATION pattern.
301
+ The ArkaOS mandatory 13-phase flow applies. It is NON-NEGOTIABLE (constitution rule
302
+ mandatory-flow). You MUST walk every phase, in order, emitting a [arka:phase:N] tag
303
+ before each:
304
+ 1. Input — restate the request verbatim.
305
+ 2. Get context — profile, repo, git, cwd tag, session digests.
306
+ 3. Decide route — emit [arka:routing] <dept> -> <lead>.
307
+ 4. Call hierarchy — escalate to Tier 0 if strategic/cross-dept/security/financial.
308
+ 5. Research — query Obsidian + vector DB, cite sources or declare the gap.
309
+ 6. Call team — dispatch specialists via Agent tool.
310
+ 7. Plan — run six parallel reviewers: positive, devil's advocate, Q&A, KB research,
311
+ best-solution validator, pessimistic. Synthesise into a spec.
312
+ 8. Present plan — save to Obsidian + vector DB + ~/.arkaos/plans/, print inline.
313
+ 9. Wait approval — EXPLICIT user go. Silence is NOT approval.
314
+ 10. TODO list — atomic, ordered, independently verifiable.
315
+ 11. Per-todo loop — team call -> complete -> QA (all tests, E2E, Playwright) ->
316
+ Security review -> Quality Gate (Marta + Eduardo + Francisca, Opus) -> Document.
317
+ Each step loops back on fail. No compound gates.
318
+ 12. Loop until TODO is exhausted.
319
+ 13. Detailed summary — what was done, where, how to verify, what is still open.
320
+
321
+ No Write, Edit, Bash-with-side-effects, or Agent dispatch before Phase 7 completes
322
+ for the affected item. No advancing a todo until QA AND Security AND Quality Gate
323
+ all pass for it. Phase 5 and Phase 8 require Obsidian/KB writes, not just reads.
324
+
325
+ Trivial override: single-file edit under 10 lines with imperative verb
326
+ (rename X, fix typo in Y). Emit [arka:trivial] <reason> as first line and proceed.
327
+ Anything else runs the full 13 phases. Source: arka/skills/flow/SKILL.md.
328
+
329
+ This is enforced by the hook and the session-start systemMessage, not by convention.
330
+ Skipping violates: mandatory-flow, squad-routing, spec-driven, mandatory-qa,
331
+ sequential-validation, full-visibility, arka-supremacy."
308
332
  fi
309
333
  fi
310
334
 
@@ -0,0 +1,272 @@
1
+ """Mandatory 13-phase flow enforcement for write-mutation tools.
2
+
3
+ Invoked by the Claude Code `PreToolUse` hook. Decides whether a `Write`,
4
+ `Edit`, or `MultiEdit` tool call may proceed, based on markers observed
5
+ in the last N assistant messages of the session transcript.
6
+
7
+ Design contract:
8
+ - Stateless transcript parse (no /tmp state for decisions).
9
+ - Side effects limited to reading the transcript path supplied by the hook.
10
+ - Signals permission when the assistant has emitted one of the flow markers:
11
+ `[arka:routing]`, `[arka:trivial]`, or `[arka:phase:`.
12
+ - Respects `ARKA_BYPASS_FLOW=1` env var (installer/`/arka update` internal).
13
+ - Honors feature flag `hooks.hardEnforcement` in `~/.arkaos/config.json`.
14
+ - Gated tool list is closed: anything outside it is always allowed.
15
+ """
16
+
17
+ import json
18
+ import os
19
+ import re
20
+ from contextlib import contextmanager
21
+ from dataclasses import asdict, dataclass
22
+ from datetime import datetime, timezone
23
+ from pathlib import Path
24
+
25
+ try:
26
+ import fcntl # POSIX only
27
+ _HAS_FLOCK = True
28
+ except ImportError:
29
+ _HAS_FLOCK = False
30
+
31
+
32
+ @contextmanager
33
+ def _locked_append(path: Path):
34
+ """Append to `path` under an exclusive advisory lock (POSIX flock).
35
+
36
+ On Windows or any platform without fcntl, falls back to a plain append
37
+ (single-process writers remain safe; cross-process interleaving is
38
+ mitigated by `O_APPEND` atomicity for writes up to PIPE_BUF).
39
+ """
40
+ path.parent.mkdir(parents=True, exist_ok=True)
41
+ fh = path.open("a", encoding="utf-8")
42
+ try:
43
+ if _HAS_FLOCK:
44
+ fcntl.flock(fh.fileno(), fcntl.LOCK_EX)
45
+ yield fh
46
+ finally:
47
+ if _HAS_FLOCK:
48
+ try:
49
+ fcntl.flock(fh.fileno(), fcntl.LOCK_UN)
50
+ except OSError:
51
+ pass
52
+ fh.close()
53
+
54
+ GATED_TOOLS: frozenset[str] = frozenset({"Write", "Edit", "MultiEdit"})
55
+
56
+ ROUTING_RE = re.compile(r"\[arka:routing\]\s*[\w-]+\s*->\s*\w+", re.IGNORECASE)
57
+ TRIVIAL_RE = re.compile(r"\[arka:trivial\]\s*\S+", re.IGNORECASE)
58
+ PHASE_RE = re.compile(r"\[arka:phase:\d+\]", re.IGNORECASE)
59
+ SAFE_SESSION_ID_RE = re.compile(r"^[A-Za-z0-9._-]{1,128}$")
60
+
61
+ ASSISTANT_WINDOW = 3
62
+ CONFIG_PATH = Path.home() / ".arkaos" / "config.json"
63
+ BYPASS_AUDIT_PATH = Path.home() / ".arkaos" / "audit" / "bypass.log"
64
+ TELEMETRY_PATH = Path.home() / ".arkaos" / "telemetry" / "enforcement.jsonl"
65
+ FLOW_REQUIRED_DIR = Path("/tmp/arkaos-wf-required")
66
+
67
+
68
+ def _safe_session_id(session_id: str) -> str | None:
69
+ """Validate session_id against a strict allowlist (prevents path traversal).
70
+
71
+ Returns the id if safe, or None if it contains path separators, dots-dots,
72
+ or characters outside `[A-Za-z0-9._-]`. Callers MUST treat None as reject.
73
+ """
74
+ if not session_id or not isinstance(session_id, str):
75
+ return None
76
+ if not SAFE_SESSION_ID_RE.match(session_id):
77
+ return None
78
+ return session_id
79
+
80
+
81
+ @dataclass
82
+ class Decision:
83
+ """Outcome of enforcement evaluation for a single tool call."""
84
+
85
+ allow: bool
86
+ reason: str
87
+ marker_found: str | None = None
88
+ phase_observed: str | None = None
89
+ bypass_used: bool = False
90
+
91
+ def to_stderr_message(self) -> str:
92
+ if self.allow:
93
+ return ""
94
+ return (
95
+ f"[ARKA:ENFORCEMENT] Flow marker missing. "
96
+ f"Emit `[arka:routing] <dept> -> <lead>` or `[arka:trivial] <reason>` "
97
+ f"before any Write/Edit/MultiEdit. Reason: {self.reason}"
98
+ )
99
+
100
+
101
+ def _feature_flag_on() -> bool:
102
+ if not CONFIG_PATH.exists():
103
+ return False
104
+ try:
105
+ data = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
106
+ except (json.JSONDecodeError, OSError):
107
+ return False
108
+ return bool(data.get("hooks", {}).get("hardEnforcement", False))
109
+
110
+
111
+ def _bypass_env_active() -> bool:
112
+ return os.environ.get("ARKA_BYPASS_FLOW", "").strip() == "1"
113
+
114
+
115
+ def _audit_bypass(session_id: str, tool: str, cwd: str) -> None:
116
+ entry = {
117
+ "ts": datetime.now(timezone.utc).isoformat(),
118
+ "session_id": session_id,
119
+ "tool": tool,
120
+ "cwd": cwd,
121
+ "reason": os.environ.get("ARKA_BYPASS_REASON", ""),
122
+ }
123
+ with _locked_append(BYPASS_AUDIT_PATH) as fh:
124
+ fh.write(json.dumps(entry) + "\n")
125
+
126
+
127
+ def record_telemetry(
128
+ session_id: str, tool: str, decision: Decision, cwd: str
129
+ ) -> None:
130
+ """Append a structured record to the enforcement telemetry log."""
131
+ entry = {
132
+ "ts": datetime.now(timezone.utc).isoformat(),
133
+ "session_id": session_id,
134
+ "tool": tool,
135
+ "cwd": cwd,
136
+ **asdict(decision),
137
+ }
138
+ with _locked_append(TELEMETRY_PATH) as fh:
139
+ fh.write(json.dumps(entry) + "\n")
140
+
141
+
142
+ def _flow_required_for_session(session_id: str) -> bool:
143
+ """Check whether the UserPromptSubmit classifier flagged this session."""
144
+ safe = _safe_session_id(session_id)
145
+ if safe is None:
146
+ return False
147
+ marker = FLOW_REQUIRED_DIR / safe
148
+ return marker.exists()
149
+
150
+
151
+ def _extract_text(content: object) -> str:
152
+ """Flatten Claude transcript message content into a single string."""
153
+ if isinstance(content, str):
154
+ return content
155
+ if isinstance(content, list):
156
+ parts: list[str] = []
157
+ for item in content:
158
+ if isinstance(item, dict):
159
+ if "text" in item:
160
+ parts.append(str(item["text"]))
161
+ elif item.get("type") == "tool_use":
162
+ parts.append(f"<tool_use:{item.get('name', '')}>")
163
+ elif isinstance(item, str):
164
+ parts.append(item)
165
+ return "\n".join(parts)
166
+ return ""
167
+
168
+
169
+ def _load_last_assistant_messages(transcript_path: str, n: int) -> list[str]:
170
+ """Read the last `n` assistant messages from a JSONL transcript."""
171
+ path = Path(transcript_path) if transcript_path else None
172
+ if path is None or not path.exists():
173
+ return []
174
+ messages: list[str] = []
175
+ for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
176
+ if not line.strip():
177
+ continue
178
+ try:
179
+ record = json.loads(line)
180
+ except json.JSONDecodeError:
181
+ continue
182
+ role = record.get("role") or record.get("message", {}).get("role")
183
+ if role != "assistant":
184
+ continue
185
+ content = record.get("content")
186
+ if content is None:
187
+ content = record.get("message", {}).get("content")
188
+ text = _extract_text(content)
189
+ if text:
190
+ messages.append(text)
191
+ return messages[-n:]
192
+
193
+
194
+ def _scan_markers(messages: list[str]) -> tuple[str | None, str | None]:
195
+ """Return (marker_found, phase_observed) across the given messages."""
196
+ marker_found: str | None = None
197
+ phase_observed: str | None = None
198
+ for text in messages:
199
+ if phase_observed is None:
200
+ phase_match = PHASE_RE.search(text)
201
+ if phase_match:
202
+ phase_observed = phase_match.group(0)
203
+ if marker_found is None:
204
+ if ROUTING_RE.search(text):
205
+ marker_found = "routing"
206
+ elif TRIVIAL_RE.search(text):
207
+ marker_found = "trivial"
208
+ elif PHASE_RE.search(text):
209
+ marker_found = "phase"
210
+ return marker_found, phase_observed
211
+
212
+
213
+ def evaluate(
214
+ tool_name: str,
215
+ transcript_path: str,
216
+ session_id: str = "",
217
+ cwd: str = "",
218
+ ) -> Decision:
219
+ """Decide whether a tool call may proceed.
220
+
221
+ Returns a Decision. Caller is responsible for translating `allow=False`
222
+ into the appropriate hook exit code or permissionDecision output.
223
+ """
224
+ if tool_name not in GATED_TOOLS:
225
+ return Decision(allow=True, reason="tool-not-gated")
226
+
227
+ if not _feature_flag_on():
228
+ return Decision(allow=True, reason="feature-flag-off")
229
+
230
+ if _bypass_env_active():
231
+ _audit_bypass(session_id, tool_name, cwd)
232
+ return Decision(allow=True, reason="env-bypass", bypass_used=True)
233
+
234
+ if not _flow_required_for_session(session_id):
235
+ return Decision(allow=True, reason="classifier-did-not-match")
236
+
237
+ messages = _load_last_assistant_messages(transcript_path, ASSISTANT_WINDOW)
238
+ marker_found, phase_observed = _scan_markers(messages)
239
+
240
+ if marker_found is None:
241
+ return Decision(
242
+ allow=False,
243
+ reason="no-flow-marker-in-last-3-assistant-messages",
244
+ phase_observed=phase_observed,
245
+ )
246
+
247
+ return Decision(
248
+ allow=True,
249
+ reason=f"marker-found:{marker_found}",
250
+ marker_found=marker_found,
251
+ phase_observed=phase_observed,
252
+ )
253
+
254
+
255
+ def mark_flow_required(session_id: str) -> None:
256
+ """Invoked by UserPromptSubmit when classifier matches creation intent."""
257
+ safe = _safe_session_id(session_id)
258
+ if safe is None:
259
+ return
260
+ FLOW_REQUIRED_DIR.mkdir(parents=True, exist_ok=True)
261
+ marker = FLOW_REQUIRED_DIR / safe
262
+ marker.write_text(datetime.now(timezone.utc).isoformat(), encoding="utf-8")
263
+
264
+
265
+ def clear_flow_required(session_id: str) -> None:
266
+ """Clear the flow-required marker (end of session / rollout tooling)."""
267
+ safe = _safe_session_id(session_id)
268
+ if safe is None:
269
+ return
270
+ marker = FLOW_REQUIRED_DIR / safe
271
+ if marker.exists():
272
+ marker.unlink()
@@ -94,6 +94,19 @@ export default {
94
94
  { hooks: [hookEntry(hooksDir, "post-tool-use", 5)] },
95
95
  ];
96
96
 
97
+ // PreToolUse — Flow enforcement gate (gated by hooks.hardEnforcement
98
+ // feature flag in ~/.arkaos/config.json; no-op when flag is false).
99
+ settings.hooks.PreToolUse = [
100
+ { hooks: [hookEntry(hooksDir, "pre-tool-use", 10)] },
101
+ ];
102
+
103
+ // Stop — Flow completion validator (WARN mode in v2.20.0; promotion
104
+ // to STRICT mode is gated on ≥ 2 weeks of clean telemetry per ADR
105
+ // 2026-04-17-binding-flow-enforcement).
106
+ settings.hooks.Stop = [
107
+ { hooks: [hookEntry(hooksDir, "stop", 5)] },
108
+ ];
109
+
97
110
  // PreCompact — Session digest
98
111
  settings.hooks.PreCompact = [
99
112
  { hooks: [hookEntry(hooksDir, "pre-compact", 30)] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "2.19.2",
3
+ "version": "2.20.0-beta.1",
4
4
  "description": "The Operating System for AI Agent Teams",
5
5
  "type": "module",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arkaos-core"
3
- version = "2.19.2"
3
+ version = "2.20.0-beta.1"
4
4
  description = "Core engine for ArkaOS — The Operating System for AI Agent Teams"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}