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 +1 -1
- package/arka/SKILL.md +38 -3
- package/arka/skills/flow/SKILL.md +160 -0
- package/config/constitution.yaml +18 -0
- package/config/hooks/_lib/workflow-classifier.sh +95 -0
- package/config/hooks/pre-tool-use.ps1 +117 -0
- package/config/hooks/pre-tool-use.sh +135 -0
- package/config/hooks/session-start.sh +11 -0
- package/config/hooks/stop.ps1 +112 -0
- package/config/hooks/stop.sh +127 -0
- package/config/hooks/user-prompt-submit.sh +51 -27
- package/core/workflow/__pycache__/flow_enforcer.cpython-313.pyc +0 -0
- package/core/workflow/flow_enforcer.py +272 -0
- package/installer/adapters/claude-code.js +13 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
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
|
-
## ⛔
|
|
15
|
+
## ⛔ Mandatory 13-phase flow (NON-NEGOTIABLE)
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
`
|
|
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.
|
package/config/constitution.yaml
CHANGED
|
@@ -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
|
-
#
|
|
280
|
-
#
|
|
281
|
-
#
|
|
282
|
-
#
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
|
|
Binary file
|
|
@@ -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