loki-mode 6.2.1 → 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/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.1
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.1 | [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.1
1
+ 6.3.0
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env bash
2
+ #===============================================================================
3
+ # Migration Hooks Engine
4
+ #
5
+ # Deterministic shell-level enforcement for migration pipelines.
6
+ # These hooks run WHETHER THE AGENT COOPERATES OR NOT.
7
+ # They are NOT LLM calls. They are shell scripts with binary pass/fail.
8
+ #
9
+ # Lifecycle points:
10
+ # pre_file_edit - Before agent modifies any source file (can BLOCK)
11
+ # post_file_edit - After agent modifies a source file (runs tests)
12
+ # post_step - After agent declares a migration step complete
13
+ # pre_phase_gate - Before transitioning between phases
14
+ # on_agent_stop - When agent tries to declare migration complete
15
+ #
16
+ # Configuration:
17
+ # .loki/migration-hooks.yaml (project-level, optional)
18
+ # Defaults applied when no config exists.
19
+ #
20
+ # Environment:
21
+ # LOKI_MIGRATION_ID - Current migration identifier
22
+ # LOKI_MIGRATION_DIR - Path to migration artifacts directory
23
+ # LOKI_CODEBASE_PATH - Path to target codebase
24
+ # LOKI_CURRENT_PHASE - Current migration phase
25
+ # LOKI_CURRENT_STEP - Current step ID (during migrate phase)
26
+ # LOKI_TEST_COMMAND - Test command to run (auto-detected or configured)
27
+ # LOKI_FEATURES_PATH - Path to features.json
28
+ # LOKI_AGENT_ID - ID of the current agent
29
+ # LOKI_FILE_PATH - Path of file being modified (for file hooks)
30
+ #===============================================================================
31
+
32
+ set -euo pipefail
33
+
34
+ HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
35
+
36
+ # Load project-specific hook config if it exists
37
+ load_migration_hook_config() {
38
+ local codebase_path="${1:-.}"
39
+ local config_file="${codebase_path}/.loki/migration-hooks.yaml"
40
+
41
+ # Defaults
42
+ HOOK_POST_FILE_EDIT_ENABLED=true
43
+ HOOK_POST_STEP_ENABLED=true
44
+ HOOK_PRE_PHASE_GATE_ENABLED=true
45
+ HOOK_ON_AGENT_STOP_ENABLED=true
46
+ HOOK_POST_FILE_EDIT_ACTION="run_tests"
47
+ HOOK_POST_FILE_EDIT_ON_FAILURE="block_and_rollback"
48
+ HOOK_POST_STEP_ON_FAILURE="reject_completion"
49
+ HOOK_ON_AGENT_STOP_ON_FAILURE="force_continue"
50
+
51
+ if [[ -f "$config_file" ]] && command -v python3 &>/dev/null; then
52
+ # Parse YAML config safely using read/declare instead of eval
53
+ while IFS='=' read -r key val; do
54
+ case "$key" in
55
+ HOOK_*) declare -g "$key=$val" ;;
56
+ esac
57
+ done < <(python3 -c "
58
+ import sys
59
+ try:
60
+ import yaml
61
+ with open('${config_file}') as f:
62
+ cfg = yaml.safe_load(f) or {}
63
+ hooks = cfg.get('hooks', {})
64
+ for key, val in hooks.items():
65
+ if isinstance(val, dict):
66
+ for k, v in val.items():
67
+ safe_key = 'HOOK_' + key.upper() + '_' + k.upper()
68
+ safe_val = str(v).replace(chr(10), ' ').replace(chr(13), '')
69
+ print(f'{safe_key}={safe_val}')
70
+ elif isinstance(val, bool):
71
+ safe_key = 'HOOK_' + key.upper() + '_ENABLED'
72
+ print(f'{safe_key}={\"true\" if val else \"false\"}')
73
+ except Exception as e:
74
+ print(f'# Hook config parse warning: {e}', file=sys.stderr)
75
+ " 2>/dev/null || true)
76
+ fi
77
+ }
78
+
79
+ # Auto-detect test command for the codebase
80
+ detect_test_command() {
81
+ local codebase_path="${1:-.}"
82
+
83
+ if [[ -n "${LOKI_TEST_COMMAND:-}" ]]; then
84
+ echo "$LOKI_TEST_COMMAND"
85
+ return
86
+ fi
87
+
88
+ # Detection priority
89
+ if [[ -f "${codebase_path}/package.json" ]] && grep -q '"test"' "${codebase_path}/package.json" 2>/dev/null; then
90
+ echo "cd '${codebase_path}' && npm test"
91
+ elif [[ -f "${codebase_path}/pom.xml" ]]; then
92
+ echo "cd '${codebase_path}' && mvn test -q"
93
+ elif [[ -f "${codebase_path}/build.gradle" || -f "${codebase_path}/build.gradle.kts" ]]; then
94
+ echo "cd '${codebase_path}' && ./gradlew test --quiet"
95
+ elif [[ -f "${codebase_path}/Cargo.toml" ]]; then
96
+ echo "cd '${codebase_path}' && cargo test --quiet"
97
+ elif [[ -f "${codebase_path}/setup.py" || -f "${codebase_path}/pyproject.toml" ]]; then
98
+ echo "cd '${codebase_path}' && python -m pytest -q"
99
+ elif [[ -f "${codebase_path}/go.mod" ]]; then
100
+ echo "cd '${codebase_path}' && go test ./..."
101
+ elif [[ -d "${codebase_path}/tests" ]]; then
102
+ echo "cd '${codebase_path}' && python -m pytest tests/ -q"
103
+ else
104
+ echo "echo 'No test command detected. Set LOKI_TEST_COMMAND.'"
105
+ fi
106
+ }
107
+
108
+ # Hook: post_file_edit - runs after ANY agent modifies a source file
109
+ hook_post_file_edit() {
110
+ local file_path="${1:-}"
111
+ local codebase_path="${LOKI_CODEBASE_PATH:-.}"
112
+ local migration_dir="${LOKI_MIGRATION_DIR:-}"
113
+
114
+ [[ "$HOOK_POST_FILE_EDIT_ENABLED" != "true" ]] && return 0
115
+
116
+ # Log the edit
117
+ if [[ -n "$migration_dir" ]]; then
118
+ local log_entry
119
+ log_entry="{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"event\":\"file_edit\",\"file\":\"${file_path}\",\"agent\":\"${LOKI_AGENT_ID:-unknown}\"}"
120
+ echo "$log_entry" >> "${migration_dir}/activity.jsonl" 2>/dev/null || true
121
+ fi
122
+
123
+ # Run tests
124
+ local test_cmd
125
+ test_cmd=$(detect_test_command "$codebase_path")
126
+ local test_result_file
127
+ test_result_file=$(mktemp)
128
+
129
+ if ! eval "$test_cmd" > "$test_result_file" 2>&1; then
130
+ local test_output
131
+ test_output=$(cat "$test_result_file")
132
+ rm -f "$test_result_file"
133
+
134
+ case "${HOOK_POST_FILE_EDIT_ON_FAILURE}" in
135
+ block_and_rollback)
136
+ # Revert the file change
137
+ git -C "$codebase_path" checkout -- "$file_path" 2>/dev/null || true
138
+ echo "HOOK_BLOCKED: Tests failed after editing ${file_path}. Change reverted."
139
+ echo "Test output: ${test_output}"
140
+ return 1
141
+ ;;
142
+ warn)
143
+ echo "HOOK_WARNING: Tests failed after editing ${file_path}."
144
+ return 0
145
+ ;;
146
+ *)
147
+ return 1
148
+ ;;
149
+ esac
150
+ fi
151
+
152
+ rm -f "$test_result_file"
153
+ return 0
154
+ }
155
+
156
+ # Hook: post_step - runs after agent declares a migration step complete
157
+ hook_post_step() {
158
+ local step_id="${1:-}"
159
+ local codebase_path="${LOKI_CODEBASE_PATH:-.}"
160
+
161
+ [[ "$HOOK_POST_STEP_ENABLED" != "true" ]] && return 0
162
+
163
+ # Run full test suite
164
+ local test_cmd
165
+ test_cmd=$(detect_test_command "$codebase_path")
166
+
167
+ if ! eval "$test_cmd" >/dev/null 2>&1; then
168
+ case "${HOOK_POST_STEP_ON_FAILURE}" in
169
+ reject_completion)
170
+ echo "HOOK_REJECTED: Step ${step_id} completion rejected. Tests do not pass."
171
+ return 1
172
+ ;;
173
+ *)
174
+ return 1
175
+ ;;
176
+ esac
177
+ fi
178
+
179
+ return 0
180
+ }
181
+
182
+ # Hook: pre_phase_gate - mechanical verification before phase transition
183
+ hook_pre_phase_gate() {
184
+ local from_phase="${1:-}"
185
+ local to_phase="${2:-}"
186
+ local migration_dir="${LOKI_MIGRATION_DIR:-}"
187
+
188
+ [[ "$HOOK_PRE_PHASE_GATE_ENABLED" != "true" ]] && return 0
189
+
190
+ case "${from_phase}:${to_phase}" in
191
+ understand:guardrail)
192
+ # Require: docs directory exists, features.json exists with >0 features
193
+ [[ ! -d "${migration_dir}/docs" ]] && echo "GATE_BLOCKED: No docs/ directory" && return 1
194
+ local feat_count
195
+ feat_count=$(python3 -c "
196
+ import json, sys
197
+ try:
198
+ with open('${migration_dir}/features.json') as f:
199
+ data = json.load(f)
200
+ features = data.get('features', data) if isinstance(data, dict) else data
201
+ print(len(features) if isinstance(features, list) else 0)
202
+ except: print(0)
203
+ " 2>/dev/null || echo 0)
204
+ [[ "$feat_count" -eq 0 ]] && echo "GATE_BLOCKED: features.json has 0 features" && return 1
205
+ ;;
206
+ guardrail:migrate)
207
+ # Require: ALL characterization tests pass
208
+ local test_cmd
209
+ test_cmd=$(detect_test_command "${LOKI_CODEBASE_PATH:-.}")
210
+ if ! eval "$test_cmd" >/dev/null 2>&1; then
211
+ echo "GATE_BLOCKED: Characterization tests do not pass"
212
+ return 1
213
+ fi
214
+ ;;
215
+ migrate:verify)
216
+ # Require: all steps completed in migration plan
217
+ local pending
218
+ pending=$(python3 -c "
219
+ import json
220
+ try:
221
+ with open('${migration_dir}/migration-plan.json') as f:
222
+ plan = json.load(f)
223
+ steps = plan.get('steps', [])
224
+ print(len([s for s in steps if s.get('status') != 'completed']))
225
+ except: print(-1)
226
+ " 2>/dev/null || echo -1)
227
+ [[ "$pending" -gt 0 ]] && echo "GATE_BLOCKED: ${pending} steps still pending" && return 1
228
+ ;;
229
+ esac
230
+
231
+ return 0
232
+ }
233
+
234
+ # Hook: on_agent_stop - prevents premature victory declaration
235
+ hook_on_agent_stop() {
236
+ local features_path="${LOKI_FEATURES_PATH:-}"
237
+
238
+ [[ "$HOOK_ON_AGENT_STOP_ENABLED" != "true" ]] && return 0
239
+ [[ ! -f "$features_path" ]] && return 0
240
+
241
+ local failing
242
+ failing=$(python3 -c "
243
+ import json
244
+ try:
245
+ with open('${features_path}') as f:
246
+ data = json.load(f)
247
+ features = data.get('features', data) if isinstance(data, dict) else data
248
+ if isinstance(features, list):
249
+ print(len([f for f in features if not f.get('passes', False)]))
250
+ else: print(0)
251
+ except: print(0)
252
+ " 2>/dev/null || echo 0)
253
+
254
+ if [[ "$failing" -gt 0 ]]; then
255
+ echo "HOOK_BLOCKED: ${failing} features still failing. Cannot declare migration complete."
256
+ return 1
257
+ fi
258
+
259
+ return 0
260
+ }
package/autonomy/loki CHANGED
@@ -31,6 +31,12 @@ BOLD='\033[1m'
31
31
  DIM='\033[2m'
32
32
  NC='\033[0m'
33
33
 
34
+ # Logging functions (portable across bash/zsh)
35
+ log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
36
+ log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
37
+ log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
38
+ log_debug() { echo -e "${CYAN}[DEBUG]${NC} $*"; }
39
+
34
40
  # Resolve the script's real path (handles symlinks)
35
41
  resolve_script_path() {
36
42
  local script="$1"
@@ -6923,7 +6929,7 @@ cmd_cluster() {
6923
6929
 
6924
6930
  case "$subcmd" in
6925
6931
  --help|-h|help)
6926
- echo -e "${BOLD}loki cluster${NC} - Custom workflow templates (v6.2.0)"
6932
+ echo -e "${BOLD}loki cluster${NC} - Custom workflow templates (v6.3.0)"
6927
6933
  echo ""
6928
6934
  echo "Usage: loki cluster <command> [options]"
6929
6935
  echo ""
@@ -6933,10 +6939,16 @@ cmd_cluster() {
6933
6939
  echo " run <name> [args] Execute a cluster workflow"
6934
6940
  echo " info <name> Show template details"
6935
6941
  echo ""
6942
+ echo "Options for 'run':"
6943
+ echo " --cluster-id <id> Named cluster ID for crash recovery"
6944
+ echo " --resume Resume a previously crashed cluster run"
6945
+ echo ""
6936
6946
  echo "Examples:"
6937
6947
  echo " loki cluster list"
6938
6948
  echo " loki cluster validate security-review"
6939
6949
  echo " loki cluster info code-review"
6950
+ echo " loki cluster run security-review --cluster-id my-review"
6951
+ echo " loki cluster run security-review --cluster-id my-review --resume"
6940
6952
  ;;
6941
6953
  list)
6942
6954
  echo -e "${BOLD}Available Cluster Templates${NC}"
@@ -7038,8 +7050,9 @@ for a in d.get('agents', []):
7038
7050
  ;;
7039
7051
  run)
7040
7052
  local template_name="${1:-}"
7053
+ shift 2>/dev/null || true
7041
7054
  if [[ -z "$template_name" ]]; then
7042
- echo -e "${RED}Usage: loki cluster run <template-name> [args]${NC}"
7055
+ echo -e "${RED}Usage: loki cluster run <template-name> [--cluster-id ID] [--resume]${NC}"
7043
7056
  return 1
7044
7057
  fi
7045
7058
  local template_file="$SKILL_DIR/templates/clusters/${template_name}.json"
@@ -7047,6 +7060,23 @@ for a in d.get('agents', []):
7047
7060
  echo -e "${RED}Template not found: $template_name${NC}"
7048
7061
  return 1
7049
7062
  fi
7063
+
7064
+ # Parse run options
7065
+ local cluster_id="" do_resume="false"
7066
+ while [[ $# -gt 0 ]]; do
7067
+ case "$1" in
7068
+ --cluster-id) cluster_id="$2"; shift 2 ;;
7069
+ --cluster-id=*) cluster_id="${1#*=}"; shift ;;
7070
+ --resume) do_resume="true"; shift ;;
7071
+ *) shift ;;
7072
+ esac
7073
+ done
7074
+
7075
+ # Auto-generate cluster ID if not provided
7076
+ if [[ -z "$cluster_id" ]]; then
7077
+ cluster_id="${template_name}_$(date +%Y%m%d_%H%M%S)"
7078
+ fi
7079
+
7050
7080
  # Validate first
