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 CHANGED
@@ -9,7 +9,7 @@
9
9
  [![Agent Types](https://img.shields.io/badge/Agent%20Types-41-blue)]()
10
10
  [![Autonomi](https://img.shields.io/badge/Autonomi-autonomi.dev-5B4EEA)](https://www.autonomi.dev/)
11
11
 
12
- **Current Version: v5.52.4**
12
+ **Current Version: v6.2.1**
13
13
 
14
14
  ---
15
15
 
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.2.0
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.2.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
266
+ **v6.3.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 6.2.0
1
+ 6.3.0
@@ -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 and block), quoted strings.
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(input_docs)}",
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: $bmad_project_path${NC}"
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 "$prd_content_detach" > "$detach_prd"
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.2.0)"
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> [args]${NC}"
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 errors
7030
- errors=$(python3 -c "
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
- for e in errors: print(e)
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 [[ -n "$errors" ]]; then
7092
+ if ! echo "$result" | grep -q "^VALID$"; then
7038
7093
  echo -e "${RED}Template validation failed:${NC}"
7039
- echo "$errors"
7094
+ echo "$result"
7040
7095
  return 1
7041
7096
  fi
7042
- echo -e "${GREEN}Cluster template: $template_name${NC}"
7043
- echo -e "${YELLOW}Note: Cluster execution engine is planned for v6.3.0.${NC}"
7044
- echo -e "Template validated successfully. Use 'loki cluster info $template_name' for details."
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}"
@@ -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 re.match(r"^\s*[-*]\s+\S", line) or re.match(r"^\s*\d+\.\s+\S", line):
263
- count += 1
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=$(head -c 32000 ".loki/bmad-tasks.json")
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' 2>/dev/null
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": f"bmad-{i+1}",
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"),
@@ -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
- local mount_args=""
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('${PROJECT_DIR}/.loki/config/settings.json') as f:
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
- # Expand ~ and $HOME
850
- host_path=$(eval echo "$host_path" 2>/dev/null || echo "$host_path")
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
- mount_args="$mount_args -v ${host_path}:${container_path}:${mode}"
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
- mount_args="$mount_args -e $key"
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
- mount_args="$mount_args -e $env_name"
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
- local preset_mounts
1041
- preset_mounts=$(resolve_docker_mounts)
1042
- if [[ -n "$preset_mounts" ]]; then
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
- c_host=$(eval echo "$c_host" 2>/dev/null || echo "$c_host")
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)"
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "6.2.0"
10
+ __version__ = "6.3.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -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
  # ---------------------------------------------------------------------------
@@ -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.2.0
5
+ **Version:** v6.3.0
6
6
 
7
7
  ---
8
8
 
9
- ## What's New in v5.49.1
10
-
11
- ### Enterprise Security (v5.36.0-v5.37.1)
12
- - TLS/HTTPS support for dashboard connections
13
- - OIDC/SSO authentication (Google, Azure AD, Okta)
14
- - RBAC roles (admin, operator, viewer, auditor)
15
- - WebSocket authentication for real-time connections
16
- - Syslog forwarding for SIEM integration
17
- - Non-root Docker with SETUID/SETGID removed
18
- - Salted token hashing and rate limiting
19
-
20
- ### Monitoring & Observability (v5.38.0)
21
- - Prometheus/OpenMetrics `/metrics` endpoint with 9 metrics
22
- - `loki metrics` CLI command
23
- - Agent action audit trail at `.loki/logs/agent-audit.jsonl`
24
- - `loki audit` CLI with log/count subcommands
25
- - SHA-256 chain-hashed tamper-evident audit entries
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
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '6.2.0'
60
+ __version__ = '6.3.0'
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
- return _chroma_collection
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=int(CHROMA_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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loki-mode",
3
- "version": "6.2.0",
3
+ "version": "6.3.0",
4
4
  "description": "Loki Mode by Autonomi - Multi-agent autonomous startup system for Claude Code, Codex CLI, and Gemini CLI",
5
5
  "keywords": [
6
6
  "agent",