arkaos 4.0.2 → 4.1.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/README.md +2 -2
- package/VERSION +1 -1
- package/arka/SKILL.md +16 -22
- package/arka/skills/flow/SKILL.md +108 -234
- package/config/claude-agents/eduardo-copy.md +53 -0
- package/config/claude-agents/francisca-tech.md +52 -0
- package/config/claude-agents/marta-cqo.md +52 -0
- package/config/claude-agents/paulo-tech-lead.md +50 -0
- package/config/cognition/schedules.yaml +15 -0
- package/config/constitution.yaml +17 -21
- package/config/hooks/_lib/workflow-classifier.sh +1 -1
- package/config/hooks/post-tool-use.sh +52 -536
- package/config/hooks/pre-tool-use.sh +46 -280
- package/config/hooks/session-start.sh +8 -10
- package/config/hooks/stop.sh +44 -424
- package/config/hooks/user-prompt-submit.sh +55 -457
- package/core/__pycache__/__init__.cpython-314.pyc +0 -0
- package/core/cognition/__pycache__/dreaming.cpython-313.pyc +0 -0
- package/core/cognition/dreaming.py +5 -0
- package/core/cognition/scheduler/__pycache__/daemon.cpython-313.pyc +0 -0
- package/core/cognition/scheduler/daemon.py +10 -0
- package/core/forge/__init__.py +2 -0
- package/core/forge/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/orchestrator.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__/runtime_dispatcher.cpython-313.pyc +0 -0
- package/core/forge/__pycache__/schema.cpython-313.pyc +0 -0
- package/core/forge/orchestrator.py +50 -27
- package/core/forge/persistence.py +16 -0
- package/core/forge/renderer.py +16 -0
- package/core/forge/runtime_dispatcher.py +34 -19
- package/core/forge/schema.py +12 -0
- package/core/governance/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/governance/__pycache__/__init__.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/closing_marker_check.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/closing_marker_check.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/compliance_telemetry.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/constitution.cpython-312.pyc +0 -0
- package/core/governance/__pycache__/constitution.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/evidence_checks.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/kb_cite_check.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/meta_tag_check.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/meta_tag_check.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/qg_verdict.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/review_workflow.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/skill_proposer.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/skill_proposer.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/sycophancy_detector.cpython-314.pyc +0 -0
- package/core/governance/closing_marker_check.py +9 -5
- package/core/governance/compliance_telemetry.py +1 -1
- package/core/governance/compliance_telemetry_cli.py +1 -1
- package/core/governance/evidence_checks.py +421 -0
- package/core/governance/meta_tag_check.py +36 -0
- package/core/governance/qg_verdict.py +78 -0
- package/core/governance/review_workflow.py +34 -1
- package/core/governance/skill_proposer.py +2 -1
- package/core/hooks/__init__.py +12 -0
- package/core/hooks/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/__init__.cpython-314.pyc +0 -0
- package/core/hooks/__pycache__/_shared.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/_shared.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/_shared.cpython-314.pyc +0 -0
- package/core/hooks/__pycache__/post_tool_use.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/post_tool_use.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/pre_tool_use.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/pre_tool_use.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/pre_tool_use.cpython-314.pyc +0 -0
- package/core/hooks/__pycache__/stop.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/stop.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/stop.cpython-314.pyc +0 -0
- package/core/hooks/__pycache__/user_prompt_submit.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/core/hooks/_shared.py +110 -0
- package/core/hooks/post_tool_use.py +689 -0
- package/core/hooks/pre_tool_use.py +267 -0
- package/core/hooks/stop.py +395 -0
- package/core/hooks/user_prompt_submit.py +600 -0
- package/core/jobs/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/jobs/__pycache__/__init__.cpython-314.pyc +0 -0
- package/core/jobs/__pycache__/auto_doc_worker.cpython-312.pyc +0 -0
- package/core/jobs/__pycache__/auto_doc_worker.cpython-314.pyc +0 -0
- package/core/jobs/__pycache__/manager.cpython-312.pyc +0 -0
- package/core/jobs/__pycache__/manager.cpython-314.pyc +0 -0
- package/core/knowledge/__pycache__/embedder.cpython-312.pyc +0 -0
- package/core/knowledge/__pycache__/embedder.cpython-313.pyc +0 -0
- package/core/knowledge/__pycache__/vector_store.cpython-312.pyc +0 -0
- package/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
- package/core/knowledge/embedder.py +118 -11
- package/core/knowledge/vector_store.py +111 -14
- package/core/memory/__pycache__/session_store.cpython-314.pyc +0 -0
- package/core/obsidian/__pycache__/relator.cpython-313.pyc +0 -0
- package/core/obsidian/relator.py +18 -8
- package/core/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/__init__.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/base.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/base.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/base.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/capabilities_cli.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/capabilities_cli.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/claude_code.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/claude_code.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/claude_code.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/codex_cli.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/codex_cli.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/codex_cli.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/cost_governor.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/cost_governor.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/cursor.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/cursor.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/cursor.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/gemini_cli.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/gemini_cli.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/gemini_cli.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/llm_cost_telemetry.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/llm_provider.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/llm_provider.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/llm_provider.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/llm_retry.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/llm_retry.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/llm_retry.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/native_usage.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/native_usage.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/native_usage.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/ollama_provider.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/path_resolver.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/pricing.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/pricing.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/pricing.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/registry.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/registry.cpython-313.pyc +0 -0
- package/core/runtime/__pycache__/registry.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/squad_orchestrator.cpython-312.pyc +0 -0
- package/core/runtime/base.py +29 -0
- package/core/runtime/capabilities_cli.py +74 -0
- package/core/runtime/claude_code.py +8 -0
- package/core/runtime/codex_cli.py +205 -37
- package/core/runtime/cost_governor.py +184 -0
- package/core/runtime/cursor.py +8 -0
- package/core/runtime/gemini_cli.py +8 -0
- package/core/runtime/llm_provider.py +32 -13
- package/core/runtime/llm_retry.py +233 -0
- package/core/runtime/native_usage.py +186 -0
- package/core/runtime/pricing.py +9 -0
- package/core/runtime/registry.py +3 -0
- package/core/shared/__pycache__/__init__.cpython-314.pyc +0 -0
- package/core/shared/__pycache__/safe_session_id.cpython-314.pyc +0 -0
- package/core/shared/__pycache__/test_evidence.cpython-313-pytest-9.1.1.pyc +0 -0
- package/core/shared/__pycache__/test_evidence.cpython-313.pyc +0 -0
- package/core/shared/test_evidence.py +34 -0
- package/core/synapse/__pycache__/__init__.cpython-314.pyc +0 -0
- package/core/synapse/__pycache__/agent_experiences_layer.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/cache.cpython-314.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-314.pyc +0 -0
- package/core/synapse/__pycache__/graph_context_layer.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/kb_cache.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/kb_cache.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/kb_cache.cpython-314.pyc +0 -0
- package/core/synapse/__pycache__/layers.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/layers.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/layers.cpython-314.pyc +0 -0
- package/core/synapse/engine.py +7 -0
- package/core/synapse/graph_context_layer.py +212 -0
- package/core/synapse/kb_cache.py +3 -0
- package/core/synapse/layers.py +104 -20
- package/core/workflow/__pycache__/__init__.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/enforcer.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/engine.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/flow_enforcer.cpython-312.pyc +0 -0
- package/core/workflow/__pycache__/flow_enforcer.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/flow_enforcer.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/gate_checkpoint.cpython-312.pyc +0 -0
- package/core/workflow/__pycache__/gate_checkpoint.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/gate_checkpoint.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/loader.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/marker_cache.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/research_gate.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/research_gate.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/rules_registry.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/schema.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/specialist_enforcer.cpython-312.pyc +0 -0
- package/core/workflow/__pycache__/specialist_enforcer.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/state.cpython-314.pyc +0 -0
- package/core/workflow/enforcer.py +5 -16
- package/core/workflow/flow_enforcer.py +36 -11
- package/core/workflow/gate_checkpoint.py +149 -0
- package/core/workflow/research_gate.py +15 -3
- package/core/workflow/rules_registry.py +137 -389
- package/core/workflow/specialist_enforcer.py +21 -4
- package/dashboard/app/pages/knowledge/index.vue +4 -1
- package/departments/ops/skills/n8n-flow/SKILL.md +13 -0
- package/departments/ops/skills/workflow-automate/SKILL.md +16 -0
- package/departments/quality/SKILL.md +45 -5
- package/departments/quality/agents/copy-director.yaml +3 -1
- package/departments/quality/agents/tech-director.yaml +3 -1
- package/installer/adapters/claude-code.js +46 -2
- package/installer/config-seed.js +39 -14
- package/installer/doctor.js +8 -0
- package/installer/graphify.js +153 -0
- package/installer/index.js +20 -0
- package/installer/init.js +11 -0
- package/installer/update.js +19 -0
- package/package.json +1 -1
- package/pyproject.toml +3 -1
- package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
- package/scripts/__pycache__/knowledge-index.cpython-313.pyc +0 -0
- package/scripts/__pycache__/synapse-bridge.cpython-312.pyc +0 -0
- package/scripts/__pycache__/synapse-bridge.cpython-313.pyc +0 -0
- package/scripts/knowledge-index.py +8 -2
- package/scripts/synapse-bridge.py +102 -26
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
"""PostToolUse — consolidated entrypoint (PR-6 v4.1.0 hook hygiene).
|
|
2
|
+
|
|
3
|
+
Replaces the ~38 python3/jq spawn sites of the old ``post-tool-use.sh``
|
|
4
|
+
with ONE python process (plus one detached child for the cognition
|
|
5
|
+
capture, preserving its fire-and-forget backgrounding, and one optional
|
|
6
|
+
venv fallback for the yaml-needing workflow sections on machines whose
|
|
7
|
+
ambient python3 lacks PyYAML — mirroring the old ARKAOS_PY resolution).
|
|
8
|
+
|
|
9
|
+
Section order is preserved exactly:
|
|
10
|
+
1. Flow marker cache write ([arka:routing]/[arka:trivial] detection)
|
|
11
|
+
2. CQO REJECTED experience auto-record + APPROVED pattern stub
|
|
12
|
+
3. Activation tracking for every Task/Agent dispatch
|
|
13
|
+
4. Early exit `{}` when the tool output carries no error signal
|
|
14
|
+
5. Gotchas memory (~/.arkaos/gotchas.json, flock, top-100)
|
|
15
|
+
6. Workflow violation rules 1-3 (branch-isolation, spec-driven,
|
|
16
|
+
sequential-validation) against ~/.arkaos/workflow-state.json
|
|
17
|
+
7. Enforcement engine (core/workflow/enforcer.py, all rules)
|
|
18
|
+
8. Forge scope-creep detection (yaml plan deliverables)
|
|
19
|
+
9. Cognition capture enqueue (detached background process)
|
|
20
|
+
10. Hook metrics append + additionalContext output
|
|
21
|
+
|
|
22
|
+
Sections 6-8 need core.workflow / yaml. When the in-process import fails
|
|
23
|
+
(PyYAML-less ambient python3) and the ArkaOS venv exists, they are
|
|
24
|
+
delegated ONCE to ``<venv>/bin/python3 -m core.hooks.post_tool_use
|
|
25
|
+
--workflow-sections`` — the same interpreter the old hook used for them.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import re
|
|
33
|
+
import subprocess
|
|
34
|
+
import sys
|
|
35
|
+
import time
|
|
36
|
+
from datetime import datetime, timezone
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
from core.hooks._shared import (
|
|
40
|
+
ensure_root_on_path,
|
|
41
|
+
get_str,
|
|
42
|
+
read_stdin_json,
|
|
43
|
+
repo_path,
|
|
44
|
+
resolve_arkaos_root,
|
|
45
|
+
venv_python,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
import fcntl # POSIX only
|
|
50
|
+
_HAS_FLOCK = True
|
|
51
|
+
except ImportError:
|
|
52
|
+
_HAS_FLOCK = False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_ROUTING_RE = re.compile(
|
|
56
|
+
r"\[arka:routing\][ \t]*([A-Za-z_-]+)[ \t]*->[ \t]*([A-Za-z_-]+)",
|
|
57
|
+
re.IGNORECASE,
|
|
58
|
+
)
|
|
59
|
+
_TRIVIAL_RE = re.compile(r"\[arka:trivial\][ \t]*\S+", re.IGNORECASE)
|
|
60
|
+
_REJECTED_RE = re.compile(r"Quality Gate Verdict:[ \t]*REJECTED")
|
|
61
|
+
_APPROVED_RE = re.compile(r"Quality Gate Verdict:[ \t]*APPROVED")
|
|
62
|
+
_REVIEWING_RE = re.compile(r"\[arka:reviewing[ \t]+([A-Za-z0-9_.-]+)\]")
|
|
63
|
+
_PATTERN_SUGGEST_RE = re.compile(
|
|
64
|
+
r"\[arka:pattern-suggest[ \t]+([A-Za-z0-9_.-]+)[ \t]+([^][]+)\]"
|
|
65
|
+
)
|
|
66
|
+
_ERROR_TRIGGER_RE = re.compile(
|
|
67
|
+
r"(error:|fatal:|exception:|failed|ENOENT|EACCES|EPERM|panic:)",
|
|
68
|
+
re.IGNORECASE,
|
|
69
|
+
)
|
|
70
|
+
_ERROR_LINE_RE = re.compile(
|
|
71
|
+
r"(error|fatal|exception|failed|ENOENT|EACCES|EPERM|panic|cannot"
|
|
72
|
+
r"|not found|permission denied)",
|
|
73
|
+
re.IGNORECASE,
|
|
74
|
+
)
|
|
75
|
+
_CODE_FILE_RE = re.compile(r"\.(py|js|ts|vue|php|jsx|tsx)$")
|
|
76
|
+
|
|
77
|
+
_CATEGORY_RES: tuple[tuple[str, re.Pattern], ...] = (
|
|
78
|
+
("laravel", re.compile(
|
|
79
|
+
r"(artisan|eloquent|laravel|blade|migration|composer|php )", re.I)),
|
|
80
|
+
("frontend", re.compile(
|
|
81
|
+
r"(npm|node|vue|react|nuxt|next|vite|webpack|typescript|tsx|jsx)",
|
|
82
|
+
re.I)),
|
|
83
|
+
("git", re.compile(
|
|
84
|
+
r"(git |merge|rebase|checkout|branch|commit|push|pull)", re.I)),
|
|
85
|
+
("database", re.compile(
|
|
86
|
+
r"(sql|postgres|mysql|database|migration|table|column|constraint)",
|
|
87
|
+
re.I)),
|
|
88
|
+
("permissions", re.compile(
|
|
89
|
+
r"(permission|denied|EACCES|EPERM|chmod|chown|sudo)", re.I)),
|
|
90
|
+
("testing", re.compile(
|
|
91
|
+
r"(test|assert|expect|jest|phpunit|bats|coverage)", re.I)),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _locked(fh) -> None:
|
|
96
|
+
if _HAS_FLOCK:
|
|
97
|
+
try:
|
|
98
|
+
fcntl.flock(fh.fileno(), fcntl.LOCK_EX)
|
|
99
|
+
except OSError:
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _unlocked(fh) -> None:
|
|
104
|
+
if _HAS_FLOCK:
|
|
105
|
+
try:
|
|
106
|
+
fcntl.flock(fh.fileno(), fcntl.LOCK_UN)
|
|
107
|
+
except OSError:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ─── Section 1: flow marker cache ────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _write_flow_marker(session_id: str, assistant_msg: str) -> None:
|
|
115
|
+
if not session_id or not assistant_msg:
|
|
116
|
+
return
|
|
117
|
+
routing = _ROUTING_RE.search(assistant_msg)
|
|
118
|
+
if routing:
|
|
119
|
+
kind, dept, lead = "routing", routing.group(1), routing.group(2)
|
|
120
|
+
elif _TRIVIAL_RE.search(assistant_msg):
|
|
121
|
+
kind, dept, lead = "trivial", "", ""
|
|
122
|
+
else:
|
|
123
|
+
return
|
|
124
|
+
try:
|
|
125
|
+
from core.workflow.marker_cache import write_marker
|
|
126
|
+
write_marker(session_id, kind, dept, lead)
|
|
127
|
+
except Exception:
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ─── Section 2+3: CQO verdicts, pattern stubs, activation ───────────────
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _record_cqo_rejected(tool_output: str, prompt: str, session_id: str) -> None:
|
|
135
|
+
if not _REJECTED_RE.search(tool_output):
|
|
136
|
+
return
|
|
137
|
+
match = _REVIEWING_RE.search(prompt)
|
|
138
|
+
if not match:
|
|
139
|
+
return
|
|
140
|
+
try:
|
|
141
|
+
from core.governance.cqo_experience_recorder import record_from_verdict
|
|
142
|
+
record_from_verdict(
|
|
143
|
+
verdict_text=tool_output,
|
|
144
|
+
agent_id=match.group(1),
|
|
145
|
+
session_id=session_id,
|
|
146
|
+
context="auto-recorded via PostToolUse hook (cqo dispatch REJECTED)",
|
|
147
|
+
)
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _record_pattern_stub(tool_output: str, prompt: str) -> None:
|
|
153
|
+
if not _APPROVED_RE.search(tool_output):
|
|
154
|
+
return
|
|
155
|
+
match = _PATTERN_SUGGEST_RE.search(prompt)
|
|
156
|
+
if not match:
|
|
157
|
+
return
|
|
158
|
+
pid, pname = match.group(1), match.group(2).strip()
|
|
159
|
+
if not pid or not pname:
|
|
160
|
+
return
|
|
161
|
+
try:
|
|
162
|
+
from core.knowledge.pattern_cards import (
|
|
163
|
+
PatternCard,
|
|
164
|
+
query_patterns,
|
|
165
|
+
record_pattern,
|
|
166
|
+
)
|
|
167
|
+
if any(c.id == pid for c in query_patterns(limit=1000)):
|
|
168
|
+
return
|
|
169
|
+
ts = datetime.now(timezone.utc).isoformat()
|
|
170
|
+
record_pattern(PatternCard(
|
|
171
|
+
id=pid,
|
|
172
|
+
name=pname,
|
|
173
|
+
feature_keywords=[pid.replace("-", " "), pname.lower()],
|
|
174
|
+
description=(
|
|
175
|
+
"Stub auto-created from APPROVED CQO verdict — enrich via "
|
|
176
|
+
"record_pattern() or by editing the JSONL."
|
|
177
|
+
),
|
|
178
|
+
stack=[], files=[], acceptance_criteria=[], edge_cases=[],
|
|
179
|
+
references=[], projects_using=["arkaos"],
|
|
180
|
+
created_at=ts, last_updated=ts,
|
|
181
|
+
))
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _record_activation(subagent_type: str, session_id: str) -> None:
|
|
187
|
+
if not subagent_type:
|
|
188
|
+
return
|
|
189
|
+
try:
|
|
190
|
+
from core.governance.activation_tracker import record_activation
|
|
191
|
+
record_activation(subagent_type=subagent_type, session_id=session_id)
|
|
192
|
+
except Exception:
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ─── Section 5: gotchas memory ───────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _extract_error_line(tool_output: str) -> str:
|
|
200
|
+
lines = tool_output.splitlines()
|
|
201
|
+
for line in lines:
|
|
202
|
+
if _ERROR_LINE_RE.search(line):
|
|
203
|
+
return line
|
|
204
|
+
# Fallback mirrors `head -5 | tail -1`.
|
|
205
|
+
head = lines[:5]
|
|
206
|
+
return head[-1] if head else ""
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _normalize_pattern(error_line: str) -> str:
|
|
210
|
+
pattern = re.sub(
|
|
211
|
+
r"\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}[^ ]*", "TIMESTAMP",
|
|
212
|
+
error_line,
|
|
213
|
+
)
|
|
214
|
+
pattern = re.sub(r"[0-9a-f]{7,40}", "HASH", pattern)
|
|
215
|
+
pattern = re.sub(r"line \d+", "line N", pattern)
|
|
216
|
+
pattern = re.sub(r":\d+:", ":N:", pattern)
|
|
217
|
+
return pattern[:200]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _categorize(error_line: str) -> str:
|
|
221
|
+
for category, regex in _CATEGORY_RES:
|
|
222
|
+
if regex.search(error_line):
|
|
223
|
+
return category
|
|
224
|
+
return "general"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _fixes_file() -> Path:
|
|
228
|
+
arka_os = os.environ.get(
|
|
229
|
+
"ARKA_OS", str(Path.home() / ".claude" / "skills" / "arka")
|
|
230
|
+
)
|
|
231
|
+
fixes = Path(arka_os) / "config" / "gotchas-fixes.json"
|
|
232
|
+
if fixes.is_file():
|
|
233
|
+
return fixes
|
|
234
|
+
try:
|
|
235
|
+
repo = (Path(arka_os) / ".repo-path").read_text(encoding="utf-8").strip()
|
|
236
|
+
except OSError:
|
|
237
|
+
repo = ""
|
|
238
|
+
return Path(repo) / "config" / "gotchas-fixes.json" if repo else fixes
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _match_suggestion(error_line: str) -> str:
|
|
242
|
+
fixes_path = _fixes_file()
|
|
243
|
+
if not fixes_path.is_file():
|
|
244
|
+
return ""
|
|
245
|
+
try:
|
|
246
|
+
fixes = json.loads(fixes_path.read_text(encoding="utf-8"))
|
|
247
|
+
except (OSError, json.JSONDecodeError):
|
|
248
|
+
return ""
|
|
249
|
+
for fix in fixes.get("fixes", []):
|
|
250
|
+
pattern = fix.get("pattern_match", "")
|
|
251
|
+
try:
|
|
252
|
+
if pattern and re.search(pattern, error_line, re.IGNORECASE):
|
|
253
|
+
return str(fix.get("suggestion", ""))
|
|
254
|
+
except re.error:
|
|
255
|
+
continue
|
|
256
|
+
return ""
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _detect_project(cwd: str) -> str:
|
|
260
|
+
if not cwd:
|
|
261
|
+
return ""
|
|
262
|
+
arka_os = os.environ.get(
|
|
263
|
+
"ARKA_OS", str(Path.home() / ".claude" / "skills" / "arka")
|
|
264
|
+
)
|
|
265
|
+
try:
|
|
266
|
+
repo = (Path(arka_os) / ".repo-path").read_text(encoding="utf-8").strip()
|
|
267
|
+
except OSError:
|
|
268
|
+
repo = ""
|
|
269
|
+
if repo and (Path(repo) / "projects").is_dir():
|
|
270
|
+
for proj_dir in sorted((Path(repo) / "projects").iterdir()):
|
|
271
|
+
marker = proj_dir / ".project-path"
|
|
272
|
+
if not marker.is_file():
|
|
273
|
+
continue
|
|
274
|
+
try:
|
|
275
|
+
proj_path = marker.read_text(encoding="utf-8").strip()
|
|
276
|
+
except OSError:
|
|
277
|
+
continue
|
|
278
|
+
if proj_path and cwd.startswith(proj_path):
|
|
279
|
+
return proj_dir.name
|
|
280
|
+
return Path(cwd).name
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _store_gotcha(
|
|
284
|
+
pattern: str, error_line: str, category: str,
|
|
285
|
+
tool_name: str, project: str, suggestion: str,
|
|
286
|
+
) -> None:
|
|
287
|
+
gotchas_file = Path.home() / ".arkaos" / "gotchas.json"
|
|
288
|
+
lock_file = Path.home() / ".arkaos" / "gotchas.lock"
|
|
289
|
+
gotchas_file.parent.mkdir(parents=True, exist_ok=True)
|
|
290
|
+
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
291
|
+
try:
|
|
292
|
+
with lock_file.open("a", encoding="utf-8") as lock_fh:
|
|
293
|
+
_locked(lock_fh)
|
|
294
|
+
try:
|
|
295
|
+
try:
|
|
296
|
+
entries = json.loads(
|
|
297
|
+
gotchas_file.read_text(encoding="utf-8")
|
|
298
|
+
)
|
|
299
|
+
if not isinstance(entries, list):
|
|
300
|
+
entries = []
|
|
301
|
+
except (OSError, json.JSONDecodeError):
|
|
302
|
+
entries = []
|
|
303
|
+
existing = next(
|
|
304
|
+
(e for e in entries if e.get("pattern") == pattern), None
|
|
305
|
+
)
|
|
306
|
+
if existing is not None:
|
|
307
|
+
existing["count"] = int(existing.get("count", 0)) + 1
|
|
308
|
+
existing["last_seen"] = now
|
|
309
|
+
if project and project not in existing.get("projects", []):
|
|
310
|
+
existing.setdefault("projects", []).append(project)
|
|
311
|
+
if suggestion and not existing.get("suggestion"):
|
|
312
|
+
existing["suggestion"] = suggestion
|
|
313
|
+
else:
|
|
314
|
+
entries.append({
|
|
315
|
+
"pattern": pattern,
|
|
316
|
+
"full_pattern": error_line[:500],
|
|
317
|
+
"category": category,
|
|
318
|
+
"tool": tool_name,
|
|
319
|
+
"count": 1,
|
|
320
|
+
"first_seen": now,
|
|
321
|
+
"last_seen": now,
|
|
322
|
+
"projects": [project] if project else [],
|
|
323
|
+
"suggestion": suggestion or None,
|
|
324
|
+
})
|
|
325
|
+
entries.sort(key=lambda e: -int(e.get("count", 0)))
|
|
326
|
+
tmp = gotchas_file.with_suffix(".json.tmp")
|
|
327
|
+
tmp.write_text(
|
|
328
|
+
json.dumps(entries[:100]), encoding="utf-8"
|
|
329
|
+
)
|
|
330
|
+
tmp.replace(gotchas_file)
|
|
331
|
+
finally:
|
|
332
|
+
_unlocked(lock_fh)
|
|
333
|
+
except OSError:
|
|
334
|
+
pass
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# ─── Sections 6-8: workflow rules, enforcer, forge (yaml-needing) ───────
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _workflow_state() -> dict | None:
|
|
341
|
+
state_file = Path.home() / ".arkaos" / "workflow-state.json"
|
|
342
|
+
if not state_file.is_file():
|
|
343
|
+
return None
|
|
344
|
+
try:
|
|
345
|
+
state = json.loads(state_file.read_text(encoding="utf-8"))
|
|
346
|
+
except (OSError, json.JSONDecodeError):
|
|
347
|
+
return None
|
|
348
|
+
return state if isinstance(state, dict) else None
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _phase_status(state: dict, phase: str) -> str:
|
|
352
|
+
return str(
|
|
353
|
+
(state.get("phases", {}) or {}).get(phase, {}).get("status", "")
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _detect_rule_violations(input_data: dict) -> tuple[str, list[tuple]]:
|
|
358
|
+
"""Rules 1-3 (stdlib detection). Returns (violation_msg, to_persist)."""
|
|
359
|
+
state = _workflow_state()
|
|
360
|
+
if state is None:
|
|
361
|
+
return "", []
|
|
362
|
+
tool_name = get_str(input_data, "tool_name")
|
|
363
|
+
tool_output = get_str(input_data, "tool_output")
|
|
364
|
+
violation_msg = ""
|
|
365
|
+
persist: list[tuple] = []
|
|
366
|
+
|
|
367
|
+
if tool_name == "Bash":
|
|
368
|
+
on_master = any(
|
|
369
|
+
re.match(r"^\[(master|main)", line)
|
|
370
|
+
for line in tool_output.splitlines()
|
|
371
|
+
)
|
|
372
|
+
cmd_text = get_str(input_data, "command")
|
|
373
|
+
if on_master and re.search(r"git commit", cmd_text):
|
|
374
|
+
persist.append((
|
|
375
|
+
"branch-isolation",
|
|
376
|
+
"Commit on master/main while workflow active", "Bash", "",
|
|
377
|
+
))
|
|
378
|
+
violation_msg = (
|
|
379
|
+
"VIOLATION [branch-isolation]: Commit on master while "
|
|
380
|
+
"workflow active. Use a feature branch."
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
if tool_name in ("Write", "Edit"):
|
|
384
|
+
file_path = get_str(input_data, "file_path")
|
|
385
|
+
if _CODE_FILE_RE.search(file_path):
|
|
386
|
+
if _phase_status(state, "spec") != "completed":
|
|
387
|
+
persist.append((
|
|
388
|
+
"spec-driven", "Code edited without completed spec",
|
|
389
|
+
tool_name, file_path,
|
|
390
|
+
))
|
|
391
|
+
violation_msg = (
|
|
392
|
+
f"VIOLATION [spec-driven]: Code edited without completed "
|
|
393
|
+
f"spec ({file_path}). Complete the spec phase first."
|
|
394
|
+
)
|
|
395
|
+
if not violation_msg and _phase_status(
|
|
396
|
+
state, "implementation"
|
|
397
|
+
) == "pending":
|
|
398
|
+
persist.append((
|
|
399
|
+
"sequential-validation",
|
|
400
|
+
"Code written before implementation phase started",
|
|
401
|
+
tool_name, file_path,
|
|
402
|
+
))
|
|
403
|
+
violation_msg = (
|
|
404
|
+
f"VIOLATION [sequential-validation]: Implementation "
|
|
405
|
+
f"started before planning completed ({file_path})."
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
return violation_msg, persist
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _run_workflow_sections(input_data: dict, persist: list, msg: str) -> str:
|
|
412
|
+
"""Persist rule violations + run enforcer + forge check (needs yaml).
|
|
413
|
+
|
|
414
|
+
Raises ImportError when the interpreter lacks the dependencies —
|
|
415
|
+
caller falls back to the venv delegate.
|
|
416
|
+
"""
|
|
417
|
+
import yaml # noqa: F401 — probe: forge + core.workflow need it
|
|
418
|
+
from core.workflow.enforcer import enforce_tool
|
|
419
|
+
from core.workflow.state import add_violation
|
|
420
|
+
|
|
421
|
+
for rule_id, message, tool, file_path in persist:
|
|
422
|
+
try:
|
|
423
|
+
add_violation(rule_id, message, tool, file_path)
|
|
424
|
+
except Exception:
|
|
425
|
+
pass
|
|
426
|
+
|
|
427
|
+
msg = _enforcer_messages(input_data, enforce_tool, add_violation, msg)
|
|
428
|
+
if not msg:
|
|
429
|
+
msg = _forge_violation(input_data, yaml)
|
|
430
|
+
return msg
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _enforcer_messages(input_data, enforce_tool, add_violation, msg: str) -> str:
|
|
434
|
+
tool_name = get_str(input_data, "tool_name")
|
|
435
|
+
extra = {}
|
|
436
|
+
if tool_name == "Bash":
|
|
437
|
+
try:
|
|
438
|
+
branch = subprocess.check_output(
|
|
439
|
+
["git", "branch", "--show-current"],
|
|
440
|
+
text=True, stderr=subprocess.DEVNULL,
|
|
441
|
+
).strip()
|
|
442
|
+
extra["git_branch"] = branch
|
|
443
|
+
except Exception:
|
|
444
|
+
extra["git_branch"] = ""
|
|
445
|
+
try:
|
|
446
|
+
result = enforce_tool(
|
|
447
|
+
tool_name=tool_name,
|
|
448
|
+
command=get_str(input_data, "command"),
|
|
449
|
+
file_path=get_str(input_data, "file_path"),
|
|
450
|
+
user_input=get_str(input_data, "user_input"),
|
|
451
|
+
**extra,
|
|
452
|
+
)
|
|
453
|
+
except Exception:
|
|
454
|
+
return msg
|
|
455
|
+
if not result.violations:
|
|
456
|
+
return msg
|
|
457
|
+
for v in result.violations:
|
|
458
|
+
try:
|
|
459
|
+
add_violation(v.rule_id, v.message, v.tool, v.file_path, v.severity)
|
|
460
|
+
except Exception:
|
|
461
|
+
pass
|
|
462
|
+
for message in result.messages:
|
|
463
|
+
if message:
|
|
464
|
+
msg = f"{msg}\n{message}" if msg else message
|
|
465
|
+
if result.blocked:
|
|
466
|
+
msg = f"🔴 BLOCK: {msg}"
|
|
467
|
+
return msg
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _forge_violation(input_data: dict, yaml_mod) -> str:
|
|
471
|
+
active = Path.home() / ".arkaos" / "plans" / "active.yaml"
|
|
472
|
+
if not active.is_file():
|
|
473
|
+
return ""
|
|
474
|
+
tool_name = get_str(input_data, "tool_name")
|
|
475
|
+
if tool_name not in ("Edit", "Write"):
|
|
476
|
+
return ""
|
|
477
|
+
try:
|
|
478
|
+
forge_id = active.read_text(encoding="utf-8").strip()
|
|
479
|
+
except OSError:
|
|
480
|
+
return ""
|
|
481
|
+
forge_file = Path.home() / ".arkaos" / "plans" / f"{forge_id}.yaml"
|
|
482
|
+
if not forge_file.is_file():
|
|
483
|
+
return ""
|
|
484
|
+
edited = get_str(input_data, "file_path")
|
|
485
|
+
if not edited:
|
|
486
|
+
return ""
|
|
487
|
+
try:
|
|
488
|
+
plan = yaml_mod.safe_load(forge_file.read_text(encoding="utf-8")) or {}
|
|
489
|
+
except Exception:
|
|
490
|
+
return ""
|
|
491
|
+
if plan.get("status", "") != "executing":
|
|
492
|
+
return ""
|
|
493
|
+
deliverables = [
|
|
494
|
+
d for p in plan.get("plan_phases", []) for d in p.get("deliverables", [])
|
|
495
|
+
]
|
|
496
|
+
if not deliverables:
|
|
497
|
+
return ""
|
|
498
|
+
if any(d in edited or edited.endswith(d) for d in deliverables):
|
|
499
|
+
return ""
|
|
500
|
+
return (
|
|
501
|
+
f"⚠ Forge scope-creep: editing {edited} which is outside forge "
|
|
502
|
+
f"plan deliverables."
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _workflow_sections_with_fallback(
|
|
507
|
+
input_data: dict, root: str, persist: list, msg: str
|
|
508
|
+
) -> str:
|
|
509
|
+
"""In-process when yaml is importable; else delegate ONCE to the venv."""
|
|
510
|
+
try:
|
|
511
|
+
return _run_workflow_sections(input_data, persist, msg)
|
|
512
|
+
except ImportError:
|
|
513
|
+
pass
|
|
514
|
+
except Exception:
|
|
515
|
+
return msg
|
|
516
|
+
venv_py = venv_python()
|
|
517
|
+
if venv_py is None:
|
|
518
|
+
return msg
|
|
519
|
+
payload = json.dumps({
|
|
520
|
+
"input": input_data,
|
|
521
|
+
"persist": persist,
|
|
522
|
+
"violation_msg": msg,
|
|
523
|
+
})
|
|
524
|
+
try:
|
|
525
|
+
proc = subprocess.run(
|
|
526
|
+
[venv_py, "-m", "core.hooks.post_tool_use", "--workflow-sections"],
|
|
527
|
+
input=payload, capture_output=True, text=True, timeout=8,
|
|
528
|
+
env={**os.environ, "PYTHONPATH": root},
|
|
529
|
+
)
|
|
530
|
+
if proc.returncode == 0 and proc.stdout.strip():
|
|
531
|
+
return str(
|
|
532
|
+
json.loads(proc.stdout).get("violation_msg", msg) or msg
|
|
533
|
+
)
|
|
534
|
+
except Exception:
|
|
535
|
+
pass
|
|
536
|
+
return msg
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
# ─── Section 9: cognition capture (detached, fire-and-forget) ───────────
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _enqueue_cognition_capture(session_id: str, tool_output: str) -> None:
|
|
543
|
+
if not session_id or not tool_output:
|
|
544
|
+
return
|
|
545
|
+
repo = repo_path()
|
|
546
|
+
if not repo or not Path(repo).is_dir():
|
|
547
|
+
return
|
|
548
|
+
try:
|
|
549
|
+
proc = subprocess.Popen(
|
|
550
|
+
[sys.executable, "-m", "core.cognition.retrieval", "capture",
|
|
551
|
+
session_id],
|
|
552
|
+
stdin=subprocess.PIPE,
|
|
553
|
+
stdout=subprocess.DEVNULL,
|
|
554
|
+
stderr=subprocess.DEVNULL,
|
|
555
|
+
env={**os.environ, "PYTHONPATH": repo},
|
|
556
|
+
start_new_session=True, # replaces `& disown`
|
|
557
|
+
)
|
|
558
|
+
if proc.stdin is not None:
|
|
559
|
+
try:
|
|
560
|
+
proc.stdin.write(tool_output.encode("utf-8", "replace"))
|
|
561
|
+
proc.stdin.close()
|
|
562
|
+
except (BrokenPipeError, OSError):
|
|
563
|
+
pass
|
|
564
|
+
except Exception:
|
|
565
|
+
pass
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
# ─── Section 10: metrics ─────────────────────────────────────────────────
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _log_metrics(duration_ms: int) -> None:
|
|
572
|
+
metrics_file = Path.home() / ".arkaos" / "hook-metrics.json"
|
|
573
|
+
lock_file = Path.home() / ".arkaos" / "hook-metrics.lock"
|
|
574
|
+
metrics_file.parent.mkdir(parents=True, exist_ok=True)
|
|
575
|
+
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
576
|
+
try:
|
|
577
|
+
with lock_file.open("a", encoding="utf-8") as lock_fh:
|
|
578
|
+
_locked(lock_fh)
|
|
579
|
+
try:
|
|
580
|
+
try:
|
|
581
|
+
entries = json.loads(
|
|
582
|
+
metrics_file.read_text(encoding="utf-8")
|
|
583
|
+
)
|
|
584
|
+
if not isinstance(entries, list):
|
|
585
|
+
entries = []
|
|
586
|
+
except (OSError, json.JSONDecodeError):
|
|
587
|
+
entries = []
|
|
588
|
+
entries.append({
|
|
589
|
+
"hook": "post-tool-use",
|
|
590
|
+
"duration_ms": duration_ms,
|
|
591
|
+
"timestamp": now,
|
|
592
|
+
})
|
|
593
|
+
tmp = metrics_file.with_suffix(".json.tmp")
|
|
594
|
+
tmp.write_text(json.dumps(entries[-500:]), encoding="utf-8")
|
|
595
|
+
tmp.replace(metrics_file)
|
|
596
|
+
finally:
|
|
597
|
+
_unlocked(lock_fh)
|
|
598
|
+
except OSError:
|
|
599
|
+
pass
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
# ─── Entry point ─────────────────────────────────────────────────────────
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def main(stdin_json: dict | None = None) -> int:
|
|
606
|
+
start = time.monotonic()
|
|
607
|
+
if stdin_json is None:
|
|
608
|
+
stdin_json, _ = read_stdin_json()
|
|
609
|
+
root = resolve_arkaos_root()
|
|
610
|
+
ensure_root_on_path(root)
|
|
611
|
+
|
|
612
|
+
tool_name = get_str(stdin_json, "tool_name")
|
|
613
|
+
tool_output = get_str(stdin_json, "tool_output")
|
|
614
|
+
exit_code = get_str(stdin_json, "exit_code") or "0"
|
|
615
|
+
cwd = get_str(stdin_json, "cwd")
|
|
616
|
+
session_id = get_str(stdin_json, "session_id")
|
|
617
|
+
assistant_msg = get_str(stdin_json, "assistant_message")
|
|
618
|
+
|
|
619
|
+
_write_flow_marker(session_id, assistant_msg)
|
|
620
|
+
|
|
621
|
+
if tool_name in ("Task", "Agent"):
|
|
622
|
+
subagent_type = get_str(stdin_json, "tool_input", "subagent_type")
|
|
623
|
+
if subagent_type == "cqo":
|
|
624
|
+
prompt = get_str(stdin_json, "tool_input", "prompt")
|
|
625
|
+
_record_cqo_rejected(tool_output, prompt, session_id)
|
|
626
|
+
_record_pattern_stub(tool_output, prompt)
|
|
627
|
+
_record_activation(subagent_type, session_id)
|
|
628
|
+
|
|
629
|
+
# Only process further if there was an error signal (same early exit
|
|
630
|
+
# as the bash version — violations/metrics only run on error turns).
|
|
631
|
+
if exit_code in ("0", "") and not _ERROR_TRIGGER_RE.search(tool_output):
|
|
632
|
+
print("{}")
|
|
633
|
+
return 0
|
|
634
|
+
|
|
635
|
+
error_line = _extract_error_line(tool_output)
|
|
636
|
+
if not error_line:
|
|
637
|
+
print("{}")
|
|
638
|
+
return 0
|
|
639
|
+
pattern = _normalize_pattern(error_line)
|
|
640
|
+
if not pattern:
|
|
641
|
+
print("{}")
|
|
642
|
+
return 0
|
|
643
|
+
|
|
644
|
+
_store_gotcha(
|
|
645
|
+
pattern, error_line, _categorize(error_line), tool_name,
|
|
646
|
+
_detect_project(cwd), _match_suggestion(error_line),
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
violation_msg, persist = _detect_rule_violations(stdin_json)
|
|
650
|
+
violation_msg = _workflow_sections_with_fallback(
|
|
651
|
+
stdin_json, root, persist, violation_msg
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
_enqueue_cognition_capture(session_id, tool_output)
|
|
655
|
+
_log_metrics(int((time.monotonic() - start) * 1000))
|
|
656
|
+
|
|
657
|
+
if violation_msg:
|
|
658
|
+
print(json.dumps({"additionalContext": violation_msg}))
|
|
659
|
+
else:
|
|
660
|
+
print("{}")
|
|
661
|
+
return 0
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _workflow_sections_main() -> int:
|
|
665
|
+
"""`--workflow-sections` mode — the venv delegate entry."""
|
|
666
|
+
payload, _ = read_stdin_json()
|
|
667
|
+
root = resolve_arkaos_root()
|
|
668
|
+
ensure_root_on_path(root)
|
|
669
|
+
input_data = payload.get("input", {})
|
|
670
|
+
persist = [tuple(item) for item in payload.get("persist", [])]
|
|
671
|
+
msg = str(payload.get("violation_msg", ""))
|
|
672
|
+
try:
|
|
673
|
+
msg = _run_workflow_sections(input_data, persist, msg)
|
|
674
|
+
except Exception:
|
|
675
|
+
pass
|
|
676
|
+
print(json.dumps({"violation_msg": msg}))
|
|
677
|
+
return 0
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
if __name__ == "__main__":
|
|
681
|
+
try:
|
|
682
|
+
if "--workflow-sections" in sys.argv[1:]:
|
|
683
|
+
raise SystemExit(_workflow_sections_main())
|
|
684
|
+
raise SystemExit(main())
|
|
685
|
+
except SystemExit:
|
|
686
|
+
raise
|
|
687
|
+
except Exception:
|
|
688
|
+
print("{}")
|
|
689
|
+
raise SystemExit(0)
|