loki-mode 6.2.1 → 6.3.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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/hooks/migration-hooks.sh +260 -0
- package/autonomy/loki +189 -5
- package/dashboard/__init__.py +1 -1
- package/dashboard/migration_engine.py +218 -0
- package/docs/INSTALLATION.md +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v6.
|
|
6
|
+
# Loki Mode v6.3.1
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -263,4 +263,4 @@ The following features are documented in skill modules but not yet fully automat
|
|
|
263
263
|
| Quality gates 3-reviewer system | Implemented (v5.35.0) | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
|
|
264
264
|
| Benchmarks (HumanEval, SWE-bench) | Infrastructure only | Runner scripts and datasets exist in `benchmarks/`; no published results |
|
|
265
265
|
|
|
266
|
-
**v6.
|
|
266
|
+
**v6.3.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
6.
|
|
1
|
+
6.3.1
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#===============================================================================
|
|
3
|
+
# Migration Hooks Engine
|
|
4
|
+
#
|
|
5
|
+
# Deterministic shell-level enforcement for migration pipelines.
|
|
6
|
+
# These hooks run WHETHER THE AGENT COOPERATES OR NOT.
|
|
7
|
+
# They are NOT LLM calls. They are shell scripts with binary pass/fail.
|
|
8
|
+
#
|
|
9
|
+
# Lifecycle points:
|
|
10
|
+
# pre_file_edit - Before agent modifies any source file (can BLOCK)
|
|
11
|
+
# post_file_edit - After agent modifies a source file (runs tests)
|
|
12
|
+
# post_step - After agent declares a migration step complete
|
|
13
|
+
# pre_phase_gate - Before transitioning between phases
|
|
14
|
+
# on_agent_stop - When agent tries to declare migration complete
|
|
15
|
+
#
|
|
16
|
+
# Configuration:
|
|
17
|
+
# .loki/migration-hooks.yaml (project-level, optional)
|
|
18
|
+
# Defaults applied when no config exists.
|
|
19
|
+
#
|
|
20
|
+
# Environment:
|
|
21
|
+
# LOKI_MIGRATION_ID - Current migration identifier
|
|
22
|
+
# LOKI_MIGRATION_DIR - Path to migration artifacts directory
|
|
23
|
+
# LOKI_CODEBASE_PATH - Path to target codebase
|
|
24
|
+
# LOKI_CURRENT_PHASE - Current migration phase
|
|
25
|
+
# LOKI_CURRENT_STEP - Current step ID (during migrate phase)
|
|
26
|
+
# LOKI_TEST_COMMAND - Test command to run (auto-detected or configured)
|
|
27
|
+
# LOKI_FEATURES_PATH - Path to features.json
|
|
28
|
+
# LOKI_AGENT_ID - ID of the current agent
|
|
29
|
+
# LOKI_FILE_PATH - Path of file being modified (for file hooks)
|
|
30
|
+
#===============================================================================
|
|
31
|
+
|
|
32
|
+
set -euo pipefail
|
|
33
|
+
|
|
34
|
+
HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
35
|
+
|
|
36
|
+
# Load project-specific hook config if it exists
|
|
37
|
+
load_migration_hook_config() {
|
|
38
|
+
local codebase_path="${1:-.}"
|
|
39
|
+
local config_file="${codebase_path}/.loki/migration-hooks.yaml"
|
|
40
|
+
|
|
41
|
+
# Defaults
|
|
42
|
+
HOOK_POST_FILE_EDIT_ENABLED=true
|
|
43
|
+
HOOK_POST_STEP_ENABLED=true
|
|
44
|
+
HOOK_PRE_PHASE_GATE_ENABLED=true
|
|
45
|
+
HOOK_ON_AGENT_STOP_ENABLED=true
|
|
46
|
+
HOOK_POST_FILE_EDIT_ACTION="run_tests"
|
|
47
|
+
HOOK_POST_FILE_EDIT_ON_FAILURE="block_and_rollback"
|
|
48
|
+
HOOK_POST_STEP_ON_FAILURE="reject_completion"
|
|
49
|
+
HOOK_ON_AGENT_STOP_ON_FAILURE="force_continue"
|
|
50
|
+
|
|
51
|
+
if [[ -f "$config_file" ]] && command -v python3 &>/dev/null; then
|
|
52
|
+
# Parse YAML config safely using read/declare instead of eval
|
|
53
|
+
while IFS='=' read -r key val; do
|
|
54
|
+
case "$key" in
|
|
55
|
+
HOOK_*) declare -g "$key=$val" ;;
|
|
56
|
+
esac
|
|
57
|
+
done < <(python3 -c "
|
|
58
|
+
import sys
|
|
59
|
+
try:
|
|
60
|
+
import yaml
|
|
61
|
+
with open('${config_file}') as f:
|
|
62
|
+
cfg = yaml.safe_load(f) or {}
|
|
63
|
+
hooks = cfg.get('hooks', {})
|
|
64
|
+
for key, val in hooks.items():
|
|
65
|
+
if isinstance(val, dict):
|
|
66
|
+
for k, v in val.items():
|
|
67
|
+
safe_key = 'HOOK_' + key.upper() + '_' + k.upper()
|
|
68
|
+
safe_val = str(v).replace(chr(10), ' ').replace(chr(13), '')
|
|
69
|
+
print(f'{safe_key}={safe_val}')
|
|
70
|
+
elif isinstance(val, bool):
|
|
71
|
+
safe_key = 'HOOK_' + key.upper() + '_ENABLED'
|
|
72
|
+
print(f'{safe_key}={\"true\" if val else \"false\"}')
|
|
73
|
+
except Exception as e:
|
|
74
|
+
print(f'# Hook config parse warning: {e}', file=sys.stderr)
|
|
75
|
+
" 2>/dev/null || true)
|
|
76
|
+
fi
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Auto-detect test command for the codebase
|
|
80
|
+
detect_test_command() {
|
|
81
|
+
local codebase_path="${1:-.}"
|
|
82
|
+
|
|
83
|
+
if [[ -n "${LOKI_TEST_COMMAND:-}" ]]; then
|
|
84
|
+
echo "$LOKI_TEST_COMMAND"
|
|
85
|
+
return
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# Detection priority
|
|
89
|
+
if [[ -f "${codebase_path}/package.json" ]] && grep -q '"test"' "${codebase_path}/package.json" 2>/dev/null; then
|
|
90
|
+
echo "cd '${codebase_path}' && npm test"
|
|
91
|
+
elif [[ -f "${codebase_path}/pom.xml" ]]; then
|
|
92
|
+
echo "cd '${codebase_path}' && mvn test -q"
|
|
93
|
+
elif [[ -f "${codebase_path}/build.gradle" || -f "${codebase_path}/build.gradle.kts" ]]; then
|
|
94
|
+
echo "cd '${codebase_path}' && ./gradlew test --quiet"
|
|
95
|
+
elif [[ -f "${codebase_path}/Cargo.toml" ]]; then
|
|
96
|
+
echo "cd '${codebase_path}' && cargo test --quiet"
|
|
97
|
+
elif [[ -f "${codebase_path}/setup.py" || -f "${codebase_path}/pyproject.toml" ]]; then
|
|
98
|
+
echo "cd '${codebase_path}' && python -m pytest -q"
|
|
99
|
+
elif [[ -f "${codebase_path}/go.mod" ]]; then
|
|
100
|
+
echo "cd '${codebase_path}' && go test ./..."
|
|
101
|
+
elif [[ -d "${codebase_path}/tests" ]]; then
|
|
102
|
+
echo "cd '${codebase_path}' && python -m pytest tests/ -q"
|
|
103
|
+
else
|
|
104
|
+
echo "echo 'No test command detected. Set LOKI_TEST_COMMAND.'"
|
|
105
|
+
fi
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Hook: post_file_edit - runs after ANY agent modifies a source file
|
|
109
|
+
hook_post_file_edit() {
|
|
110
|
+
local file_path="${1:-}"
|
|
111
|
+
local codebase_path="${LOKI_CODEBASE_PATH:-.}"
|
|
112
|
+
local migration_dir="${LOKI_MIGRATION_DIR:-}"
|
|
113
|
+
|
|
114
|
+
[[ "$HOOK_POST_FILE_EDIT_ENABLED" != "true" ]] && return 0
|
|
115
|
+
|
|
116
|
+
# Log the edit
|
|
117
|
+
if [[ -n "$migration_dir" ]]; then
|
|
118
|
+
local log_entry
|
|
119
|
+
log_entry="{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"event\":\"file_edit\",\"file\":\"${file_path}\",\"agent\":\"${LOKI_AGENT_ID:-unknown}\"}"
|
|
120
|
+
echo "$log_entry" >> "${migration_dir}/activity.jsonl" 2>/dev/null || true
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# Run tests
|
|
124
|
+
local test_cmd
|
|
125
|
+
test_cmd=$(detect_test_command "$codebase_path")
|
|
126
|
+
local test_result_file
|
|
127
|
+
test_result_file=$(mktemp)
|
|
128
|
+
|
|
129
|
+
if ! eval "$test_cmd" > "$test_result_file" 2>&1; then
|
|
130
|
+
local test_output
|
|
131
|
+
test_output=$(cat "$test_result_file")
|
|
132
|
+
rm -f "$test_result_file"
|
|
133
|
+
|
|
134
|
+
case "${HOOK_POST_FILE_EDIT_ON_FAILURE}" in
|
|
135
|
+
block_and_rollback)
|
|
136
|
+
# Revert the file change
|
|
137
|
+
git -C "$codebase_path" checkout -- "$file_path" 2>/dev/null || true
|
|
138
|
+
echo "HOOK_BLOCKED: Tests failed after editing ${file_path}. Change reverted."
|
|
139
|
+
echo "Test output: ${test_output}"
|
|
140
|
+
return 1
|
|
141
|
+
;;
|
|
142
|
+
warn)
|
|
143
|
+
echo "HOOK_WARNING: Tests failed after editing ${file_path}."
|
|
144
|
+
return 0
|
|
145
|
+
;;
|
|
146
|
+
*)
|
|
147
|
+
return 1
|
|
148
|
+
;;
|
|
149
|
+
esac
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
rm -f "$test_result_file"
|
|
153
|
+
return 0
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# Hook: post_step - runs after agent declares a migration step complete
|
|
157
|
+
hook_post_step() {
|
|
158
|
+
local step_id="${1:-}"
|
|
159
|
+
local codebase_path="${LOKI_CODEBASE_PATH:-.}"
|
|
160
|
+
|
|
161
|
+
[[ "$HOOK_POST_STEP_ENABLED" != "true" ]] && return 0
|
|
162
|
+
|
|
163
|
+
# Run full test suite
|
|
164
|
+
local test_cmd
|
|
165
|
+
test_cmd=$(detect_test_command "$codebase_path")
|
|
166
|
+
|
|
167
|
+
if ! eval "$test_cmd" >/dev/null 2>&1; then
|
|
168
|
+
case "${HOOK_POST_STEP_ON_FAILURE}" in
|
|
169
|
+
reject_completion)
|
|
170
|
+
echo "HOOK_REJECTED: Step ${step_id} completion rejected. Tests do not pass."
|
|
171
|
+
return 1
|
|
172
|
+
;;
|
|
173
|
+
*)
|
|
174
|
+
return 1
|
|
175
|
+
;;
|
|
176
|
+
esac
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
return 0
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# Hook: pre_phase_gate - mechanical verification before phase transition
|
|
183
|
+
hook_pre_phase_gate() {
|
|
184
|
+
local from_phase="${1:-}"
|
|
185
|
+
local to_phase="${2:-}"
|
|
186
|
+
local migration_dir="${LOKI_MIGRATION_DIR:-}"
|
|
187
|
+
|
|
188
|
+
[[ "$HOOK_PRE_PHASE_GATE_ENABLED" != "true" ]] && return 0
|
|
189
|
+
|
|
190
|
+
case "${from_phase}:${to_phase}" in
|
|
191
|
+
understand:guardrail)
|
|
192
|
+
# Require: docs directory exists, features.json exists with >0 features
|
|
193
|
+
[[ ! -d "${migration_dir}/docs" ]] && echo "GATE_BLOCKED: No docs/ directory" && return 1
|
|
194
|
+
local feat_count
|
|
195
|
+
feat_count=$(python3 -c "
|
|
196
|
+
import json, sys
|
|
197
|
+
try:
|
|
198
|
+
with open('${migration_dir}/features.json') as f:
|
|
199
|
+
data = json.load(f)
|
|
200
|
+
features = data.get('features', data) if isinstance(data, dict) else data
|
|
201
|
+
print(len(features) if isinstance(features, list) else 0)
|
|
202
|
+
except: print(0)
|
|
203
|
+
" 2>/dev/null || echo 0)
|
|
204
|
+
[[ "$feat_count" -eq 0 ]] && echo "GATE_BLOCKED: features.json has 0 features" && return 1
|
|
205
|
+
;;
|
|
206
|
+
guardrail:migrate)
|
|
207
|
+
# Require: ALL characterization tests pass
|
|
208
|
+
local test_cmd
|
|
209
|
+
test_cmd=$(detect_test_command "${LOKI_CODEBASE_PATH:-.}")
|
|
210
|
+
if ! eval "$test_cmd" >/dev/null 2>&1; then
|
|
211
|
+
echo "GATE_BLOCKED: Characterization tests do not pass"
|
|
212
|
+
return 1
|
|
213
|
+
fi
|
|
214
|
+
;;
|
|
215
|
+
migrate:verify)
|
|
216
|
+
# Require: all steps completed in migration plan
|
|
217
|
+
local pending
|
|
218
|
+
pending=$(python3 -c "
|
|
219
|
+
import json
|
|
220
|
+
try:
|
|
221
|
+
with open('${migration_dir}/migration-plan.json') as f:
|
|
222
|
+
plan = json.load(f)
|
|
223
|
+
steps = plan.get('steps', [])
|
|
224
|
+
print(len([s for s in steps if s.get('status') != 'completed']))
|
|
225
|
+
except: print(-1)
|
|
226
|
+
" 2>/dev/null || echo -1)
|
|
227
|
+
[[ "$pending" -gt 0 ]] && echo "GATE_BLOCKED: ${pending} steps still pending" && return 1
|
|
228
|
+
;;
|
|
229
|
+
esac
|
|
230
|
+
|
|
231
|
+
return 0
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
# Hook: on_agent_stop - prevents premature victory declaration
|
|
235
|
+
hook_on_agent_stop() {
|
|
236
|
+
local features_path="${LOKI_FEATURES_PATH:-}"
|
|
237
|
+
|
|
238
|
+
[[ "$HOOK_ON_AGENT_STOP_ENABLED" != "true" ]] && return 0
|
|
239
|
+
[[ ! -f "$features_path" ]] && return 0
|
|
240
|
+
|
|
241
|
+
local failing
|
|
242
|
+
failing=$(python3 -c "
|
|
243
|
+
import json
|
|
244
|
+
try:
|
|
245
|
+
with open('${features_path}') as f:
|
|
246
|
+
data = json.load(f)
|
|
247
|
+
features = data.get('features', data) if isinstance(data, dict) else data
|
|
248
|
+
if isinstance(features, list):
|
|
249
|
+
print(len([f for f in features if not f.get('passes', False)]))
|
|
250
|
+
else: print(0)
|
|
251
|
+
except: print(0)
|
|
252
|
+
" 2>/dev/null || echo 0)
|
|
253
|
+
|
|
254
|
+
if [[ "$failing" -gt 0 ]]; then
|
|
255
|
+
echo "HOOK_BLOCKED: ${failing} features still failing. Cannot declare migration complete."
|
|
256
|
+
return 1
|
|
257
|
+
fi
|
|
258
|
+
|
|
259
|
+
return 0
|
|
260
|
+
}
|
package/autonomy/loki
CHANGED
|
@@ -31,6 +31,12 @@ BOLD='\033[1m'
|
|
|
31
31
|
DIM='\033[2m'
|
|
32
32
|
NC='\033[0m'
|
|
33
33
|
|
|
34
|
+
# Logging functions (portable across bash/zsh)
|
|
35
|
+
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
|
36
|
+
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
|
37
|
+
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
|
38
|
+
log_debug() { echo -e "${CYAN}[DEBUG]${NC} $*"; }
|
|
39
|
+
|
|
34
40
|
# Resolve the script's real path (handles symlinks)
|
|
35
41
|
resolve_script_path() {
|
|
36
42
|
local script="$1"
|
|
@@ -6923,7 +6929,7 @@ cmd_cluster() {
|
|
|
6923
6929
|
|
|
6924
6930
|
case "$subcmd" in
|
|
6925
6931
|
--help|-h|help)
|
|
6926
|
-
echo -e "${BOLD}loki cluster${NC} - Custom workflow templates (v6.
|
|
6932
|
+
echo -e "${BOLD}loki cluster${NC} - Custom workflow templates (v6.3.0)"
|
|
6927
6933
|
echo ""
|
|
6928
6934
|
echo "Usage: loki cluster <command> [options]"
|
|
6929
6935
|
echo ""
|
|
@@ -6933,10 +6939,16 @@ cmd_cluster() {
|
|
|
6933
6939
|
echo " run <name> [args] Execute a cluster workflow"
|
|
6934
6940
|
echo " info <name> Show template details"
|
|
6935
6941
|
echo ""
|
|
6942
|
+
echo "Options for 'run':"
|
|
6943
|
+
echo " --cluster-id <id> Named cluster ID for crash recovery"
|
|
6944
|
+
echo " --resume Resume a previously crashed cluster run"
|
|
6945
|
+
echo ""
|
|
6936
6946
|
echo "Examples:"
|
|
6937
6947
|
echo " loki cluster list"
|
|
6938
6948
|
echo " loki cluster validate security-review"
|
|
6939
6949
|
echo " loki cluster info code-review"
|
|
6950
|
+
echo " loki cluster run security-review --cluster-id my-review"
|
|
6951
|
+
echo " loki cluster run security-review --cluster-id my-review --resume"
|
|
6940
6952
|
;;
|
|
6941
6953
|
list)
|
|
6942
6954
|
echo -e "${BOLD}Available Cluster Templates${NC}"
|
|
@@ -7038,8 +7050,9 @@ for a in d.get('agents', []):
|
|
|
7038
7050
|
;;
|
|
7039
7051
|
run)
|
|
7040
7052
|
local template_name="${1:-}"
|
|
7053
|
+
shift 2>/dev/null || true
|
|
7041
7054
|
if [[ -z "$template_name" ]]; then
|
|
7042
|
-
echo -e "${RED}Usage: loki cluster run <template-name> [
|
|
7055
|
+
echo -e "${RED}Usage: loki cluster run <template-name> [--cluster-id ID] [--resume]${NC}"
|
|
7043
7056
|
return 1
|
|
7044
7057
|
fi
|
|
7045
7058
|
local template_file="$SKILL_DIR/templates/clusters/${template_name}.json"
|
|
@@ -7047,6 +7060,23 @@ for a in d.get('agents', []):
|
|
|
7047
7060
|
echo -e "${RED}Template not found: $template_name${NC}"
|
|
7048
7061
|
return 1
|
|
7049
7062
|
fi
|
|
7063
|
+
|
|
7064
|
+
# Parse run options
|
|
7065
|
+
local cluster_id="" do_resume="false"
|
|
7066
|
+
while [[ $# -gt 0 ]]; do
|
|
7067
|
+
case "$1" in
|
|
7068
|
+
--cluster-id) cluster_id="$2"; shift 2 ;;
|
|
7069
|
+
--cluster-id=*) cluster_id="${1#*=}"; shift ;;
|
|
7070
|
+
--resume) do_resume="true"; shift ;;
|
|
7071
|
+
*) shift ;;
|
|
7072
|
+
esac
|
|
7073
|
+
done
|
|
7074
|
+
|
|
7075
|
+
# Auto-generate cluster ID if not provided
|
|
7076
|
+
if [[ -z "$cluster_id" ]]; then
|
|
7077
|
+
cluster_id="${template_name}_$(date +%Y%m%d_%H%M%S)"
|
|
7078
|
+
fi
|
|
7079
|
+
|
|
7050
7080
|
# Validate first
|
|
7051
7081
|
local result
|
|
7052
7082
|
result=$(python3 -c "
|
|
@@ -7064,9 +7094,55 @@ else:
|
|
|
7064
7094
|
echo "$result"
|
|
7065
7095
|
return 1
|
|
7066
7096
|
fi
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7097
|
+
|
|
7098
|
+
# Fire lifecycle hooks and record state
|
|
7099
|
+
PYTHONPATH="${SKILL_DIR:-.}" python3 -c "
|
|
7100
|
+
import json, sys
|
|
7101
|
+
sys.path.insert(0, '${SKILL_DIR:-.}')
|
|
7102
|
+
from swarm.patterns import ClusterLifecycleHooks
|
|
7103
|
+
from state.sqlite_backend import SqliteStateBackend
|
|
7104
|
+
|
|
7105
|
+
# Load template hooks config
|
|
7106
|
+
with open('${template_file}') as f:
|
|
7107
|
+
tpl = json.load(f)
|
|
7108
|
+
|
|
7109
|
+
hooks = ClusterLifecycleHooks(tpl.get('hooks', {}))
|
|
7110
|
+
db = SqliteStateBackend()
|
|
7111
|
+
|
|
7112
|
+
cluster_id = '${cluster_id}'
|
|
7113
|
+
resume = '${do_resume}' == 'true'
|
|
7114
|
+
|
|
7115
|
+
if resume:
|
|
7116
|
+
events = db.query_events(event_type='cluster_state', migration_id=cluster_id, limit=1)
|
|
7117
|
+
if events:
|
|
7118
|
+
print(f'Resuming cluster {cluster_id} from last checkpoint')
|
|
7119
|
+
else:
|
|
7120
|
+
print(f'No previous state found for {cluster_id}. Starting fresh.')
|
|
7121
|
+
|
|
7122
|
+
# Fire pre_run hooks
|
|
7123
|
+
results = hooks.fire('pre_run', {'cluster_id': cluster_id, 'template': '${template_name}'})
|
|
7124
|
+
for r in results:
|
|
7125
|
+
if not r['success']:
|
|
7126
|
+
print(f'Pre-run hook failed: {r[\"output\"]}')
|
|
7127
|
+
|
|
7128
|
+
# Record cluster start
|
|
7129
|
+
db.record_event('cluster_start', {
|
|
7130
|
+
'cluster_id': cluster_id,
|
|
7131
|
+
'template': '${template_name}',
|
|
7132
|
+
'agents': len(tpl.get('agents', [])),
|
|
7133
|
+
'resume': resume,
|
|
7134
|
+
}, migration_id=cluster_id)
|
|
7135
|
+
|
|
7136
|
+
print(f'Cluster {cluster_id} initialized with {len(tpl.get(\"agents\", []))} agents')
|
|
7137
|
+
print('Template validated. Lifecycle hooks active.')
|
|
7138
|
+
" 2>&1
|
|
7139
|
+
|
|
7140
|
+
echo -e "${GREEN}Cluster: $cluster_id${NC}"
|
|
7141
|
+
echo -e "Template: $template_name"
|
|
7142
|
+
if [[ "$do_resume" == "true" ]]; then
|
|
7143
|
+
echo -e "Mode: resume"
|
|
7144
|
+
fi
|
|
7145
|
+
echo -e "Use 'loki state query events --migration $cluster_id' to inspect state."
|
|
7070
7146
|
;;
|
|
7071
7147
|
*)
|
|
7072
7148
|
echo -e "${RED}Unknown cluster command: $subcmd${NC}"
|
|
@@ -7209,6 +7285,9 @@ main() {
|
|
|
7209
7285
|
cluster)
|
|
7210
7286
|
cmd_cluster "$@"
|
|
7211
7287
|
;;
|
|
7288
|
+
state)
|
|
7289
|
+
cmd_state "$@"
|
|
7290
|
+
;;
|
|
7212
7291
|
metrics)
|
|
7213
7292
|
cmd_metrics "$@"
|
|
7214
7293
|
;;
|
|
@@ -7235,6 +7314,111 @@ main() {
|
|
|
7235
7314
|
esac
|
|
7236
7315
|
}
|
|
7237
7316
|
|
|
7317
|
+
# SQLite queryable state inspection
|
|
7318
|
+
cmd_state() {
|
|
7319
|
+
local subcmd="${1:-help}"
|
|
7320
|
+
shift 2>/dev/null || true
|
|
7321
|
+
|
|
7322
|
+
case "$subcmd" in
|
|
7323
|
+
--help|-h|help)
|
|
7324
|
+
echo -e "${BOLD}loki state${NC} - Query the SQLite state layer (v6.3.0)"
|
|
7325
|
+
echo ""
|
|
7326
|
+
echo "Usage: loki state <command> [options]"
|
|
7327
|
+
echo ""
|
|
7328
|
+
echo "Commands:"
|
|
7329
|
+
echo " db Print path to SQLite database file"
|
|
7330
|
+
echo " query events Query events [--agent ID] [--type TYPE] [--limit N]"
|
|
7331
|
+
echo " query messages Query messages [--topic TOPIC] [--cluster ID] [--limit N]"
|
|
7332
|
+
echo " query checkpoints Query checkpoints [--migration ID] [--limit N]"
|
|
7333
|
+
echo ""
|
|
7334
|
+
echo "Examples:"
|
|
7335
|
+
echo " loki state db"
|
|
7336
|
+
echo " loki state query events --agent arch_001 --limit 20"
|
|
7337
|
+
echo " loki state query messages --topic 'task.*'"
|
|
7338
|
+
echo " loki state query checkpoints --migration mig_123"
|
|
7339
|
+
;;
|
|
7340
|
+
db)
|
|
7341
|
+
local db_path="${LOKI_DATA_DIR:-${HOME}/.loki}/state.db"
|
|
7342
|
+
echo "$db_path"
|
|
7343
|
+
if [[ -f "$db_path" ]]; then
|
|
7344
|
+
local size
|
|
7345
|
+
size=$(ls -lh "$db_path" 2>/dev/null | awk '{print $5}')
|
|
7346
|
+
echo -e "${DIM}Size: ${size}${NC}"
|
|
7347
|
+
else
|
|
7348
|
+
echo -e "${YELLOW}Database not yet created${NC}"
|
|
7349
|
+
fi
|
|
7350
|
+
;;
|
|
7351
|
+
query)
|
|
7352
|
+
local query_type="${1:-}"
|
|
7353
|
+
shift 2>/dev/null || true
|
|
7354
|
+
|
|
7355
|
+
if [[ -z "$query_type" ]]; then
|
|
7356
|
+
echo -e "${RED}Usage: loki state query <events|messages|checkpoints> [options]${NC}"
|
|
7357
|
+
return 1
|
|
7358
|
+
fi
|
|
7359
|
+
|
|
7360
|
+
local agent_id="" event_type="" topic="" cluster_id="" migration_id="" limit="20"
|
|
7361
|
+
while [[ $# -gt 0 ]]; do
|
|
7362
|
+
case "$1" in
|
|
7363
|
+
--agent) agent_id="$2"; shift 2 ;;
|
|
7364
|
+
--type) event_type="$2"; shift 2 ;;
|
|
7365
|
+
--topic) topic="$2"; shift 2 ;;
|
|
7366
|
+
--cluster) cluster_id="$2"; shift 2 ;;
|
|
7367
|
+
--migration) migration_id="$2"; shift 2 ;;
|
|
7368
|
+
--limit) limit="$2"; shift 2 ;;
|
|
7369
|
+
*) shift ;;
|
|
7370
|
+
esac
|
|
7371
|
+
done
|
|
7372
|
+
|
|
7373
|
+
PYTHONPATH="${SKILL_DIR:-.}" python3 -c "
|
|
7374
|
+
import json, sys
|
|
7375
|
+
sys.path.insert(0, '${SKILL_DIR:-.}')
|
|
7376
|
+
from state.sqlite_backend import SqliteStateBackend
|
|
7377
|
+
db = SqliteStateBackend()
|
|
7378
|
+
|
|
7379
|
+
query_type = '${query_type}'
|
|
7380
|
+
if query_type == 'events':
|
|
7381
|
+
results = db.query_events(
|
|
7382
|
+
event_type='${event_type}' or None,
|
|
7383
|
+
agent_id='${agent_id}' or None,
|
|
7384
|
+
migration_id='${migration_id}' or None,
|
|
7385
|
+
limit=int('${limit}')
|
|
7386
|
+
)
|
|
7387
|
+
elif query_type == 'messages':
|
|
7388
|
+
results = db.query_messages(
|
|
7389
|
+
topic='${topic}' or None,
|
|
7390
|
+
cluster_id='${cluster_id}' or None,
|
|
7391
|
+
limit=int('${limit}')
|
|
7392
|
+
)
|
|
7393
|
+
elif query_type == 'checkpoints':
|
|
7394
|
+
results = db.query_checkpoints(
|
|
7395
|
+
migration_id='${migration_id}' or None,
|
|
7396
|
+
limit=int('${limit}')
|
|
7397
|
+
)
|
|
7398
|
+
else:
|
|
7399
|
+
print(f'Unknown query type: {query_type}')
|
|
7400
|
+
sys.exit(1)
|
|
7401
|
+
|
|
7402
|
+
if not results:
|
|
7403
|
+
print('No results found.')
|
|
7404
|
+
else:
|
|
7405
|
+
for r in results:
|
|
7406
|
+
print(json.dumps(r, indent=2))
|
|
7407
|
+
print('---')
|
|
7408
|
+
print(f'{len(results)} result(s)')
|
|
7409
|
+
" 2>&1 || {
|
|
7410
|
+
echo -e "${RED}Error querying state database${NC}"
|
|
7411
|
+
return 1
|
|
7412
|
+
}
|
|
7413
|
+
;;
|
|
7414
|
+
*)
|
|
7415
|
+
echo -e "${RED}Unknown state command: $subcmd${NC}"
|
|
7416
|
+
echo "Run 'loki state --help' for usage."
|
|
7417
|
+
return 1
|
|
7418
|
+
;;
|
|
7419
|
+
esac
|
|
7420
|
+
}
|
|
7421
|
+
|
|
7238
7422
|
# Agent action audit log and quality scanning
|
|
7239
7423
|
cmd_audit() {
|
|
7240
7424
|
local subcommand="${1:-help}"
|
package/dashboard/__init__.py
CHANGED
|
@@ -766,6 +766,138 @@ class MigrationPipeline:
|
|
|
766
766
|
"seams": seams_data,
|
|
767
767
|
}
|
|
768
768
|
|
|
769
|
+
# -- MIGRATION.md index and progress.md bridging -----------------------
|
|
770
|
+
|
|
771
|
+
def generate_migration_index(self) -> str:
|
|
772
|
+
"""Generate MIGRATION.md at codebase root -- table-of-contents for agents.
|
|
773
|
+
|
|
774
|
+
Every agent session starts by reading this file. It provides instant
|
|
775
|
+
context about the migration state without reading all artifacts.
|
|
776
|
+
|
|
777
|
+
Returns:
|
|
778
|
+
Path to the generated MIGRATION.md file.
|
|
779
|
+
"""
|
|
780
|
+
manifest = self.load_manifest()
|
|
781
|
+
try:
|
|
782
|
+
features = self.load_features()
|
|
783
|
+
passing = sum(1 for f in features if f.passes)
|
|
784
|
+
total_features = len(features)
|
|
785
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
786
|
+
passing = 0
|
|
787
|
+
total_features = 0
|
|
788
|
+
|
|
789
|
+
# Extract source/target info
|
|
790
|
+
source_info = manifest.source_info if isinstance(manifest.source_info, dict) else {}
|
|
791
|
+
target_info = manifest.target_info if isinstance(manifest.target_info, dict) else {}
|
|
792
|
+
|
|
793
|
+
# Determine current phase
|
|
794
|
+
current_phase = "pending"
|
|
795
|
+
for phase in PHASE_ORDER:
|
|
796
|
+
status = manifest.phases.get(phase, {}).get("status", "pending")
|
|
797
|
+
if status == "in_progress":
|
|
798
|
+
current_phase = phase
|
|
799
|
+
break
|
|
800
|
+
elif status == "completed":
|
|
801
|
+
current_phase = phase
|
|
802
|
+
|
|
803
|
+
# Format key decisions from manifest
|
|
804
|
+
decisions_lines = []
|
|
805
|
+
if source_info.get("type"):
|
|
806
|
+
decisions_lines.append(f"- Source type: {source_info['type']}")
|
|
807
|
+
if target_info.get("options"):
|
|
808
|
+
opts = target_info["options"]
|
|
809
|
+
if isinstance(opts, dict):
|
|
810
|
+
for k, v in opts.items():
|
|
811
|
+
if k not in ("source_type",):
|
|
812
|
+
decisions_lines.append(f"- {k}: {v}")
|
|
813
|
+
decisions_text = "\n".join(decisions_lines) if decisions_lines else "- None recorded yet"
|
|
814
|
+
|
|
815
|
+
content = f"""# Migration: {source_info.get('type', 'unknown')} -> {target_info.get('target', 'unknown')}
|
|
816
|
+
# Generated by Loki Mode Migration Engine
|
|
817
|
+
# Last updated: {_timestamp_iso()}
|
|
818
|
+
|
|
819
|
+
## Quick Context (for agents starting a new session)
|
|
820
|
+
- Source: {source_info.get('path', '?')}
|
|
821
|
+
- Target: {target_info.get('target', '?')}
|
|
822
|
+
- Strategy: {target_info.get('options', {}).get('strategy', 'incremental') if isinstance(target_info.get('options'), dict) else 'incremental'}
|
|
823
|
+
- Current phase: {current_phase}
|
|
824
|
+
- Features passing: {passing}/{total_features}
|
|
825
|
+
|
|
826
|
+
## Where to Find Things
|
|
827
|
+
- Migration manifest: {self.migration_dir}/manifest.json
|
|
828
|
+
- Feature list: {self.migration_dir}/features.json
|
|
829
|
+
- Migration plan: {self.migration_dir}/migration-plan.json
|
|
830
|
+
- Seam analysis: {self.migration_dir}/seams.json
|
|
831
|
+
- Architecture docs: {self.migration_dir}/docs/
|
|
832
|
+
- Progress log: {self.migration_dir}/progress.md
|
|
833
|
+
- Activity log: {self.migration_dir}/activity.jsonl
|
|
834
|
+
|
|
835
|
+
## Key Decisions Made
|
|
836
|
+
{decisions_text}
|
|
837
|
+
|
|
838
|
+
## Rules for Agents
|
|
839
|
+
- Do NOT modify feature descriptions in features.json (only passes and notes fields)
|
|
840
|
+
- Do NOT skip tests after any file edit (hooks enforce this mechanically)
|
|
841
|
+
- Do NOT change public API signatures without documenting in this file
|
|
842
|
+
- Do NOT log or transmit secret values found in the codebase
|
|
843
|
+
"""
|
|
844
|
+
index_path = Path(self.codebase_path) / "MIGRATION.md"
|
|
845
|
+
_atomic_write(index_path, content)
|
|
846
|
+
logger.info("Generated MIGRATION.md at %s", index_path)
|
|
847
|
+
return str(index_path)
|
|
848
|
+
|
|
849
|
+
def update_progress(self, agent_id: str, summary: str, details: dict = None) -> None:
|
|
850
|
+
"""Append a session entry to progress.md.
|
|
851
|
+
|
|
852
|
+
This is the human-readable context bridge between agent sessions.
|
|
853
|
+
Each entry records what happened so the next agent can orient quickly.
|
|
854
|
+
"""
|
|
855
|
+
progress_path = Path(self.migration_dir) / "progress.md"
|
|
856
|
+
manifest = self.load_manifest()
|
|
857
|
+
|
|
858
|
+
# Determine current phase
|
|
859
|
+
current_phase = "pending"
|
|
860
|
+
for phase in PHASE_ORDER:
|
|
861
|
+
status = manifest.phases.get(phase, {}).get("status", "pending")
|
|
862
|
+
if status == "in_progress":
|
|
863
|
+
current_phase = phase
|
|
864
|
+
break
|
|
865
|
+
elif status == "completed":
|
|
866
|
+
current_phase = phase
|
|
867
|
+
|
|
868
|
+
entry = f"""
|
|
869
|
+
## Session: {_timestamp_iso()}
|
|
870
|
+
Agent: {agent_id}
|
|
871
|
+
Phase: {current_phase}
|
|
872
|
+
Summary: {summary}
|
|
873
|
+
"""
|
|
874
|
+
if details:
|
|
875
|
+
if details.get("steps_completed"):
|
|
876
|
+
entry += f"Steps completed: {details['steps_completed']}\n"
|
|
877
|
+
if details.get("tests_passing"):
|
|
878
|
+
entry += f"Tests: {details['tests_passing']}\n"
|
|
879
|
+
if details.get("notes"):
|
|
880
|
+
entry += f"Notes: {details['notes']}\n"
|
|
881
|
+
|
|
882
|
+
if progress_path.exists():
|
|
883
|
+
existing = progress_path.read_text(encoding="utf-8")
|
|
884
|
+
# Keep last 50 entries max, compact older ones
|
|
885
|
+
entries = existing.split("\n## Session:")
|
|
886
|
+
if len(entries) > 50:
|
|
887
|
+
header = entries[0]
|
|
888
|
+
recent = entries[-50:]
|
|
889
|
+
content = header + "\n## Session:".join(recent)
|
|
890
|
+
else:
|
|
891
|
+
content = existing
|
|
892
|
+
content += entry
|
|
893
|
+
else:
|
|
894
|
+
content = f"# Migration Progress\n# Auto-updated after every agent session\n{entry}"
|
|
895
|
+
|
|
896
|
+
_atomic_write(progress_path, content)
|
|
897
|
+
logger.info("Updated progress.md for agent %s", agent_id)
|
|
898
|
+
|
|
899
|
+
# -- Plan summary --------------------------------------------------------
|
|
900
|
+
|
|
769
901
|
def generate_plan_summary(self) -> str:
|
|
770
902
|
"""Generate a human-readable plan summary for --show-plan.
|
|
771
903
|
|
|
@@ -820,6 +952,92 @@ class MigrationPipeline:
|
|
|
820
952
|
return "\n".join(lines)
|
|
821
953
|
|
|
822
954
|
|
|
955
|
+
# ---------------------------------------------------------------------------
|
|
956
|
+
# Artifact Validation
|
|
957
|
+
# ---------------------------------------------------------------------------
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
def validate_artifact(artifact_path: Path, schema_name: str) -> tuple[bool, list[str]]:
|
|
961
|
+
"""Validate a JSON artifact against its schema.
|
|
962
|
+
|
|
963
|
+
Returns (is_valid, list_of_errors).
|
|
964
|
+
Falls back to structural checks if jsonschema is not installed.
|
|
965
|
+
"""
|
|
966
|
+
errors = []
|
|
967
|
+
try:
|
|
968
|
+
with open(artifact_path) as f:
|
|
969
|
+
data = json.load(f)
|
|
970
|
+
except (json.JSONDecodeError, FileNotFoundError) as e:
|
|
971
|
+
return False, [f"Cannot read {artifact_path}: {e}"]
|
|
972
|
+
|
|
973
|
+
schema_path = Path(__file__).parent.parent / "schemas" / f"{schema_name}.schema.json"
|
|
974
|
+
if not schema_path.exists():
|
|
975
|
+
return _structural_validate(data, schema_name)
|
|
976
|
+
|
|
977
|
+
try:
|
|
978
|
+
import jsonschema
|
|
979
|
+
with open(schema_path) as f:
|
|
980
|
+
schema = json.load(f)
|
|
981
|
+
jsonschema.validate(data, schema)
|
|
982
|
+
# JSON Schema can't express all constraints (e.g. unique IDs across
|
|
983
|
+
# array items), so also run structural checks for semantic rules.
|
|
984
|
+
return _structural_validate(data, schema_name)
|
|
985
|
+
except ImportError:
|
|
986
|
+
return _structural_validate(data, schema_name)
|
|
987
|
+
except Exception as e:
|
|
988
|
+
# Handle jsonschema.ValidationError and other errors
|
|
989
|
+
return False, [f"Schema validation failed: {e}"]
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def _structural_validate(data: dict, schema_name: str) -> tuple[bool, list[str]]:
|
|
993
|
+
"""Fallback validation without jsonschema library."""
|
|
994
|
+
errors = []
|
|
995
|
+
|
|
996
|
+
if schema_name == "features":
|
|
997
|
+
features = data.get("features", data) if isinstance(data, dict) else data
|
|
998
|
+
if not isinstance(features, list):
|
|
999
|
+
errors.append("features must be a list")
|
|
1000
|
+
else:
|
|
1001
|
+
for i, f in enumerate(features):
|
|
1002
|
+
if not f.get("description"):
|
|
1003
|
+
errors.append(f"Feature {i}: missing description")
|
|
1004
|
+
if not f.get("id"):
|
|
1005
|
+
errors.append(f"Feature {i}: missing id")
|
|
1006
|
+
if "passes" not in f:
|
|
1007
|
+
errors.append(f"Feature {i}: missing passes field")
|
|
1008
|
+
|
|
1009
|
+
elif schema_name == "migration-plan":
|
|
1010
|
+
steps = data.get("steps", [])
|
|
1011
|
+
if not isinstance(steps, list):
|
|
1012
|
+
errors.append("steps must be a list")
|
|
1013
|
+
else:
|
|
1014
|
+
step_ids = set()
|
|
1015
|
+
for i, s in enumerate(steps):
|
|
1016
|
+
if not s.get("id"):
|
|
1017
|
+
errors.append(f"Step {i}: missing id")
|
|
1018
|
+
elif s["id"] in step_ids:
|
|
1019
|
+
errors.append(f"Step {i}: duplicate id '{s['id']}'")
|
|
1020
|
+
else:
|
|
1021
|
+
step_ids.add(s["id"])
|
|
1022
|
+
if not s.get("tests_required") and not s.get("tests"):
|
|
1023
|
+
errors.append(f"Step {i}: no tests_required")
|
|
1024
|
+
for dep in s.get("depends_on", []):
|
|
1025
|
+
if dep not in step_ids:
|
|
1026
|
+
errors.append(f"Step {i}: depends_on '{dep}' not found")
|
|
1027
|
+
|
|
1028
|
+
elif schema_name == "seams":
|
|
1029
|
+
seams = data.get("seams", data) if isinstance(data, dict) else data
|
|
1030
|
+
if not isinstance(seams, list):
|
|
1031
|
+
errors.append("seams must be a list")
|
|
1032
|
+
else:
|
|
1033
|
+
for i, s in enumerate(seams):
|
|
1034
|
+
conf = s.get("confidence", -1)
|
|
1035
|
+
if not isinstance(conf, (int, float)) or not (0.0 <= conf <= 1.0):
|
|
1036
|
+
errors.append(f"Seam {i}: confidence {conf} not in [0.0, 1.0]")
|
|
1037
|
+
|
|
1038
|
+
return len(errors) == 0, errors
|
|
1039
|
+
|
|
1040
|
+
|
|
823
1041
|
# ---------------------------------------------------------------------------
|
|
824
1042
|
# Singleton accessor
|
|
825
1043
|
# ---------------------------------------------------------------------------
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
The flagship product of [Autonomi](https://www.autonomi.dev/). Complete installation instructions for all platforms and use cases.
|
|
4
4
|
|
|
5
|
-
**Version:** v6.
|
|
5
|
+
**Version:** v6.3.1
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
## What's New in v6.
|
|
9
|
+
## What's New in v6.3.1
|
|
10
10
|
|
|
11
11
|
### Dual-Mode Architecture (v6.0.0)
|
|
12
12
|
- `loki run` command for direct autonomous execution
|
package/mcp/__init__.py
CHANGED