arkaos 2.12.0 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/VERSION +1 -1
- package/arka/skills/forge/SKILL.md +649 -0
- package/config/constitution.yaml +8 -0
- package/config/hooks/post-tool-use.sh +108 -2
- package/config/hooks/session-start.sh +38 -0
- package/config/hooks/user-prompt-submit.sh +51 -1
- package/core/forge/__init__.py +104 -0
- package/core/forge/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/complexity.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/handoff.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/persistence.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/renderer.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/schema.cpython-313.pyc +0 -0
- package/core/forge/complexity.py +125 -0
- package/core/forge/handoff.py +100 -0
- package/core/forge/persistence.py +308 -0
- package/core/forge/renderer.py +261 -0
- package/core/forge/schema.py +213 -0
- package/core/synapse/__init__.py +2 -2
- package/core/synapse/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/layers.cpython-313.pyc +0 -0
- package/core/synapse/engine.py +4 -2
- package/core/synapse/layers.py +49 -0
- package/core/workflow/__init__.py +14 -1
- package/core/workflow/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/state.cpython-313.pyc +0 -0
- package/core/workflow/state.py +128 -0
- package/core/workflow/state_reader.sh +92 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
|
@@ -170,6 +170,108 @@ fi
|
|
|
170
170
|
|
|
171
171
|
) 200>"$LOCK_FILE"
|
|
172
172
|
|
|
173
|
+
# ─── Workflow Violation Detection ────────────────────────────────────────
|
|
174
|
+
VIOLATION_MSG=""
|
|
175
|
+
STATE_READER=""
|
|
176
|
+
[ -f "$HOME/.arkaos/.repo-path" ] && STATE_READER="$(cat "$HOME/.arkaos/.repo-path")/core/workflow/state_reader.sh"
|
|
177
|
+
|
|
178
|
+
if [ -n "$STATE_READER" ] && [ -f "$STATE_READER" ] && bash "$STATE_READER" active 2>/dev/null; then
|
|
179
|
+
ARKAOS_PY=""
|
|
180
|
+
[ -f "$HOME/.arkaos/venv/bin/python3" ] && ARKAOS_PY="$HOME/.arkaos/venv/bin/python3"
|
|
181
|
+
[ -z "$ARKAOS_PY" ] && [ -f "$HOME/.arkaos/.venv/bin/python3" ] && ARKAOS_PY="$HOME/.arkaos/.venv/bin/python3"
|
|
182
|
+
[ -z "$ARKAOS_PY" ] && ARKAOS_PY=$(command -v python3 2>/dev/null)
|
|
183
|
+
ARKAOS_ROOT=$(cat "$HOME/.arkaos/.repo-path" 2>/dev/null)
|
|
184
|
+
|
|
185
|
+
# Rule 1: Branch isolation — commit on master while workflow active
|
|
186
|
+
if [ "$TOOL_NAME" = "Bash" ]; then
|
|
187
|
+
if echo "$TOOL_OUTPUT" | grep -qE '^\[(master|main)' 2>/dev/null; then
|
|
188
|
+
CMD_TEXT=$(echo "$input" | jq -r '.command // ""' 2>/dev/null)
|
|
189
|
+
if echo "$CMD_TEXT" | grep -qE 'git commit'; then
|
|
190
|
+
[ -n "$ARKAOS_PY" ] && [ -n "$ARKAOS_ROOT" ] && \
|
|
191
|
+
PYTHONPATH="$ARKAOS_ROOT" $ARKAOS_PY -c "
|
|
192
|
+
from core.workflow.state import add_violation
|
|
193
|
+
add_violation('branch-isolation', 'Commit on master/main while workflow active', 'Bash')
|
|
194
|
+
" 2>/dev/null
|
|
195
|
+
VIOLATION_MSG="VIOLATION [branch-isolation]: Commit on master while workflow active. Use a feature branch."
|
|
196
|
+
fi
|
|
197
|
+
fi
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
# Rule 2: Spec-driven — code edited without completed spec
|
|
201
|
+
if [ "$TOOL_NAME" = "Write" ] || [ "$TOOL_NAME" = "Edit" ]; then
|
|
202
|
+
FILE_PATH=$(echo "$input" | jq -r '.file_path // ""' 2>/dev/null)
|
|
203
|
+
if echo "$FILE_PATH" | grep -qE '\.(py|js|ts|vue|php|jsx|tsx)$'; then
|
|
204
|
+
if ! bash "$STATE_READER" check spec 2>/dev/null; then
|
|
205
|
+
[ -n "$ARKAOS_PY" ] && [ -n "$ARKAOS_ROOT" ] && \
|
|
206
|
+
PYTHONPATH="$ARKAOS_ROOT" _V_TOOL="$TOOL_NAME" _V_FILE="$FILE_PATH" $ARKAOS_PY -c "
|
|
207
|
+
import os; from core.workflow.state import add_violation
|
|
208
|
+
add_violation('spec-driven', 'Code edited without completed spec', os.environ['_V_TOOL'], os.environ['_V_FILE'])
|
|
209
|
+
" 2>/dev/null
|
|
210
|
+
VIOLATION_MSG="VIOLATION [spec-driven]: Code edited without completed spec ($FILE_PATH). Complete the spec phase first."
|
|
211
|
+
fi
|
|
212
|
+
fi
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
# Rule 3: Sequential — implementation before planning
|
|
216
|
+
if [ -z "$VIOLATION_MSG" ] && { [ "$TOOL_NAME" = "Write" ] || [ "$TOOL_NAME" = "Edit" ]; }; then
|
|
217
|
+
FILE_PATH=$(echo "$input" | jq -r '.file_path // ""' 2>/dev/null)
|
|
218
|
+
if echo "$FILE_PATH" | grep -qE '\.(py|js|ts|vue|php|jsx|tsx)$'; then
|
|
219
|
+
IMPL_STATUS=$(bash "$STATE_READER" phase implementation 2>/dev/null)
|
|
220
|
+
if [ "$IMPL_STATUS" = "pending" ]; then
|
|
221
|
+
[ -n "$ARKAOS_PY" ] && [ -n "$ARKAOS_ROOT" ] && \
|
|
222
|
+
PYTHONPATH="$ARKAOS_ROOT" _V_TOOL="$TOOL_NAME" _V_FILE="$FILE_PATH" $ARKAOS_PY -c "
|
|
223
|
+
import os; from core.workflow.state import add_violation
|
|
224
|
+
add_violation('sequential-validation', 'Code written before implementation phase started', os.environ['_V_TOOL'], os.environ['_V_FILE'])
|
|
225
|
+
" 2>/dev/null
|
|
226
|
+
VIOLATION_MSG="VIOLATION [sequential-validation]: Implementation started before planning completed ($FILE_PATH)."
|
|
227
|
+
fi
|
|
228
|
+
fi
|
|
229
|
+
fi
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
# --- Forge Violation Detection ---
|
|
233
|
+
_FORGE_ACTIVE="$HOME/.arkaos/plans/active.yaml"
|
|
234
|
+
|
|
235
|
+
# Ensure ARKAOS_PY and ARKAOS_ROOT are set (may not be set if no active workflow)
|
|
236
|
+
if [ -z "${ARKAOS_PY:-}" ]; then
|
|
237
|
+
[ -f "$HOME/.arkaos/venv/bin/python3" ] && ARKAOS_PY="$HOME/.arkaos/venv/bin/python3"
|
|
238
|
+
[ -z "${ARKAOS_PY:-}" ] && [ -f "$HOME/.arkaos/.venv/bin/python3" ] && ARKAOS_PY="$HOME/.arkaos/.venv/bin/python3"
|
|
239
|
+
[ -z "${ARKAOS_PY:-}" ] && ARKAOS_PY=$(command -v python3 2>/dev/null)
|
|
240
|
+
fi
|
|
241
|
+
[ -z "${ARKAOS_ROOT:-}" ] && ARKAOS_ROOT=$(cat "$HOME/.arkaos/.repo-path" 2>/dev/null)
|
|
242
|
+
|
|
243
|
+
if [ -z "$VIOLATION_MSG" ] && [ -f "$_FORGE_ACTIVE" ] && [ -n "$ARKAOS_PY" ] && [ -n "$ARKAOS_ROOT" ]; then
|
|
244
|
+
_FORGE_ID=$(cat "$_FORGE_ACTIVE" 2>/dev/null)
|
|
245
|
+
_FORGE_FILE="$HOME/.arkaos/plans/${_FORGE_ID}.yaml"
|
|
246
|
+
|
|
247
|
+
if [ -f "$_FORGE_FILE" ]; then
|
|
248
|
+
if [ "$TOOL_NAME" = "Edit" ] || [ "$TOOL_NAME" = "Write" ]; then
|
|
249
|
+
_EDITED_FILE="${tool_input_file_path:-}"
|
|
250
|
+
# Fallback: extract file_path from input JSON
|
|
251
|
+
[ -z "$_EDITED_FILE" ] && _EDITED_FILE=$(echo "$input" | jq -r '.file_path // ""' 2>/dev/null)
|
|
252
|
+
if [ -n "$_EDITED_FILE" ]; then
|
|
253
|
+
_FORGE_VIOLATION=$(FORGE_FILE="$_FORGE_FILE" EDITED_FILE="$_EDITED_FILE" PYTHONPATH="$ARKAOS_ROOT" $ARKAOS_PY -c "
|
|
254
|
+
import yaml, sys, os
|
|
255
|
+
plan = yaml.safe_load(open(os.environ['FORGE_FILE']))
|
|
256
|
+
if plan.get('status', '') != 'executing':
|
|
257
|
+
sys.exit(0)
|
|
258
|
+
phases = plan.get('plan_phases', [])
|
|
259
|
+
all_deliverables = []
|
|
260
|
+
for p in phases:
|
|
261
|
+
all_deliverables.extend(p.get('deliverables', []))
|
|
262
|
+
edited = os.environ['EDITED_FILE']
|
|
263
|
+
match = any(d in edited or edited.endswith(d) for d in all_deliverables)
|
|
264
|
+
if not match and all_deliverables:
|
|
265
|
+
print('forge-scope-creep')
|
|
266
|
+
" 2>/dev/null)
|
|
267
|
+
if [ "$_FORGE_VIOLATION" = "forge-scope-creep" ]; then
|
|
268
|
+
VIOLATION_MSG="⚠ Forge scope-creep: editing ${_EDITED_FILE} which is outside forge plan deliverables."
|
|
269
|
+
fi
|
|
270
|
+
fi
|
|
271
|
+
fi
|
|
272
|
+
fi
|
|
273
|
+
fi
|
|
274
|
+
|
|
173
275
|
# ─── Log Metrics ─────────────────────────────────────────────────────────
|
|
174
276
|
_DURATION_MS=$(_hook_ms)
|
|
175
277
|
METRICS_FILE="$HOME/.arkaos/hook-metrics.json"
|
|
@@ -184,5 +286,9 @@ mkdir -p "$HOME/.arkaos"
|
|
|
184
286
|
"$METRICS_FILE" > "$METRICS_FILE.tmp" 2>/dev/null && mv "$METRICS_FILE.tmp" "$METRICS_FILE"
|
|
185
287
|
) 200>"$METRICS_LOCK" 2>/dev/null
|
|
186
288
|
|
|
187
|
-
#
|
|
188
|
-
|
|
289
|
+
# Output violation as context if detected, otherwise empty
|
|
290
|
+
if [ -n "$VIOLATION_MSG" ]; then
|
|
291
|
+
echo "{\"additionalContext\": \"$VIOLATION_MSG\"}"
|
|
292
|
+
else
|
|
293
|
+
echo '{}'
|
|
294
|
+
fi
|
|
@@ -49,7 +49,45 @@ MSG+="║ ║\\n"
|
|
|
49
49
|
MSG+="╚══════════════════════════════════════════════╝\\n"
|
|
50
50
|
MSG+="\\n"
|
|
51
51
|
MSG+="${GREETING}, ${NAME} (${COMPANY})\\n"
|
|
52
|
+
# ─── Active Workflow ──────────────────────────────────────────────────
|
|
53
|
+
STATE_READER="$REPO/core/workflow/state_reader.sh"
|
|
54
|
+
if [ -n "$REPO" ] && [ -f "$STATE_READER" ] && bash "$STATE_READER" active 2>/dev/null; then
|
|
55
|
+
WF_SUMMARY=$(bash "$STATE_READER" summary 2>/dev/null)
|
|
56
|
+
WF_NAME=$(echo "$WF_SUMMARY" | cut -d'|' -f1)
|
|
57
|
+
WF_PHASE=$(echo "$WF_SUMMARY" | cut -d'|' -f2)
|
|
58
|
+
WF_PROGRESS=$(echo "$WF_SUMMARY" | cut -d'|' -f3)
|
|
59
|
+
WF_BRANCH=$(echo "$WF_SUMMARY" | cut -d'|' -f4)
|
|
60
|
+
WF_VIOLATIONS=$(echo "$WF_SUMMARY" | cut -d'|' -f5)
|
|
61
|
+
MSG+="\\nWorkflow: ${WF_NAME} (${WF_PROGRESS})"
|
|
62
|
+
[ -n "$WF_BRANCH" ] && MSG+=" branch:${WF_BRANCH}"
|
|
63
|
+
[ "$WF_VIOLATIONS" != "0" ] && MSG+=" VIOLATIONS:${WF_VIOLATIONS}"
|
|
64
|
+
MSG+="\\n"
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# --- Forge Plan Display ---
|
|
68
|
+
_FORGE_PLANS="$HOME/.arkaos/plans"
|
|
69
|
+
_FORGE_ACTIVE="$_FORGE_PLANS/active.yaml"
|
|
70
|
+
_FORGE_LINE=""
|
|
71
|
+
|
|
72
|
+
if [ -f "$_FORGE_ACTIVE" ]; then
|
|
73
|
+
_FORGE_ID=$(cat "$_FORGE_ACTIVE" 2>/dev/null)
|
|
74
|
+
_FORGE_FILE="$_FORGE_PLANS/${_FORGE_ID}.yaml"
|
|
75
|
+
if [ -f "$_FORGE_FILE" ] && command -v python3 &>/dev/null; then
|
|
76
|
+
_FORGE_NAME=$(FORGE_FILE="$_FORGE_FILE" python3 -c "import yaml,os; d=yaml.safe_load(open(os.environ['FORGE_FILE'])); print(d.get('name',''))" 2>/dev/null)
|
|
77
|
+
_FORGE_STATUS=$(FORGE_FILE="$_FORGE_FILE" python3 -c "import yaml,os; d=yaml.safe_load(open(os.environ['FORGE_FILE'])); print(d.get('status',''))" 2>/dev/null)
|
|
78
|
+
_FORGE_PHASES=$(FORGE_FILE="$_FORGE_FILE" python3 -c "import yaml,os; d=yaml.safe_load(open(os.environ['FORGE_FILE'])); print(len(d.get('plan_phases',[])))" 2>/dev/null)
|
|
79
|
+
_FORGE_BRANCH=$(FORGE_FILE="$_FORGE_FILE" python3 -c "import yaml,os; d=yaml.safe_load(open(os.environ['FORGE_FILE'])); print(d.get('governance',{}).get('branch_strategy',''))" 2>/dev/null)
|
|
80
|
+
|
|
81
|
+
if [ "$_FORGE_STATUS" = "approved" ]; then
|
|
82
|
+
_FORGE_LINE=" ⚒ Forge plan pending: ${_FORGE_NAME} | Phases: ${_FORGE_PHASES} | /forge resume"
|
|
83
|
+
elif [ "$_FORGE_STATUS" = "executing" ]; then
|
|
84
|
+
_FORGE_LINE=" ⚒ Forge executing: ${_FORGE_NAME} | Phases: ${_FORGE_PHASES} | Branch: ${_FORGE_BRANCH}"
|
|
85
|
+
fi
|
|
86
|
+
fi
|
|
87
|
+
fi
|
|
88
|
+
|
|
52
89
|
MSG+="ArkaOS v${VERSION} | 65 agents | 17 departments | 244+ skills"
|
|
90
|
+
[ -n "$_FORGE_LINE" ] && MSG+="\\n${_FORGE_LINE}"
|
|
53
91
|
MSG+="${DRIFT}"
|
|
54
92
|
|
|
55
93
|
# ─── Output as systemMessage (same protocol as claude-mem) ─────────────
|
|
@@ -99,6 +99,31 @@ if command -v python3 &>/dev/null && [ -f "$BRIDGE_SCRIPT" ]; then
|
|
|
99
99
|
if [ -n "$bridge_output" ]; then
|
|
100
100
|
python_result=$(echo "$bridge_output" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('context_string',''))" 2>/dev/null)
|
|
101
101
|
fi
|
|
102
|
+
|
|
103
|
+
# Append workflow state to synapse context
|
|
104
|
+
_WF_READER="$ARKAOS_ROOT/core/workflow/state_reader.sh"
|
|
105
|
+
if [ -f "$_WF_READER" ] && bash "$_WF_READER" active 2>/dev/null; then
|
|
106
|
+
_WF_SUM=$(bash "$_WF_READER" summary 2>/dev/null)
|
|
107
|
+
_WF_N=$(echo "$_WF_SUM" | cut -d'|' -f1)
|
|
108
|
+
_WF_P=$(echo "$_WF_SUM" | cut -d'|' -f2)
|
|
109
|
+
_WF_B=$(echo "$_WF_SUM" | cut -d'|' -f4)
|
|
110
|
+
_WF_V=$(echo "$_WF_SUM" | cut -d'|' -f5)
|
|
111
|
+
_WF_TAG="[workflow:${_WF_N}] [phase:${_WF_P}] [branch:${_WF_B}] [violations:${_WF_V}]"
|
|
112
|
+
[ "$_WF_V" != "0" ] && _WF_TAG="WARNING: ${_WF_V} workflow violation(s). $_WF_TAG"
|
|
113
|
+
python_result="${python_result} ${_WF_TAG}"
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# --- Forge Context Injection ---
|
|
117
|
+
_FORGE_ACTIVE="$HOME/.arkaos/plans/active.yaml"
|
|
118
|
+
if [ -f "$_FORGE_ACTIVE" ]; then
|
|
119
|
+
_FORGE_ID=$(cat "$_FORGE_ACTIVE" 2>/dev/null)
|
|
120
|
+
_FORGE_FILE="$HOME/.arkaos/plans/${_FORGE_ID}.yaml"
|
|
121
|
+
if [ -f "$_FORGE_FILE" ] && command -v python3 &>/dev/null; then
|
|
122
|
+
_FORGE_STATUS=$(FORGE_FILE="$_FORGE_FILE" python3 -c "import yaml,os; d=yaml.safe_load(open(os.environ['FORGE_FILE'])); print(d.get('status',''))" 2>/dev/null)
|
|
123
|
+
_FORGE_TAG="[forge:${_FORGE_ID}] [forge-status:${_FORGE_STATUS}]"
|
|
124
|
+
python_result="${python_result} ${_FORGE_TAG}"
|
|
125
|
+
fi
|
|
126
|
+
fi
|
|
102
127
|
fi
|
|
103
128
|
|
|
104
129
|
# ─── Fallback: Bash-only context (if Python unavailable) ────────────────
|
|
@@ -130,7 +155,32 @@ if [ -z "$python_result" ]; then
|
|
|
130
155
|
L7="[time:evening]"
|
|
131
156
|
fi
|
|
132
157
|
|
|
133
|
-
|
|
158
|
+
# L8: Workflow state
|
|
159
|
+
L8=""
|
|
160
|
+
_WF_READER="$ARKAOS_ROOT/core/workflow/state_reader.sh"
|
|
161
|
+
if [ -f "$_WF_READER" ] && bash "$_WF_READER" active 2>/dev/null; then
|
|
162
|
+
_WF_SUM=$(bash "$_WF_READER" summary 2>/dev/null)
|
|
163
|
+
_WF_N=$(echo "$_WF_SUM" | cut -d'|' -f1)
|
|
164
|
+
_WF_P=$(echo "$_WF_SUM" | cut -d'|' -f2)
|
|
165
|
+
_WF_B=$(echo "$_WF_SUM" | cut -d'|' -f4)
|
|
166
|
+
_WF_V=$(echo "$_WF_SUM" | cut -d'|' -f5)
|
|
167
|
+
L8="[workflow:${_WF_N}] [phase:${_WF_P}] [branch:${_WF_B}] [violations:${_WF_V}]"
|
|
168
|
+
[ "$_WF_V" != "0" ] && L8="WARNING: ${_WF_V} workflow violation(s). $L8"
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
# L9: Forge state
|
|
172
|
+
L9=""
|
|
173
|
+
_FORGE_ACTIVE_FB="$HOME/.arkaos/plans/active.yaml"
|
|
174
|
+
if [ -f "$_FORGE_ACTIVE_FB" ]; then
|
|
175
|
+
_FORGE_ID_FB=$(cat "$_FORGE_ACTIVE_FB" 2>/dev/null)
|
|
176
|
+
_FORGE_FILE_FB="$HOME/.arkaos/plans/${_FORGE_ID_FB}.yaml"
|
|
177
|
+
if [ -f "$_FORGE_FILE_FB" ] && command -v python3 &>/dev/null; then
|
|
178
|
+
_FORGE_STATUS_FB=$(FORGE_FILE="$_FORGE_FILE_FB" python3 -c "import yaml,os; d=yaml.safe_load(open(os.environ['FORGE_FILE'])); print(d.get('status',''))" 2>/dev/null)
|
|
179
|
+
L9="[forge:${_FORGE_ID_FB}] [forge-status:${_FORGE_STATUS_FB}]"
|
|
180
|
+
fi
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
python_result="$L0 $L4 $L7 $L8 $L9"
|
|
134
184
|
fi
|
|
135
185
|
|
|
136
186
|
# ─── Output ──────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""The Forge — ArkaOS Intelligent Planning Engine."""
|
|
2
|
+
|
|
3
|
+
from core.forge.schema import (
|
|
4
|
+
ForgeTier,
|
|
5
|
+
ForgeStatus,
|
|
6
|
+
ExplorerLens,
|
|
7
|
+
RiskSeverity,
|
|
8
|
+
ExecutionPathType,
|
|
9
|
+
ComplexityDimensions,
|
|
10
|
+
ComplexityScore,
|
|
11
|
+
KeyDecision,
|
|
12
|
+
PhaseDeliverable,
|
|
13
|
+
ExplorerApproach,
|
|
14
|
+
RejectedElement,
|
|
15
|
+
IdentifiedRisk,
|
|
16
|
+
CriticVerdict,
|
|
17
|
+
ForgeContext,
|
|
18
|
+
PlanPhase,
|
|
19
|
+
ExecutionPath,
|
|
20
|
+
ForgeGovernance,
|
|
21
|
+
ForgePlan,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from core.forge.complexity import (
|
|
25
|
+
score_dimensions,
|
|
26
|
+
calculate_weighted_score,
|
|
27
|
+
determine_tier,
|
|
28
|
+
analyze_complexity,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
from core.forge.persistence import (
|
|
32
|
+
save_plan,
|
|
33
|
+
load_plan,
|
|
34
|
+
list_plans,
|
|
35
|
+
get_active_plan,
|
|
36
|
+
set_active_plan,
|
|
37
|
+
clear_active_plan,
|
|
38
|
+
export_to_obsidian,
|
|
39
|
+
extract_patterns,
|
|
40
|
+
load_patterns,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
from core.forge.renderer import (
|
|
44
|
+
render_complexity,
|
|
45
|
+
render_critic_summary,
|
|
46
|
+
render_plan_overview,
|
|
47
|
+
render_terminal,
|
|
48
|
+
render_html,
|
|
49
|
+
should_suggest_companion,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
from core.forge.handoff import (
|
|
53
|
+
select_execution_path,
|
|
54
|
+
generate_workflow_yaml,
|
|
55
|
+
check_repo_drift,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
# schema
|
|
60
|
+
"ForgeTier",
|
|
61
|
+
"ForgeStatus",
|
|
62
|
+
"ExplorerLens",
|
|
63
|
+
"RiskSeverity",
|
|
64
|
+
"ExecutionPathType",
|
|
65
|
+
"ComplexityDimensions",
|
|
66
|
+
"ComplexityScore",
|
|
67
|
+
"KeyDecision",
|
|
68
|
+
"PhaseDeliverable",
|
|
69
|
+
"ExplorerApproach",
|
|
70
|
+
"RejectedElement",
|
|
71
|
+
"IdentifiedRisk",
|
|
72
|
+
"CriticVerdict",
|
|
73
|
+
"ForgeContext",
|
|
74
|
+
"PlanPhase",
|
|
75
|
+
"ExecutionPath",
|
|
76
|
+
"ForgeGovernance",
|
|
77
|
+
"ForgePlan",
|
|
78
|
+
# complexity
|
|
79
|
+
"score_dimensions",
|
|
80
|
+
"calculate_weighted_score",
|
|
81
|
+
"determine_tier",
|
|
82
|
+
"analyze_complexity",
|
|
83
|
+
# persistence
|
|
84
|
+
"save_plan",
|
|
85
|
+
"load_plan",
|
|
86
|
+
"list_plans",
|
|
87
|
+
"get_active_plan",
|
|
88
|
+
"set_active_plan",
|
|
89
|
+
"clear_active_plan",
|
|
90
|
+
"export_to_obsidian",
|
|
91
|
+
"extract_patterns",
|
|
92
|
+
"load_patterns",
|
|
93
|
+
# renderer
|
|
94
|
+
"render_complexity",
|
|
95
|
+
"render_critic_summary",
|
|
96
|
+
"render_plan_overview",
|
|
97
|
+
"render_terminal",
|
|
98
|
+
"render_html",
|
|
99
|
+
"should_suggest_companion",
|
|
100
|
+
# handoff
|
|
101
|
+
"select_execution_path",
|
|
102
|
+
"generate_workflow_yaml",
|
|
103
|
+
"check_repo_drift",
|
|
104
|
+
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Forge complexity scorer — 5-dimension analysis with tier determination."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from core.forge.schema import ComplexityDimensions, ComplexityScore, ForgeTier
|
|
6
|
+
|
|
7
|
+
_WEIGHTS = {
|
|
8
|
+
"scope": 0.30,
|
|
9
|
+
"dependencies": 0.25,
|
|
10
|
+
"ambiguity": 0.20,
|
|
11
|
+
"risk": 0.15,
|
|
12
|
+
"novelty": 0.10,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
_RISK_KEYWORDS = re.compile(
|
|
16
|
+
r"\b(auth\w*|security\w*|encrypt\w*|password\w*|token\w*|secret\w*|permission\w*|migration\w*|database\w*|schema\w*|deploy\w*|infra\w*|production\w*|payment\w*|billing\w*)",
|
|
17
|
+
re.IGNORECASE,
|
|
18
|
+
)
|
|
19
|
+
_VAGUE_PATTERNS = re.compile(
|
|
20
|
+
r"\b(fix|improve|make.*better|update|change|refactor|clean|optimize)\b",
|
|
21
|
+
re.IGNORECASE,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def score_dimensions(
|
|
26
|
+
prompt: str,
|
|
27
|
+
affected_files: list[str],
|
|
28
|
+
departments: list[str],
|
|
29
|
+
similar_plans: list[str],
|
|
30
|
+
reused_patterns: list[str],
|
|
31
|
+
) -> ComplexityDimensions:
|
|
32
|
+
"""Score all 5 complexity dimensions from prompt and context."""
|
|
33
|
+
return ComplexityDimensions(
|
|
34
|
+
scope=_score_scope(affected_files, departments),
|
|
35
|
+
dependencies=_score_dependencies(affected_files),
|
|
36
|
+
ambiguity=_score_ambiguity(prompt, affected_files),
|
|
37
|
+
risk=_score_risk(prompt, affected_files),
|
|
38
|
+
novelty=_score_novelty(similar_plans, reused_patterns),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def calculate_weighted_score(dims: ComplexityDimensions) -> int:
|
|
43
|
+
"""Calculate weighted total score from dimensions."""
|
|
44
|
+
total = (
|
|
45
|
+
dims.scope * _WEIGHTS["scope"]
|
|
46
|
+
+ dims.dependencies * _WEIGHTS["dependencies"]
|
|
47
|
+
+ dims.ambiguity * _WEIGHTS["ambiguity"]
|
|
48
|
+
+ dims.risk * _WEIGHTS["risk"]
|
|
49
|
+
+ dims.novelty * _WEIGHTS["novelty"]
|
|
50
|
+
)
|
|
51
|
+
return round(total)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def determine_tier(score: int) -> ForgeTier:
|
|
55
|
+
"""Map a 0-100 score to a forge tier."""
|
|
56
|
+
if score <= 30:
|
|
57
|
+
return ForgeTier.SHALLOW
|
|
58
|
+
if score <= 65:
|
|
59
|
+
return ForgeTier.STANDARD
|
|
60
|
+
return ForgeTier.DEEP
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def analyze_complexity(
|
|
64
|
+
prompt: str,
|
|
65
|
+
affected_files: list[str],
|
|
66
|
+
departments: list[str],
|
|
67
|
+
similar_plans: list[str],
|
|
68
|
+
reused_patterns: list[str],
|
|
69
|
+
) -> ComplexityScore:
|
|
70
|
+
"""Full complexity analysis: dimensions, weighted score, tier."""
|
|
71
|
+
dims = score_dimensions(prompt, affected_files, departments, similar_plans, reused_patterns)
|
|
72
|
+
score = calculate_weighted_score(dims)
|
|
73
|
+
tier = determine_tier(score)
|
|
74
|
+
return ComplexityScore(
|
|
75
|
+
score=score,
|
|
76
|
+
tier=tier,
|
|
77
|
+
dimensions=dims,
|
|
78
|
+
similar_plans=similar_plans,
|
|
79
|
+
reused_patterns=reused_patterns,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _score_scope(files: list[str], departments: list[str]) -> int:
|
|
84
|
+
file_score = min(100, len(files) * 10)
|
|
85
|
+
dept_score = min(100, len(departments) * 30)
|
|
86
|
+
return min(100, (file_score + dept_score) // 2)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _score_dependencies(files: list[str]) -> int:
|
|
90
|
+
if not files:
|
|
91
|
+
return 20
|
|
92
|
+
core_files = sum(1 for f in files if f.startswith("core/"))
|
|
93
|
+
ratio = core_files / len(files)
|
|
94
|
+
return min(100, int(ratio * 80) + 20)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _score_ambiguity(prompt: str, files: list[str]) -> int:
|
|
98
|
+
score = 50
|
|
99
|
+
if len(prompt.split()) < 5:
|
|
100
|
+
score += 30
|
|
101
|
+
if not files:
|
|
102
|
+
score += 20
|
|
103
|
+
vague_matches = len(_VAGUE_PATTERNS.findall(prompt))
|
|
104
|
+
score += min(20, vague_matches * 10)
|
|
105
|
+
specific_indicators = len(re.findall(r"[/\.]", prompt))
|
|
106
|
+
score -= min(30, specific_indicators * 5)
|
|
107
|
+
return max(0, min(100, score))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _score_risk(prompt: str, files: list[str]) -> int:
|
|
111
|
+
score = 20
|
|
112
|
+
risk_matches = len(_RISK_KEYWORDS.findall(prompt))
|
|
113
|
+
score += min(50, risk_matches * 15)
|
|
114
|
+
sensitive_paths = sum(
|
|
115
|
+
1 for f in files if any(kw in f for kw in ("auth", "security", "migration", "deploy", "config"))
|
|
116
|
+
)
|
|
117
|
+
score += min(30, sensitive_paths * 15)
|
|
118
|
+
return min(100, score)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _score_novelty(similar_plans: list[str], patterns: list[str]) -> int:
|
|
122
|
+
score = 90
|
|
123
|
+
score -= min(40, len(similar_plans) * 20)
|
|
124
|
+
score -= min(30, len(patterns) * 15)
|
|
125
|
+
return max(10, score)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Forge handoff — execution path routing and workflow generation."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import yaml
|
|
5
|
+
from core.forge.schema import ExecutionPath, ExecutionPathType, ForgePlan, PlanPhase
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def select_execution_path(phases: list[PlanPhase]) -> ExecutionPath:
|
|
9
|
+
"""Select the execution path based on phase count and departments.
|
|
10
|
+
|
|
11
|
+
Rules: 1 phase, 1 dept → skill | 2-3 phases, 1 dept → workflow | 4+ phases or 2+ depts → enterprise
|
|
12
|
+
"""
|
|
13
|
+
departments = list({p.department for p in phases})
|
|
14
|
+
n_phases = len(phases)
|
|
15
|
+
n_depts = len(departments)
|
|
16
|
+
|
|
17
|
+
if n_depts >= 2:
|
|
18
|
+
path_type = ExecutionPathType.ENTERPRISE_WORKFLOW
|
|
19
|
+
elif n_phases >= 4:
|
|
20
|
+
path_type = ExecutionPathType.ENTERPRISE_WORKFLOW
|
|
21
|
+
elif n_phases >= 2:
|
|
22
|
+
path_type = ExecutionPathType.WORKFLOW
|
|
23
|
+
else:
|
|
24
|
+
path_type = ExecutionPathType.SKILL
|
|
25
|
+
|
|
26
|
+
target = ""
|
|
27
|
+
if path_type == ExecutionPathType.SKILL and departments:
|
|
28
|
+
target = f"arka-{departments[0]}"
|
|
29
|
+
elif path_type in (ExecutionPathType.WORKFLOW, ExecutionPathType.ENTERPRISE_WORKFLOW):
|
|
30
|
+
target = "generated-workflow.yaml"
|
|
31
|
+
|
|
32
|
+
return ExecutionPath(
|
|
33
|
+
type=path_type,
|
|
34
|
+
target=target,
|
|
35
|
+
departments=departments,
|
|
36
|
+
estimated_commits=max(1, n_phases * 2),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def generate_workflow_yaml(plan: ForgePlan) -> str:
|
|
41
|
+
"""Generate a complete workflow YAML from a forge plan."""
|
|
42
|
+
phases_data = []
|
|
43
|
+
for phase in plan.plan_phases:
|
|
44
|
+
d: dict = {"name": phase.name, "department": phase.department}
|
|
45
|
+
if phase.agents:
|
|
46
|
+
d["agents"] = [{"role": a} for a in phase.agents]
|
|
47
|
+
if phase.deliverables:
|
|
48
|
+
d["deliverables"] = phase.deliverables
|
|
49
|
+
if phase.acceptance_criteria:
|
|
50
|
+
d["acceptance_criteria"] = phase.acceptance_criteria
|
|
51
|
+
if phase.depends_on:
|
|
52
|
+
d["depends_on"] = phase.depends_on
|
|
53
|
+
if phase.context_from_forge:
|
|
54
|
+
d["context_from_forge"] = phase.context_from_forge
|
|
55
|
+
phases_data.append(d)
|
|
56
|
+
|
|
57
|
+
workflow = {
|
|
58
|
+
"name": plan.id,
|
|
59
|
+
"type": "enterprise" if len(plan.plan_phases) >= 4 else "focused",
|
|
60
|
+
"generated_by": "forge",
|
|
61
|
+
"forge_plan_id": plan.id,
|
|
62
|
+
"phases": phases_data,
|
|
63
|
+
"quality_gate_required": plan.governance.quality_gate_required,
|
|
64
|
+
"branch": plan.governance.branch_strategy,
|
|
65
|
+
}
|
|
66
|
+
return yaml.dump(workflow, default_flow_style=False, allow_unicode=True)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _git_changed_files(base: str, current: str) -> list[str]:
|
|
70
|
+
"""Return list of files changed between two commits, or [] on error."""
|
|
71
|
+
try:
|
|
72
|
+
result = subprocess.run(
|
|
73
|
+
["git", "diff", "--name-only", base, current],
|
|
74
|
+
capture_output=True, text=True, check=True,
|
|
75
|
+
)
|
|
76
|
+
return [f for f in result.stdout.strip().split("\n") if f]
|
|
77
|
+
except subprocess.CalledProcessError:
|
|
78
|
+
return []
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def check_repo_drift(commit_at_forge: str) -> dict:
|
|
82
|
+
"""Check if the repo has changed since the forge snapshot."""
|
|
83
|
+
try:
|
|
84
|
+
result = subprocess.run(
|
|
85
|
+
["git", "rev-parse", "HEAD"],
|
|
86
|
+
capture_output=True, text=True, check=True,
|
|
87
|
+
)
|
|
88
|
+
current = result.stdout.strip()
|
|
89
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
90
|
+
return {"changed": False, "files": [], "message": "Could not read git state"}
|
|
91
|
+
|
|
92
|
+
if current == commit_at_forge:
|
|
93
|
+
return {"changed": False, "files": [], "message": "Repo unchanged"}
|
|
94
|
+
|
|
95
|
+
files = _git_changed_files(commit_at_forge, current)
|
|
96
|
+
return {
|
|
97
|
+
"changed": True,
|
|
98
|
+
"files": files,
|
|
99
|
+
"message": f"Repo changed: {len(files)} files modified since forge",
|
|
100
|
+
}
|