loki-mode 6.2.0 → 6.3.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/README.md +1 -1
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/bmad-adapter.py +3 -2
- package/autonomy/hooks/migration-hooks.sh +260 -0
- package/autonomy/loki +228 -19
- package/autonomy/prd-analyzer.py +3 -3
- package/autonomy/run.sh +20 -4
- package/autonomy/sandbox.sh +26 -18
- package/dashboard/__init__.py +1 -1
- package/dashboard/migration_engine.py +218 -0
- package/docs/INSTALLATION.md +19 -24
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +7 -2
- package/package.json +1 -1
package/README.md
CHANGED
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.0
|
|
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.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
6.
|
|
1
|
+
6.3.0
|
package/autonomy/bmad-adapter.py
CHANGED
|
@@ -80,7 +80,7 @@ def parse_frontmatter(text: str) -> Tuple[Dict[str, Any], str]:
|
|
|
80
80
|
"""Extract YAML frontmatter from a markdown document.
|
|
81
81
|
|
|
82
82
|
Returns (metadata_dict, body_without_frontmatter).
|
|
83
|
-
Handles simple YAML: scalars, lists (flow
|
|
83
|
+
Handles simple YAML: scalars, lists (flow-style only; block-style not supported), quoted strings.
|
|
84
84
|
Does NOT require PyYAML -- uses regex-based extraction.
|
|
85
85
|
"""
|
|
86
86
|
stripped = text.lstrip()
|
|
@@ -514,9 +514,10 @@ def validate_chain(
|
|
|
514
514
|
"message": "PRD frontmatter has no inputDocuments -- cannot verify product-brief linkage.",
|
|
515
515
|
})
|
|
516
516
|
else:
|
|
517
|
+
docs = input_docs if isinstance(input_docs, list) else [input_docs] if input_docs else []
|
|
517
518
|
findings.append({
|
|
518
519
|
"level": "info",
|
|
519
|
-
"message": f"PRD references input documents: {', '.join(
|
|
520
|
+
"message": f"PRD references input documents: {', '.join(docs)}",
|
|
520
521
|
})
|
|
521
522
|
|
|
522
523
|
# 2. Missing artifacts
|
|
@@ -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"
|
|
@@ -646,8 +652,9 @@ cmd_start() {
|
|
|
646
652
|
if [[ -n "$bmad_project_path" ]]; then
|
|
647
653
|
# Resolve to absolute path
|
|
648
654
|
if [[ ! "$bmad_project_path" = /* ]]; then
|
|
655
|
+
local original_bmad_path="$bmad_project_path"
|
|
649
656
|
bmad_project_path="$(cd "$bmad_project_path" 2>/dev/null && pwd)" || {
|
|
650
|
-
echo -e "${RED}Error: BMAD project path does not exist: $
|
|
657
|
+
echo -e "${RED}Error: BMAD project path does not exist: $original_bmad_path${NC}"
|
|
651
658
|
exit 1
|
|
652
659
|
}
|
|
653
660
|
fi
|
|
@@ -2754,10 +2761,12 @@ cmd_run() {
|
|
|
2754
2761
|
|
|
2755
2762
|
# Write PRD first so background process can use it
|
|
2756
2763
|
mkdir -p "$LOKI_DIR"
|
|
2757
|
-
local prd_content_detach
|
|
2758
|
-
prd_content_detach=$(echo "$issue_json" | generate_prd_from_issue)
|
|
2759
2764
|
local detach_prd="$LOKI_DIR/prd-issue-${number}.md"
|
|
2760
|
-
echo "$
|
|
2765
|
+
echo "$prd_content" > "$detach_prd"
|
|
2766
|
+
|
|
2767
|
+
if [[ -z "${branch_name:-}" ]]; then
|
|
2768
|
+
branch_name="issue/detach-$(date +%s)"
|
|
2769
|
+
fi
|
|
2761
2770
|
|
|
2762
2771
|
nohup bash -c "
|
|
2763
2772
|
cd $(pwd)
|
|
@@ -2765,6 +2774,24 @@ cmd_run() {
|
|
|
2765
2774
|
export LOKI_PARALLEL_MODE=true
|
|
2766
2775
|
export LOKI_WORKTREE_BRANCH=\"$branch_name\"
|
|
2767
2776
|
$(command -v loki || echo "$0") start \"$detach_prd\" --parallel ${start_args[*]+"${start_args[*]}"}
|
|
2777
|
+
|
|
2778
|
+
# Post-completion: create PR if requested
|
|
2779
|
+
if [[ \"$create_pr\" == \"true\" ]]; then
|
|
2780
|
+
branch_current=\$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo \"\")
|
|
2781
|
+
if [[ -n \"\$branch_current\" && \"\$branch_current\" != \"main\" && \"\$branch_current\" != \"master\" ]]; then
|
|
2782
|
+
git push origin \"\$branch_current\" 2>/dev/null || true
|
|
2783
|
+
gh pr create --title \"${title:-Implementation for issue ${issue_ref}}\" --body \"Implemented by Loki Mode\" --head \"\$branch_current\" 2>/dev/null || true
|
|
2784
|
+
fi
|
|
2785
|
+
fi
|
|
2786
|
+
# Post-completion: auto-merge if requested
|
|
2787
|
+
if [[ \"$auto_merge\" == \"true\" ]]; then
|
|
2788
|
+
branch_current=\$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo \"\")
|
|
2789
|
+
if gh pr merge \"\$branch_current\" --squash --delete-branch 2>/dev/null; then
|
|
2790
|
+
if [[ -n \"${number:-}\" ]]; then
|
|
2791
|
+
gh issue close \"$number\" --comment \"Resolved by Loki Mode\" 2>/dev/null || true
|
|
2792
|
+
fi
|
|
2793
|
+
fi
|
|
2794
|
+
fi
|
|
2768
2795
|
" > "$log_file" 2>&1 &
|
|
2769
2796
|
|
|
2770
2797
|
local bg_pid=$!
|
|
@@ -2862,11 +2889,13 @@ $(git log --oneline "main..HEAD" 2>/dev/null || echo "See diff")"
|
|
|
2862
2889
|
github)
|
|
2863
2890
|
local branch_current
|
|
2864
2891
|
branch_current=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
2865
|
-
gh pr merge "$branch_current" --squash --delete-branch 2>/dev/null
|
|
2866
|
-
echo -e "${GREEN}PR merged and branch deleted${NC}"
|
|
2892
|
+
if gh pr merge "$branch_current" --squash --delete-branch 2>/dev/null; then
|
|
2893
|
+
echo -e "${GREEN}PR merged and branch deleted${NC}"
|
|
2894
|
+
if [[ -n "${number:-}" ]]; then
|
|
2895
|
+
gh issue close "$number" --comment "Resolved by Loki Mode" 2>/dev/null || true
|
|
2896
|
+
fi
|
|
2897
|
+
else
|
|
2867
2898
|
echo -e "${YELLOW}Auto-merge failed. PR remains open.${NC}"
|
|
2868
|
-
if [[ -n "${number:-}" ]]; then
|
|
2869
|
-
gh issue close "$number" --comment "Resolved by Loki Mode" 2>/dev/null || true
|
|
2870
2899
|
fi
|
|
2871
2900
|
;;
|
|
2872
2901
|
gitlab)
|
|
@@ -6900,7 +6929,7 @@ cmd_cluster() {
|
|
|
6900
6929
|
|
|
6901
6930
|
case "$subcmd" in
|
|
6902
6931
|
--help|-h|help)
|
|
6903
|
-
echo -e "${BOLD}loki cluster${NC} - Custom workflow templates (v6.
|
|
6932
|
+
echo -e "${BOLD}loki cluster${NC} - Custom workflow templates (v6.3.0)"
|
|
6904
6933
|
echo ""
|
|
6905
6934
|
echo "Usage: loki cluster <command> [options]"
|
|
6906
6935
|
echo ""
|
|
@@ -6910,10 +6939,16 @@ cmd_cluster() {
|
|
|
6910
6939
|
echo " run <name> [args] Execute a cluster workflow"
|
|
6911
6940
|
echo " info <name> Show template details"
|
|
6912
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 ""
|
|
6913
6946
|
echo "Examples:"
|
|
6914
6947
|
echo " loki cluster list"
|
|
6915
6948
|
echo " loki cluster validate security-review"
|
|
6916
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"
|
|
6917
6952
|
;;
|
|
6918
6953
|
list)
|
|
6919
6954
|
echo -e "${BOLD}Available Cluster Templates${NC}"
|
|
@@ -6964,7 +6999,6 @@ except: print('?')
|
|
|
6964
6999
|
local errors
|
|
6965
7000
|
errors=$(python3 -c "
|
|
6966
7001
|
import json, sys
|
|
6967
|
-
sys.path.insert(0, '$(dirname "$SKILL_DIR/swarm/")')
|
|
6968
7002
|
sys.path.insert(0, '$SKILL_DIR')
|
|
6969
7003
|
from swarm.patterns import TopologyValidator
|
|
6970
7004
|
errors = TopologyValidator.validate_file(sys.argv[1])
|
|
@@ -7016,8 +7050,9 @@ for a in d.get('agents', []):
|
|
|
7016
7050
|
;;
|
|
7017
7051
|
run)
|
|
7018
7052
|
local template_name="${1:-}"
|
|
7053
|
+
shift 2>/dev/null || true
|
|
7019
7054
|
if [[ -z "$template_name" ]]; then
|
|
7020
|
-
echo -e "${RED}Usage: loki cluster run <template-name> [
|
|
7055
|
+
echo -e "${RED}Usage: loki cluster run <template-name> [--cluster-id ID] [--resume]${NC}"
|
|
7021
7056
|
return 1
|
|
7022
7057
|
fi
|
|
7023
7058
|
local template_file="$SKILL_DIR/templates/clusters/${template_name}.json"
|
|
@@ -7025,23 +7060,89 @@ for a in d.get('agents', []):
|
|
|
7025
7060
|
echo -e "${RED}Template not found: $template_name${NC}"
|
|
7026
7061
|
return 1
|
|
7027
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
|
+
|
|
7028
7080
|
# Validate first
|
|
7029
|
-
local
|
|
7030
|
-
|
|
7081
|
+
local result
|
|
7082
|
+
result=$(python3 -c "
|
|
7031
7083
|
import json, sys
|
|
7032
7084
|
sys.path.insert(0, '$SKILL_DIR')
|
|
7033
7085
|
from swarm.patterns import TopologyValidator
|
|
7034
7086
|
errors = TopologyValidator.validate_file(sys.argv[1])
|
|
7035
|
-
|
|
7087
|
+
if errors:
|
|
7088
|
+
for e in errors: print(f'ERROR: {e}')
|
|
7089
|
+
else:
|
|
7090
|
+
print('VALID')
|
|
7036
7091
|
" "$template_file" 2>&1)
|
|
7037
|
-
if
|
|
7092
|
+
if ! echo "$result" | grep -q "^VALID$"; then
|
|
7038
7093
|
echo -e "${RED}Template validation failed:${NC}"
|
|
7039
|
-
echo "$
|
|
7094
|
+
echo "$result"
|
|
7040
7095
|
return 1
|
|
7041
7096
|
fi
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
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."
|
|
7045
7146
|
;;
|
|
7046
7147
|
*)
|
|
7047
7148
|
echo -e "${RED}Unknown cluster command: $subcmd${NC}"
|
|
@@ -7184,6 +7285,9 @@ main() {
|
|
|
7184
7285
|
cluster)
|
|
7185
7286
|
cmd_cluster "$@"
|
|
7186
7287
|
;;
|
|
7288
|
+
state)
|
|
7289
|
+
cmd_state "$@"
|
|
7290
|
+
;;
|
|
7187
7291
|
metrics)
|
|
7188
7292
|
cmd_metrics "$@"
|
|
7189
7293
|
;;
|
|
@@ -7210,6 +7314,111 @@ main() {
|
|
|
7210
7314
|
esac
|
|
7211
7315
|
}
|
|
7212
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
|
+
|
|
7213
7422
|
# Agent action audit log and quality scanning
|
|
7214
7423
|
cmd_audit() {
|
|
7215
7424
|
local subcommand="${1:-help}"
|
package/autonomy/prd-analyzer.py
CHANGED
|
@@ -114,7 +114,6 @@ DIMENSIONS = {
|
|
|
114
114
|
"weight": 0.75,
|
|
115
115
|
"heading_patterns": [
|
|
116
116
|
r"(?i)#+\s.*(?:deploy|hosting|infra|ci.?cd|environment)",
|
|
117
|
-
r"(?i)^##\s+Non-Functional\s+Requirements",
|
|
118
117
|
],
|
|
119
118
|
"content_patterns": [
|
|
120
119
|
r"(?i)\b(?:deploy|hosting|ci.?cd|pipeline|staging|production)\b",
|
|
@@ -259,8 +258,9 @@ class PrdAnalyzer:
|
|
|
259
258
|
if in_feature_section and re.match(r"^\s*#+\s", line):
|
|
260
259
|
in_feature_section = False
|
|
261
260
|
continue
|
|
262
|
-
if
|
|
263
|
-
|
|
261
|
+
if in_feature_section:
|
|
262
|
+
if re.match(r"^\s*[-*]\s+\S", line) or re.match(r"^\s*\d+\.\s+\S", line):
|
|
263
|
+
count += 1
|
|
264
264
|
|
|
265
265
|
self.feature_count = count
|
|
266
266
|
for threshold, label in SCOPE_THRESHOLDS:
|
package/autonomy/run.sh
CHANGED
|
@@ -7043,7 +7043,19 @@ except: pass
|
|
|
7043
7043
|
fi
|
|
7044
7044
|
local bmad_tasks=""
|
|
7045
7045
|
if [[ -f ".loki/bmad-tasks.json" ]]; then
|
|
7046
|
-
bmad_tasks=$(
|
|
7046
|
+
bmad_tasks=$(python3 -c "
|
|
7047
|
+
import json, sys
|
|
7048
|
+
try:
|
|
7049
|
+
with open('.loki/bmad-tasks.json') as f:
|
|
7050
|
+
data = json.load(f)
|
|
7051
|
+
out = json.dumps(data, indent=None)
|
|
7052
|
+
if len(out) > 32000 and isinstance(data, list):
|
|
7053
|
+
while len(json.dumps(data, indent=None)) > 32000 and data:
|
|
7054
|
+
data.pop()
|
|
7055
|
+
out = json.dumps(data, indent=None)
|
|
7056
|
+
print(out[:32000])
|
|
7057
|
+
except: pass
|
|
7058
|
+
" 2>/dev/null)
|
|
7047
7059
|
fi
|
|
7048
7060
|
local bmad_validation=""
|
|
7049
7061
|
if [[ -f ".loki/bmad-validation.md" ]]; then
|
|
@@ -7100,7 +7112,7 @@ populate_bmad_queue() {
|
|
|
7100
7112
|
mkdir -p ".loki/queue"
|
|
7101
7113
|
|
|
7102
7114
|
# Read BMAD tasks and create queue entries
|
|
7103
|
-
python3 << 'BMAD_QUEUE_EOF'
|
|
7115
|
+
python3 << 'BMAD_QUEUE_EOF'
|
|
7104
7116
|
import json
|
|
7105
7117
|
import os
|
|
7106
7118
|
import sys
|
|
@@ -7155,12 +7167,16 @@ if os.path.exists(pending_path):
|
|
|
7155
7167
|
except (json.JSONDecodeError, FileNotFoundError):
|
|
7156
7168
|
existing = []
|
|
7157
7169
|
|
|
7158
|
-
# Convert BMAD stories to queue task format
|
|
7170
|
+
# Convert BMAD stories to queue task format (with deduplication)
|
|
7171
|
+
existing_ids = {t.get("id") for t in existing if isinstance(t, dict)}
|
|
7159
7172
|
for i, story in enumerate(stories):
|
|
7160
7173
|
if not isinstance(story, dict):
|
|
7161
7174
|
continue
|
|
7175
|
+
task_id = f"bmad-{i+1}"
|
|
7176
|
+
if task_id in existing_ids:
|
|
7177
|
+
continue
|
|
7162
7178
|
task = {
|
|
7163
|
-
"id":
|
|
7179
|
+
"id": task_id,
|
|
7164
7180
|
"title": story.get("title", story.get("name", f"BMAD Story {i+1}")),
|
|
7165
7181
|
"description": story.get("description", story.get("action", "")),
|
|
7166
7182
|
"priority": story.get("priority", "medium"),
|
package/autonomy/sandbox.sh
CHANGED
|
@@ -70,9 +70,10 @@ PROMPT_INJECTION_ENABLED="${LOKI_PROMPT_INJECTION:-false}"
|
|
|
70
70
|
# Container runs as user 'loki' (UID 1000), so mount to /home/loki/
|
|
71
71
|
declare -A DOCKER_MOUNT_PRESETS
|
|
72
72
|
DOCKER_MOUNT_PRESETS=(
|
|
73
|
+
# Note: gh and git are also hardcoded in start_sandbox() defaults
|
|
73
74
|
[gh]="$HOME/.config/gh:/home/loki/.config/gh:ro"
|
|
74
75
|
[git]="$HOME/.gitconfig:/home/loki/.gitconfig:ro"
|
|
75
|
-
[ssh]="$HOME/.ssh:/home/loki/.ssh:ro"
|
|
76
|
+
[ssh]="$HOME/.ssh/known_hosts:/home/loki/.ssh/known_hosts:ro"
|
|
76
77
|
[aws]="$HOME/.aws:/home/loki/.aws:ro"
|
|
77
78
|
[azure]="$HOME/.azure:/home/loki/.azure:ro"
|
|
78
79
|
[kube]="$HOME/.kube:/home/loki/.kube:ro"
|
|
@@ -799,7 +800,7 @@ docker_desktop_sandbox_run() {
|
|
|
799
800
|
# Reads from: .loki/config/settings.json dockerMounts, LOKI_DOCKER_MOUNTS env var
|
|
800
801
|
# Returns: string of -v and -e flags for docker run
|
|
801
802
|
resolve_docker_mounts() {
|
|
802
|
-
|
|
803
|
+
RESOLVED_MOUNTS=()
|
|
803
804
|
|
|
804
805
|
# Read configured presets (JSON array of strings)
|
|
805
806
|
local presets_json=""
|
|
@@ -807,10 +808,10 @@ resolve_docker_mounts() {
|
|
|
807
808
|
presets_json=$(python3 -c "
|
|
808
809
|
import json, sys
|
|
809
810
|
try:
|
|
810
|
-
with open('
|
|
811
|
+
with open(sys.argv[1] + '/.loki/config/settings.json') as f:
|
|
811
812
|
print(json.dumps(json.load(f).get('dockerMounts', [])))
|
|
812
813
|
except: print('[]')
|
|
813
|
-
" 2>/dev/null || echo "[]")
|
|
814
|
+
" "${PROJECT_DIR}" 2>/dev/null || echo "[]")
|
|
814
815
|
fi
|
|
815
816
|
|
|
816
817
|
# Override with env var if set
|
|
@@ -846,12 +847,16 @@ except: pass
|
|
|
846
847
|
local container_path="${parts[1]}"
|
|
847
848
|
local mode="${parts[2]:-ro}"
|
|
848
849
|
|
|
849
|
-
#
|
|
850
|
-
|
|
850
|
+
# Safe tilde expansion (no eval)
|
|
851
|
+
if [[ "$host_path" == "~/"* ]]; then
|
|
852
|
+
host_path="$HOME/${host_path#\~/}"
|
|
853
|
+
elif [[ "$host_path" == "~" ]]; then
|
|
854
|
+
host_path="$HOME"
|
|
855
|
+
fi
|
|
851
856
|
|
|
852
857
|
# Only mount if host path exists
|
|
853
858
|
if [[ -e "$host_path" ]]; then
|
|
854
|
-
|
|
859
|
+
RESOLVED_MOUNTS+=("-v" "${host_path}:${container_path}:${mode}")
|
|
855
860
|
log_info " Mount preset [$name]: $host_path -> $container_path ($mode)"
|
|
856
861
|
fi
|
|
857
862
|
|
|
@@ -865,18 +870,18 @@ except: pass
|
|
|
865
870
|
# Wildcard: pass all matching env vars
|
|
866
871
|
local prefix="${env_name%\*}"
|
|
867
872
|
while IFS='=' read -r key val; do
|
|
868
|
-
[[ "$key" == "$prefix"* ]] && [[ -n "$val" ]] &&
|
|
869
|
-
|
|
873
|
+
[[ "$key" == "$prefix"* ]] && [[ -n "$val" ]] && {
|
|
874
|
+
RESOLVED_MOUNTS+=("-e" "$key")
|
|
875
|
+
log_info " Env wildcard [$name]: passing $key"
|
|
876
|
+
}
|
|
870
877
|
done < <(env)
|
|
871
878
|
elif [[ -n "${!env_name:-}" ]]; then
|
|
872
|
-
|
|
879
|
+
RESOLVED_MOUNTS+=("-e" "$env_name")
|
|
873
880
|
fi
|
|
874
881
|
done
|
|
875
882
|
fi
|
|
876
883
|
done <<< "$preset_names"
|
|
877
884
|
fi
|
|
878
|
-
|
|
879
|
-
echo "$mount_args"
|
|
880
885
|
}
|
|
881
886
|
|
|
882
887
|
start_sandbox() {
|
|
@@ -1037,11 +1042,9 @@ start_sandbox() {
|
|
|
1037
1042
|
|
|
1038
1043
|
# Apply Docker credential mount presets (additive on top of defaults above)
|
|
1039
1044
|
if [[ "$no_mounts" != "true" ]] && [[ "${LOKI_NO_DOCKER_MOUNTS:-}" != "true" ]]; then
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
# shellcheck disable=SC2206
|
|
1044
|
-
docker_args+=($preset_mounts)
|
|
1045
|
+
resolve_docker_mounts
|
|
1046
|
+
if [[ ${#RESOLVED_MOUNTS[@]} -gt 0 ]]; then
|
|
1047
|
+
docker_args+=("${RESOLVED_MOUNTS[@]}")
|
|
1045
1048
|
fi
|
|
1046
1049
|
fi
|
|
1047
1050
|
|
|
@@ -1054,7 +1057,12 @@ start_sandbox() {
|
|
|
1054
1057
|
local c_container="${mount_parts[1]:-}"
|
|
1055
1058
|
local c_mode="${mount_parts[2]:-ro}"
|
|
1056
1059
|
if [[ -n "$c_host" ]] && [[ -n "$c_container" ]]; then
|
|
1057
|
-
|
|
1060
|
+
# Safe tilde expansion (no eval)
|
|
1061
|
+
if [[ "$c_host" == "~/"* ]]; then
|
|
1062
|
+
c_host="$HOME/${c_host#\~/}"
|
|
1063
|
+
elif [[ "$c_host" == "~" ]]; then
|
|
1064
|
+
c_host="$HOME"
|
|
1065
|
+
fi
|
|
1058
1066
|
if [[ -e "$c_host" ]]; then
|
|
1059
1067
|
docker_args+=("--volume" "${c_host}:${c_container}:${c_mode}")
|
|
1060
1068
|
log_info " Custom mount: $c_host -> $c_container ($c_mode)"
|
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,33 +2,28 @@
|
|
|
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.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
## What's New in
|
|
10
|
-
|
|
11
|
-
###
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
### Workflow Protection (v5.38.0)
|
|
28
|
-
- Branch protection: agent sessions auto-create feature branches
|
|
29
|
-
- PR creation via `gh` on session completion
|
|
30
|
-
- OpenClaw bridge foundation for external integrations
|
|
31
|
-
- Network security documentation (Docker/Kubernetes)
|
|
9
|
+
## What's New in v6.3.0
|
|
10
|
+
|
|
11
|
+
### Dual-Mode Architecture (v6.0.0)
|
|
12
|
+
- `loki run` command for direct autonomous execution
|
|
13
|
+
- Dual-mode: skill mode (inside Claude Code) and standalone mode
|
|
14
|
+
- Dynamic model resolution across all providers
|
|
15
|
+
- Multi-provider issue fixes and stability improvements
|
|
16
|
+
|
|
17
|
+
### ChromaDB Semantic Code Search (v6.1.0)
|
|
18
|
+
- Semantic code search via ChromaDB vector database
|
|
19
|
+
- MCP integration with `loki_code_search` and `loki_code_search_stats` tools
|
|
20
|
+
- Automatic codebase indexing with `tools/index-codebase.py`
|
|
21
|
+
|
|
22
|
+
### Memory System (v5.15.0+)
|
|
23
|
+
- Episodic, semantic, and procedural memory layers
|
|
24
|
+
- Progressive disclosure with 3-layer loading
|
|
25
|
+
- Token economics tracking for discovery vs read tokens
|
|
26
|
+
- Optional vector search with sentence-transformers
|
|
32
27
|
|
|
33
28
|
---
|
|
34
29
|
|
package/mcp/__init__.py
CHANGED
package/mcp/server.py
CHANGED
|
@@ -1231,10 +1231,15 @@ def _get_chroma_collection():
|
|
|
1231
1231
|
"""Get or create ChromaDB collection (lazy connection)."""
|
|
1232
1232
|
global _chroma_client, _chroma_collection
|
|
1233
1233
|
if _chroma_collection is not None:
|
|
1234
|
-
|
|
1234
|
+
try:
|
|
1235
|
+
_chroma_client.heartbeat()
|
|
1236
|
+
return _chroma_collection
|
|
1237
|
+
except Exception:
|
|
1238
|
+
_chroma_client = None
|
|
1239
|
+
_chroma_collection = None
|
|
1235
1240
|
try:
|
|
1236
1241
|
import chromadb
|
|
1237
|
-
_chroma_client = chromadb.HttpClient(host=CHROMA_HOST, port=
|
|
1242
|
+
_chroma_client = chromadb.HttpClient(host=CHROMA_HOST, port=CHROMA_PORT)
|
|
1238
1243
|
_chroma_collection = _chroma_client.get_collection(name=CHROMA_COLLECTION)
|
|
1239
1244
|
return _chroma_collection
|
|
1240
1245
|
except Exception as e:
|