7051
7081
  local result
7052
7082
  result=$(python3 -c "
@@ -7064,9 +7094,55 @@ else:
7064
7094
  echo "$result"
7065
7095
  return 1
7066
7096
  fi
7067
- echo -e "${GREEN}Cluster template: $template_name${NC}"
7068
- echo -e "${YELLOW}Note: Cluster execution engine is planned for v6.3.0.${NC}"
7069
- 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."
7070
7146
  ;;
7071
7147
  *)
7072
7148
  echo -e "${RED}Unknown cluster command: $subcmd${NC}"
@@ -7209,6 +7285,9 @@ main() {
7209
7285
  cluster)
7210
7286
  cmd_cluster "$@"
7211
7287
  ;;
7288
+ state)
7289
+ cmd_state "$@"
7290
+ ;;
7212
7291
  metrics)
7213
7292
  cmd_metrics "$@"
7214
7293
  ;;
@@ -7235,6 +7314,111 @@ main() {
7235
7314
  esac
7236
7315
  }
7237
7316
 
7317
+ # SQLite queryable state inspection
7318
+ cmd_state() {
7319
+ local subcmd="${1:-help}"
7320
+ shift 2>/dev/null || true
7321
+
7322
+ case "$subcmd" in
7323
+ --help|-h|help)
7324
+ echo -e "${BOLD}loki state${NC} - Query the SQLite state layer (v6.3.0)"
7325
+ echo ""
7326
+ echo "Usage: loki state <command> [options]"
7327
+ echo ""
7328
+ echo "Commands:"
7329
+ echo " db Print path to SQLite database file"
7330
+ echo " query events Query events [--agent ID] [--type TYPE] [--limit N]"
7331
+ echo " query messages Query messages [--topic TOPIC] [--cluster ID] [--limit N]"
7332
+ echo " query checkpoints Query checkpoints [--migration ID] [--limit N]"
7333
+ echo ""
7334
+ echo "Examples:"
7335
+ echo " loki state db"
7336
+ echo " loki state query events --agent arch_001 --limit 20"
7337
+ echo " loki state query messages --topic 'task.*'"
7338
+ echo " loki state query checkpoints --migration mig_123"
7339
+ ;;
7340
+ db)
7341
+ local db_path="${LOKI_DATA_DIR:-${HOME}/.loki}/state.db"
7342
+ echo "$db_path"
7343
+ if [[ -f "$db_path" ]]; then
7344
+ local size
7345
+ size=$(ls -lh "$db_path" 2>/dev/null | awk '{print $5}')
7346
+ echo -e "${DIM}Size: ${size}${NC}"
7347
+ else
7348
+ echo -e "${YELLOW}Database not yet created${NC}"
7349
+ fi
7350
+ ;;
7351
+ query)
7352
+ local query_type="${1:-}"
7353
+ shift 2>/dev/null || true
7354
+
7355
+ if [[ -z "$query_type" ]]; then
7356
+ echo -e "${RED}Usage: loki state query <events|messages|checkpoints> [options]${NC}"
7357
+ return 1
7358
+ fi
7359
+
7360
+ local agent_id="" event_type="" topic="" cluster_id="" migration_id="" limit="20"
7361
+ while [[ $# -gt 0 ]]; do
7362
+ case "$1" in
7363
+ --agent) agent_id="$2"; shift 2 ;;
7364
+ --type) event_type="$2"; shift 2 ;;
7365
+ --topic) topic="$2"; shift 2 ;;
7366
+ --cluster) cluster_id="$2"; shift 2 ;;
7367
+ --migration) migration_id="$2"; shift 2 ;;
7368
+ --limit) limit="$2"; shift 2 ;;
7369
+ *) shift ;;
7370
+ esac
7371
+ done
7372
+
7373
+ PYTHONPATH="${SKILL_DIR:-.}" python3 -c "
7374
+ import json, sys
7375
+ sys.path.insert(0, '${SKILL_DIR:-.}')
7376
+ from state.sqlite_backend import SqliteStateBackend
7377
+ db = SqliteStateBackend()
7378
+
7379
+ query_type = '${query_type}'
7380
+ if query_type == 'events':
7381
+ results = db.query_events(
7382
+ event_type='${event_type}' or None,
7383
+ agent_id='${agent_id}' or None,
7384
+ migration_id='${migration_id}' or None,
7385
+ limit=int('${limit}')
7386
+ )
7387
+ elif query_type == 'messages':
7388
+ results = db.query_messages(
7389
+ topic='${topic}' or None,
7390
+ cluster_id='${cluster_id}' or None,
7391
+ limit=int('${limit}')
7392
+ )
7393
+ elif query_type == 'checkpoints':
7394
+ results = db.query_checkpoints(
7395
+ migration_id='${migration_id}' or None,
7396
+ limit=int('${limit}')
7397
+ )
7398
+ else:
7399
+ print(f'Unknown query type: {query_type}')
7400
+ sys.exit(1)
7401
+
7402
+ if not results:
7403
+ print('No results found.')
7404
+ else:
7405
+ for r in results:
7406
+ print(json.dumps(r, indent=2))
7407
+ print('---')
7408
+ print(f'{len(results)} result(s)')
7409
+ " 2>&1 || {
7410
+ echo -e "${RED}Error querying state database${NC}"
7411
+ return 1
7412
+ }
7413
+ ;;
7414
+ *)
7415
+ echo -e "${RED}Unknown state command: $subcmd${NC}"
7416
+ echo "Run 'loki state --help' for usage."
7417
+ return 1
7418
+ ;;
7419
+ esac
7420
+ }
7421
+
7238
7422
  # Agent action audit log and quality scanning
7239
7423
  cmd_audit() {
7240
7424
  local subcommand="${1:-help}"
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "6.2.1"
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,11 +2,11 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v6.2.1
5
+ **Version:** v6.3.0
6
6
 
7
7
  ---
8
8
 
9
- ## What's New in v6.2.1
9
+ ## What's New in v6.3.0
10
10
 
11
11
  ### Dual-Mode Architecture (v6.0.0)
12
12
  - `loki run` command for direct autonomous execution
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '6.2.1'
60
+ __version__ = '6.3.0'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loki-mode",
3
- "version": "6.2.1",
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",