@jetrabbits/agentic 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/AGENTS.md +13 -15
  2. package/CHANGELOG.md +24 -0
  3. package/MEMORY.md +67 -0
  4. package/Makefile +96 -14
  5. package/README.md +2 -2
  6. package/agentic +1269 -99
  7. package/areas/devops/ci-cd/AGENTS.md +1 -15
  8. package/areas/devops/database-ops/AGENTS.md +1 -15
  9. package/areas/devops/devsecops/AGENTS.md +1 -15
  10. package/areas/devops/infrastructure/AGENTS.md +1 -15
  11. package/areas/devops/kubernetes/AGENTS.md +1 -15
  12. package/areas/devops/networking/AGENTS.md +1 -15
  13. package/areas/devops/observability/AGENTS.md +1 -15
  14. package/areas/devops/sre/AGENTS.md +1 -15
  15. package/areas/software/backend/AGENTS.md +1 -16
  16. package/areas/software/data-engineering/AGENTS.md +1 -16
  17. package/areas/software/frontend/AGENTS.md +1 -16
  18. package/areas/software/full-stack/AGENTS.md +1 -16
  19. package/areas/software/general/AGENTS.md +1 -7
  20. package/areas/software/mlops/AGENTS.md +1 -16
  21. package/areas/software/mobile/AGENTS.md +1 -16
  22. package/areas/software/platform/AGENTS.md +1 -16
  23. package/areas/software/qa/AGENTS.md +1 -16
  24. package/areas/software/security/AGENTS.md +1 -16
  25. package/areas/template/AGENTS.tmpl.md +1 -17
  26. package/docs/agentic-lifecycle.md +8 -4
  27. package/docs/agentic-stabilization/README.md +37 -0
  28. package/docs/agentic-token-minimization/README.md +7 -5
  29. package/docs/agentic-usage.md +17 -14
  30. package/docs/opencode_setup.md +8 -4
  31. package/extensions/opencode/opencode.json +1 -1
  32. package/extensions/opencode/plugins/agent-model-mapper.ts +117 -0
  33. package/extensions/opencode/plugins/telegram-notification.ts +30 -20
  34. package/package.json +2 -1
  35. package/extensions/opencode/plugins/model-checker.json +0 -13
  36. package/extensions/opencode/plugins/model-checker.ts +0 -302
package/agentic CHANGED
@@ -42,6 +42,7 @@ ACTIVE_THEME="dark"
42
42
  SELECTED_AGENT_OS=("$DEFAULT_AGENT_OS")
43
43
  SELECTED_AREAS=()
44
44
  SELECTED_SPECS=()
45
+ INSTALL_SETTINGS_REPLAY=false
45
46
 
46
47
  SELF_INSTALL_FORCE=false
47
48
  SELF_INSTALL_BIN_DIR="${HOME}/.local/bin"
@@ -58,9 +59,18 @@ CONTEXT7_API_KEY="${CONTEXT7_API_KEY:-}"
58
59
  AGENTIC_ENABLE_CONTEXT7="${AGENTIC_ENABLE_CONTEXT7:-}"
59
60
  AGENTIC_DOCTOR="${AGENTIC_DOCTOR:-1}"
60
61
  AGENTIC_DOCTOR_KEEP_TMP="${AGENTIC_DOCTOR_KEEP_TMP:-0}"
62
+ AGENTIC_DOCTOR_TIMEOUT_SECONDS="${AGENTIC_DOCTOR_TIMEOUT_SECONDS:-10}"
63
+ AGENTIC_MEMPALACE_TIMEOUT_SECONDS="${AGENTIC_MEMPALACE_TIMEOUT_SECONDS:-60}"
64
+
65
+ OPENCODE_TELEGRAM_ENABLED=""
66
+ OPENCODE_TELEGRAM_BOT_TOKEN=""
67
+ OPENCODE_TELEGRAM_CHAT_ID=""
68
+ OPENCODE_AGENT_MODEL_MAPPER_ENABLED=""
69
+ OPENCODE_PLUGINS_CONFIGURED=false
61
70
 
62
71
  RUN_LOG_ACTIVE=false
63
72
  RUN_LOG_FILE=""
73
+ CHANGED_PATHS_REPORT_FILE=""
64
74
 
65
75
  COLOR_RESET=""
66
76
  COLOR_HEADER=""
@@ -276,6 +286,7 @@ init_run_logging() {
276
286
  local stamp
277
287
  stamp="$(date '+%Y%m%d-%H%M%S')"
278
288
  RUN_LOG_FILE="$(mktemp "$base_dir/agentic-$stamp.XXXXXX")"
289
+ CHANGED_PATHS_REPORT_FILE="$RUN_LOG_FILE.changes"
279
290
  RUN_LOG_ACTIVE=true
280
291
  log "Run log initialized: $RUN_LOG_FILE"
281
292
  }
@@ -363,6 +374,55 @@ log_file_block() {
363
374
  write_run_log_line "$(timestamp_now) --- $label output end ---"
364
375
  }
365
376
 
377
+ write_changed_paths_report() {
378
+ if [[ -z "$CHANGED_PATHS_REPORT_FILE" ]]; then
379
+ local base_dir="${TMPDIR:-/tmp}"
380
+ local stamp
381
+ stamp="$(date '+%Y%m%d-%H%M%S')"
382
+ CHANGED_PATHS_REPORT_FILE="$(mktemp "$base_dir/agentic-changed-paths-$stamp.XXXXXX")"
383
+ fi
384
+
385
+ {
386
+ printf 'Agentic changed paths report\n'
387
+ printf 'Generated at: %s\n' "$(timestamp_now)"
388
+ printf 'Project dir: %s\n' "$PROJECT_DIR"
389
+ printf 'Knowledge base repo: %s\n' "$REPO_ROOT"
390
+ printf '\n'
391
+
392
+ printf 'Created directories (%s)\n' "${#CREATED_PATHS[@]}"
393
+ if [[ "${#CREATED_PATHS[@]}" -eq 0 ]]; then
394
+ printf -- '- (none)\n'
395
+ else
396
+ local created_path
397
+ for created_path in "${CREATED_PATHS[@]}"; do
398
+ printf -- '- %s\n' "$created_path"
399
+ done
400
+ fi
401
+ printf '\n'
402
+
403
+ printf 'Copied/generated paths (%s)\n' "${#COPIED_PATHS[@]}"
404
+ if [[ "${#COPIED_PATHS[@]}" -eq 0 ]]; then
405
+ printf -- '- (none)\n'
406
+ else
407
+ local copied_path
408
+ for copied_path in "${COPIED_PATHS[@]}"; do
409
+ printf -- '- %s\n' "$copied_path"
410
+ done
411
+ fi
412
+ printf '\n'
413
+
414
+ printf 'Warnings (%s)\n' "${#WARNINGS[@]}"
415
+ if [[ "${#WARNINGS[@]}" -eq 0 ]]; then
416
+ printf -- '- (none)\n'
417
+ else
418
+ local warning
419
+ for warning in "${WARNINGS[@]}"; do
420
+ printf -- '- %s\n' "$warning"
421
+ done
422
+ fi
423
+ } > "$CHANGED_PATHS_REPORT_FILE"
424
+ }
425
+
366
426
  unique_append() {
367
427
  local value="$1"
368
428
  local arr_name="$2"
@@ -972,7 +1032,7 @@ write_agentic_manifest() {
972
1032
  : > "$skipped_file"
973
1033
  fi
974
1034
 
975
- local agent_os_csv areas_csv specs_csv
1035
+ local agent_os_csv areas_csv specs_csv mcp_integrations_csv
976
1036
  local old_ifs="$IFS"
977
1037
  IFS=,
978
1038
  agent_os_csv="${SELECTED_AGENT_OS[*]}"
@@ -980,8 +1040,21 @@ write_agentic_manifest() {
980
1040
  specs_csv="${SELECTED_SPECS[*]}"
981
1041
  IFS="$old_ifs"
982
1042
 
1043
+ # Build mcp_integrations list from current env selections
1044
+ local mcp_integrations=()
1045
+ if [[ "${AGENTIC_ENABLE_CONTEXT7:-}" =~ ^[Yy](es)?$ ]]; then
1046
+ mcp_integrations+=("context7")
1047
+ fi
1048
+ if [[ "${AGENTIC_ENABLE_MEMPALACE:-}" =~ ^[Yy](es)?$ ]]; then
1049
+ mcp_integrations+=("mempalace")
1050
+ fi
1051
+ old_ifs="$IFS"
1052
+ IFS=,
1053
+ mcp_integrations_csv="${mcp_integrations[*]:-}"
1054
+ IFS="$old_ifs"
1055
+
983
1056
  local manifest_status
984
- manifest_status="$(python3 - "$manifest" "$records_file" "$skipped_file" "$APP_REPO_LINK" "$REPO_ROOT" "$agent_os_csv" "$areas_csv" "$specs_csv" "$(app_version_label)" <<'PY'
1057
+ manifest_status="$(python3 - "$manifest" "$records_file" "$skipped_file" "$APP_REPO_LINK" "$REPO_ROOT" "$agent_os_csv" "$areas_csv" "$specs_csv" "$(app_version_label)" "$mcp_integrations_csv" "$OPENCODE_TELEGRAM_ENABLED" "$OPENCODE_TELEGRAM_BOT_TOKEN" "$OPENCODE_TELEGRAM_CHAT_ID" "$OPENCODE_AGENT_MODEL_MAPPER_ENABLED" <<'PY'
985
1058
  import json
986
1059
  import sys
987
1060
  from datetime import datetime, timezone
@@ -996,6 +1069,11 @@ agent_os = [x for x in sys.argv[6].split(",") if x]
996
1069
  areas = [x for x in sys.argv[7].split(",") if x]
997
1070
  specs = [x for x in sys.argv[8].split(",") if x]
998
1071
  app_version = sys.argv[9]
1072
+ mcp_integrations = [x for x in sys.argv[10].split(",") if x] if len(sys.argv) > 10 else []
1073
+ telegram_enabled = sys.argv[11].lower() == "true" if len(sys.argv) > 11 and sys.argv[11] else None
1074
+ telegram_bot_token = sys.argv[12] if len(sys.argv) > 12 else ""
1075
+ telegram_chat_id = sys.argv[13] if len(sys.argv) > 13 else ""
1076
+ mapper_enabled = sys.argv[14].lower() == "true" if len(sys.argv) > 14 and sys.argv[14] else None
999
1077
  now = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
1000
1078
 
1001
1079
  existing = {}
@@ -1037,7 +1115,26 @@ for line in records_file.read_text(encoding="utf-8").splitlines():
1037
1115
 
1038
1116
  skipped = [x for x in skipped_file.read_text(encoding="utf-8").splitlines() if x]
1039
1117
  old_agentic = old_data.get("_agentic", {}) if isinstance(old_data, dict) else {}
1118
+ old_settings = old_data.get("settings", {}) if isinstance(old_data, dict) else {}
1040
1119
  created_by = old_agentic.get("created_by", app_version)
1120
+ opencode_plugins = old_settings.get("opencode_plugins", {}) if isinstance(old_settings, dict) else {}
1121
+ if not isinstance(opencode_plugins, dict):
1122
+ opencode_plugins = {}
1123
+ telegram = opencode_plugins.get("telegram", {}) if isinstance(opencode_plugins.get("telegram"), dict) else {}
1124
+ agent_model_mapper = opencode_plugins.get("agentModelMapper", {}) if isinstance(opencode_plugins.get("agentModelMapper"), dict) else {}
1125
+ if telegram_enabled is not None:
1126
+ telegram = {
1127
+ "enabled": telegram_enabled,
1128
+ "botToken": telegram_bot_token,
1129
+ "chatId": telegram_chat_id,
1130
+ }
1131
+ if mapper_enabled is not None:
1132
+ agent_model_mapper = {"enabled": mapper_enabled}
1133
+ if telegram or agent_model_mapper:
1134
+ opencode_plugins = {
1135
+ "telegram": telegram or {"enabled": False},
1136
+ "agentModelMapper": agent_model_mapper or {"enabled": False},
1137
+ }
1041
1138
  data = {
1042
1139
  "_agentic": {
1043
1140
  "generated_by": "agentic",
@@ -1052,6 +1149,8 @@ data = {
1052
1149
  "agent_os": agent_os,
1053
1150
  "areas": areas,
1054
1151
  "specializations": specs,
1152
+ "mcp_integrations": mcp_integrations,
1153
+ "opencode_plugins": opencode_plugins,
1055
1154
  "source_repo": repo_link,
1056
1155
  "source_checkout": repo_root,
1057
1156
  },
@@ -1085,6 +1184,7 @@ load_install_settings_from_manifest() {
1085
1184
  local manifest="$1"
1086
1185
  [[ -f "$manifest" ]] || return 1
1087
1186
  ensure_python_available
1187
+ INSTALL_SETTINGS_REPLAY=true
1088
1188
 
1089
1189
  local values=()
1090
1190
  readlines values < <(python3 - "$manifest" <<'PY'
@@ -1094,10 +1194,28 @@ from pathlib import Path
1094
1194
 
1095
1195
  data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
1096
1196
  settings = data.get("settings", {})
1097
- for key in ("agent_os", "areas", "specializations"):
1197
+ for key in ("agent_os", "areas", "specializations", "mcp_integrations"):
1098
1198
  print("::" + key)
1099
1199
  for value in settings.get(key, []):
1100
1200
  print(value)
1201
+ plugins = settings.get("opencode_plugins", {})
1202
+ if isinstance(plugins, dict):
1203
+ telegram = plugins.get("telegram", {})
1204
+ if isinstance(telegram, dict):
1205
+ print("::opencode_telegram_enabled")
1206
+ if telegram.get("enabled") is not None:
1207
+ print("true" if telegram.get("enabled") is True else "false")
1208
+ print("::opencode_telegram_bot_token")
1209
+ if telegram.get("botToken"):
1210
+ print(telegram.get("botToken"))
1211
+ print("::opencode_telegram_chat_id")
1212
+ if telegram.get("chatId"):
1213
+ print(telegram.get("chatId"))
1214
+ mapper = plugins.get("agentModelMapper", {})
1215
+ if isinstance(mapper, dict):
1216
+ print("::opencode_agent_model_mapper_enabled")
1217
+ if mapper.get("enabled") is not None:
1218
+ print("true" if mapper.get("enabled") is True else "false")
1101
1219
  PY
1102
1220
  )
1103
1221
 
@@ -1105,17 +1223,28 @@ PY
1105
1223
  local loaded_agent_os=()
1106
1224
  local loaded_areas=()
1107
1225
  local loaded_specs=()
1226
+ local loaded_mcp_integrations=()
1227
+ local loaded_telegram_enabled=""
1228
+ local loaded_telegram_bot_token=""
1229
+ local loaded_telegram_chat_id=""
1230
+ local loaded_mapper_enabled=""
1108
1231
  local value
1109
1232
  for value in "${values[@]}"; do
1110
1233
  case "$value" in
1111
1234
  "::agent_os") section="agent_os" ;;
1112
1235
  "::areas") section="areas" ;;
1113
1236
  "::specializations") section="specializations" ;;
1237
+ "::mcp_integrations") section="mcp_integrations" ;;
1114
1238
  *)
1115
1239
  case "$section" in
1116
1240
  agent_os) loaded_agent_os+=("$value") ;;
1117
1241
  areas) loaded_areas+=("$value") ;;
1118
1242
  specializations) loaded_specs+=("$value") ;;
1243
+ mcp_integrations) loaded_mcp_integrations+=("$value") ;;
1244
+ opencode_telegram_enabled) loaded_telegram_enabled="$value" ;;
1245
+ opencode_telegram_bot_token) loaded_telegram_bot_token="$value" ;;
1246
+ opencode_telegram_chat_id) loaded_telegram_chat_id="$value" ;;
1247
+ opencode_agent_model_mapper_enabled) loaded_mapper_enabled="$value" ;;
1119
1248
  esac
1120
1249
  ;;
1121
1250
  esac
@@ -1130,6 +1259,36 @@ PY
1130
1259
  if [[ "${#SELECTED_SPECS[@]}" -eq 0 && "${#loaded_specs[@]}" -gt 0 ]]; then
1131
1260
  SELECTED_SPECS=("${loaded_specs[@]}")
1132
1261
  fi
1262
+
1263
+ # Restore MCP integration selections so configure_*_if_needed skip interactive prompts
1264
+ local mcp_item
1265
+ if [[ "${#loaded_mcp_integrations[@]}" -gt 0 ]]; then
1266
+ for mcp_item in "${loaded_mcp_integrations[@]}"; do
1267
+ case "$mcp_item" in
1268
+ context7)
1269
+ if [[ -z "${AGENTIC_ENABLE_CONTEXT7:-}" ]]; then
1270
+ AGENTIC_ENABLE_CONTEXT7="y"
1271
+ fi
1272
+ ;;
1273
+ mempalace)
1274
+ if [[ -z "${AGENTIC_ENABLE_MEMPALACE:-}" ]]; then
1275
+ AGENTIC_ENABLE_MEMPALACE="y"
1276
+ fi
1277
+ ;;
1278
+ esac
1279
+ done
1280
+ fi
1281
+
1282
+ if [[ -n "$loaded_telegram_enabled" ]]; then
1283
+ OPENCODE_TELEGRAM_ENABLED="$loaded_telegram_enabled"
1284
+ OPENCODE_TELEGRAM_BOT_TOKEN="$loaded_telegram_bot_token"
1285
+ OPENCODE_TELEGRAM_CHAT_ID="$loaded_telegram_chat_id"
1286
+ OPENCODE_PLUGINS_CONFIGURED=true
1287
+ fi
1288
+ if [[ -n "$loaded_mapper_enabled" ]]; then
1289
+ OPENCODE_AGENT_MODEL_MAPPER_ENABLED="$loaded_mapper_enabled"
1290
+ OPENCODE_PLUGINS_CONFIGURED=true
1291
+ fi
1133
1292
  }
1134
1293
 
1135
1294
  path_ref_for_shell_export() {
@@ -1704,6 +1863,39 @@ mcp_servers["context7"] = context7
1704
1863
  write_json_config_file "$dest" "generated:context7-gemini-config" "$body"
1705
1864
  }
1706
1865
 
1866
+ print_context7_key_recommendation() {
1867
+ [[ -z "$CONTEXT7_API_KEY" ]] || return 0
1868
+
1869
+ out "Context7 MCP configured without an API key."
1870
+ }
1871
+
1872
+ configure_context7_key_interactive() {
1873
+ is_interactive_terminal || return 0
1874
+
1875
+ local choice
1876
+ if fzf_available; then
1877
+ choice="$(choose_single_fzf "Context7 API key mode:" "Use without API key" "Enter CONTEXT7_API_KEY" || true)"
1878
+ else
1879
+ echo "Context7 API key mode:" >&2
1880
+ echo " 1) Use without API key" >&2
1881
+ echo " 2) Enter CONTEXT7_API_KEY" >&2
1882
+ local answer
1883
+ read -r -p "Select one (empty=1): " answer
1884
+ answer="$(trim "$answer")"
1885
+ case "$answer" in
1886
+ ""|1) choice="Use without API key" ;;
1887
+ 2) choice="Enter CONTEXT7_API_KEY" ;;
1888
+ *) error "Invalid choice"; exit 1 ;;
1889
+ esac
1890
+ fi
1891
+
1892
+ if [[ "$choice" == "Enter CONTEXT7_API_KEY" ]]; then
1893
+ CONTEXT7_API_KEY="$(prompt_text_interactive "CONTEXT7_API_KEY" "$CONTEXT7_API_KEY")"
1894
+ else
1895
+ CONTEXT7_API_KEY=""
1896
+ fi
1897
+ }
1898
+
1707
1899
  write_mempalace_opencode_config() {
1708
1900
  local dest="$1"
1709
1901
  local body
@@ -1747,65 +1939,380 @@ servers["mempalace"] = {"command": "mempalace-mcp"}
1747
1939
  }
1748
1940
 
1749
1941
  print_mempalace_project_setup_instructions() {
1750
- log "MemPalace setup instructions for target project: $PROJECT_DIR"
1942
+ local project_wing
1943
+ project_wing="$(mempalace_project_wing)"
1944
+ log "Optional MemPalace project indexing instructions for target project: $PROJECT_DIR"
1751
1945
  out "1) Ensure Python is installed and available in PATH."
1752
1946
  out "2) Install MemPalace:"
1753
1947
  out " pip install mempalace"
1754
- out "3) Initialize project-local MemPalace cache:"
1755
- out " mempalace init \"$PROJECT_DIR\" --yes --auto-mine"
1756
- out "4) Index existing project memory:"
1757
- out " # optional if --auto-mine was skipped"
1758
- out " mempalace mine \"$PROJECT_DIR\""
1759
- out "5) Verify in your IDE/agent that MemPalace MCP tools are connected."
1760
- out "Note: Ollama at localhost:11434 is optional; MemPalace can run heuristics-only without it."
1948
+ out "3) Initialize the project memory taxonomy without LLM calls:"
1949
+ out " mempalace init \"$PROJECT_DIR\" --yes --no-llm"
1950
+ out "4) Mine project knowledge into its isolated wing:"
1951
+ out " mempalace mine \"$PROJECT_DIR\" --wing \"$project_wing\""
1952
+ if [[ -d "$PROJECT_DIR/docs" ]]; then
1953
+ out "5) Mine shared project docs into the cross-project docs wing:"
1954
+ out " mempalace mine \"$PROJECT_DIR/docs\" --wing shared_docs"
1955
+ out "6) Verify in your IDE/agent that MemPalace MCP tools are connected."
1956
+ else
1957
+ out "5) Verify in your IDE/agent that MemPalace MCP tools are connected."
1958
+ fi
1959
+ out "Note: agentic uses --no-llm by default to keep MemPalace setup low-cost."
1960
+ }
1961
+
1962
+ mempalace_sanitize_wing_name() {
1963
+ local raw="$1"
1964
+ local sanitized
1965
+ sanitized="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/_/g; s/^_+//; s/_+$//; s/_+/_/g')"
1966
+ if [[ -z "$sanitized" ]]; then
1967
+ sanitized="project"
1968
+ fi
1969
+ printf '%s\n' "$sanitized"
1970
+ }
1971
+
1972
+ mempalace_project_wing() {
1973
+ mempalace_sanitize_wing_name "$(basename "$PROJECT_DIR")"
1974
+ }
1975
+
1976
+ mempalace_shared_docs_wing() {
1977
+ printf '%s\n' "shared_docs"
1978
+ }
1979
+
1980
+ write_mempalace_ignore_file() {
1981
+ local dest="$PROJECT_DIR/.mempalaceignore"
1982
+ local content
1983
+ content='node_modules/
1984
+ .venv/
1985
+ venv/
1986
+ dist/
1987
+ logs/
1988
+ build/
1989
+ target/
1990
+ coverage/
1991
+ .ai/
1992
+ .git/
1993
+ .github/
1994
+ .cursor/
1995
+ .agent/
1996
+ .opencode/
1997
+ .claude/
1998
+ .gemini/
1999
+ .codex/
2000
+ .idea/
2001
+ *.csv
2002
+ *.parquet
2003
+ *.log
2004
+ *.jsonl
2005
+
2006
+ data/
2007
+ dumps/
2008
+ tmp/
2009
+ '
2010
+
2011
+ if [[ -e "$dest" ]]; then
2012
+ log "MemPalace ignore file already exists: $dest"
2013
+ return 0
2014
+ fi
2015
+
2016
+ write_text_config_file "$dest" "generated:mempalace-ignore" "$content"
2017
+ }
2018
+
2019
+ warn_mempalace_failure_reason() {
2020
+ local output_file="$1"
2021
+ [[ -f "$output_file" ]] || return 0
2022
+
2023
+ if grep -Fq "incompatible architecture" "$output_file" && grep -Fq "numpy" "$output_file"; then
2024
+ warn "MemPalace failed because Python/NumPy architecture is inconsistent. Reinstall MemPalace dependencies with the same architecture as the Python running 'mempalace'."
2025
+ warn "Typical fix: reinstall numpy/chromadb/mempalace in the active Python environment, or use a matching arm64/x86_64 Python. See the MemPalace log above for the exact Python path."
2026
+ return 0
2027
+ fi
2028
+
2029
+ if grep -Fq "No LLM provider reachable" "$output_file"; then
2030
+ warn "MemPalace could not reach an LLM provider and continued heuristics-only; this is non-fatal unless a later dependency error appears."
2031
+ fi
2032
+ }
2033
+
2034
+ warn_mempalace_pip_failure_reason() {
2035
+ local output_file="$1"
2036
+ [[ -f "$output_file" ]] || return 0
2037
+
2038
+ local reason
2039
+ reason="$(sed -n '/[^[:space:]]/{p;q;}' "$output_file" 2>/dev/null || true)"
2040
+ if [[ -n "$reason" ]]; then
2041
+ warn "pip failure reason: $reason"
2042
+ else
2043
+ warn "pip failure output was empty; inspect the MemPalace pip install log for details."
2044
+ fi
2045
+ }
2046
+
2047
+ mempalace_timeout_seconds() {
2048
+ local value="${AGENTIC_MEMPALACE_TIMEOUT_SECONDS:-60}"
2049
+ if [[ "$value" =~ ^[0-9]+$ ]] && (( value > 0 )); then
2050
+ printf '%s\n' "$value"
2051
+ return
2052
+ fi
2053
+ printf '%s\n' "60"
2054
+ }
2055
+
2056
+ run_mempalace_command() {
2057
+ local label="$1"
2058
+ shift
2059
+ local output_file timeout_seconds child_pid elapsed status
2060
+ output_file="$(mktemp "${TMPDIR:-/tmp}/agentic-mempalace.XXXXXX")"
2061
+ timeout_seconds="$(mempalace_timeout_seconds)"
2062
+
2063
+ "$@" >"$output_file" 2>&1 &
2064
+ child_pid=$!
2065
+ elapsed=0
2066
+ status=0
2067
+ while kill -0 "$child_pid" 2>/dev/null; do
2068
+ if (( elapsed >= timeout_seconds )); then
2069
+ pkill -TERM -P "$child_pid" 2>/dev/null || true
2070
+ kill "$child_pid" 2>/dev/null || true
2071
+ sleep 1
2072
+ pkill -KILL -P "$child_pid" 2>/dev/null || true
2073
+ kill -9 "$child_pid" 2>/dev/null || true
2074
+ wait "$child_pid" 2>/dev/null || true
2075
+ warn "Timed out after ${timeout_seconds}s: $* (log: $output_file)"
2076
+ log_file_block "$label" "$output_file"
2077
+ return 1
2078
+ fi
2079
+ sleep 1
2080
+ elapsed=$((elapsed + 1))
2081
+ done
2082
+
2083
+ if wait "$child_pid"; then
2084
+ log "$label completed"
2085
+ log_file_block "$label" "$output_file"
2086
+ rm -f "$output_file"
2087
+ return 0
2088
+ fi
2089
+ status=$?
2090
+
2091
+ warn "Failed: $* (exit $status, log: $output_file)"
2092
+ log_file_block "$label" "$output_file"
2093
+ warn_mempalace_failure_reason "$output_file"
2094
+ return 1
1761
2095
  }
1762
2096
 
1763
- setup_mempalace_for_agentic_opencode() {
2097
+ install_mempalace_with_pip() {
2098
+ local pip_bin="$1"
2099
+ local output_file status
2100
+ local py_bin venv_dir venv_python
2101
+
2102
+ output_file="$(mktemp "${TMPDIR:-/tmp}/agentic-mempalace-pip.XXXXXX")"
2103
+
2104
+ if $pip_bin install mempalace >"$output_file" 2>&1; then
2105
+ log "MemPalace package installed via '$pip_bin install mempalace'"
2106
+ log_file_block "MemPalace pip install" "$output_file"
2107
+ rm -f "$output_file"
2108
+ return 0
2109
+ else
2110
+ status=$?
2111
+ fi
2112
+
2113
+ if grep -qi "externally-managed-environment" "$output_file"; then
2114
+ log "Detected PEP 668 externally-managed Python environment; retrying inside isolated venv"
2115
+
2116
+ py_bin="$(command -v python3 || command -v python)"
2117
+ if [[ -z "$py_bin" ]]; then
2118
+ warn "python3/python executable not found"
2119
+ return 1
2120
+ fi
2121
+
2122
+ venv_dir="${HOME}/.agentic/mempalace-venv"
2123
+
2124
+ if [[ ! -d "$venv_dir" ]]; then
2125
+ mkdir -p "$(dirname "$venv_dir")"
2126
+
2127
+ if ! "$py_bin" -m venv "$venv_dir" >>"$output_file" 2>&1; then
2128
+ warn "Unable to create virtual environment at $venv_dir"
2129
+ log_file_block "MemPalace pip install" "$output_file"
2130
+ return 1
2131
+ fi
2132
+ fi
2133
+
2134
+ venv_python="$venv_dir/bin/python"
2135
+
2136
+ if ! "$venv_python" -m pip install --upgrade pip setuptools wheel >>"$output_file" 2>&1; then
2137
+ warn "Unable to upgrade pip inside virtual environment"
2138
+ log_file_block "MemPalace pip install" "$output_file"
2139
+ return 1
2140
+ fi
2141
+
2142
+ if ! "$venv_python" -m pip install --no-cache-dir --upgrade mempalace >>"$output_file" 2>&1; then
2143
+ warn "Unable to install mempalace inside virtual environment"
2144
+ log_file_block "MemPalace pip install" "$output_file"
2145
+ return 1
2146
+ fi
2147
+
2148
+ local_bin_dir="$HOME/.local/bin"
2149
+
2150
+ mkdir -p "$local_bin_dir"
2151
+
2152
+ ln -sf "$venv_dir/bin/mempalace" "$local_bin_dir/mempalace"
2153
+
2154
+ export PATH="$local_bin_dir:$venv_dir/bin:$PATH"
2155
+
2156
+ shell_name="$(basename "${SHELL:-}")"
2157
+
2158
+ if [[ "$shell_name" == "zsh" ]]; then
2159
+ grep -qxF 'export PATH="$HOME/.local/bin:$PATH"' "$HOME/.zshrc" 2>/dev/null || \
2160
+ echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.zshrc"
2161
+ else
2162
+ grep -qxF 'export PATH="$HOME/.local/bin:$PATH"' "$HOME/.bashrc" 2>/dev/null || \
2163
+ echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.bashrc"
2164
+ fi
2165
+
2166
+ log "MemPalace installed successfully inside virtual environment: $venv_dir"
2167
+ log "MemPalace binary linked to: $local_bin_dir/mempalace"
2168
+ log_file_block "MemPalace pip install" "$output_file"
2169
+
2170
+ rm -f "$output_file"
2171
+ return 0
2172
+ fi
2173
+
2174
+ warn "Unable to auto-install mempalace via pip; continuing with manual setup instructions (exit $status, log: $output_file)"
2175
+ log_file_block "MemPalace pip install" "$output_file"
2176
+ warn_mempalace_pip_failure_reason "$output_file"
2177
+
2178
+ return 1
2179
+ }
2180
+
2181
+ mempalace_venv_dir() {
2182
+ printf '%s\n' "${AGENTIC_MEMPALACE_VENV:-$HOME/.venvs/mempalace}"
2183
+ }
2184
+
2185
+ mempalace_bin_dir() {
2186
+ printf '%s\n' "${AGENTIC_MEMPALACE_BIN_DIR:-$HOME/.local/bin}"
2187
+ }
2188
+
2189
+ python3_command() {
2190
+ if command -v python3 >/dev/null 2>&1; then
2191
+ printf '%s\n' "python3"
2192
+ return 0
2193
+ fi
2194
+ if command -v python >/dev/null 2>&1; then
2195
+ printf '%s\n' "python"
2196
+ return 0
2197
+ fi
2198
+ return 1
2199
+ }
2200
+
2201
+ install_mempalace_managed() {
2202
+ local py_bin venv_dir bin_dir venv_python venv_mempalace
2203
+
2204
+ py_bin="$(python3_command)" || return 1
2205
+ venv_dir="$(mempalace_venv_dir)"
2206
+ bin_dir="$(mempalace_bin_dir)"
2207
+
2208
+ mkdir -p "$(dirname "$venv_dir")" "$bin_dir"
2209
+
2210
+ if [[ ! -x "$venv_dir/bin/python" ]]; then
2211
+ "$py_bin" -m venv "$venv_dir" || return 1
2212
+ fi
2213
+
2214
+ venv_python="$venv_dir/bin/python"
2215
+ venv_mempalace="$venv_dir/bin/mempalace"
2216
+
2217
+ "$venv_python" -m pip install --upgrade pip setuptools wheel >/dev/null 2>&1 || return 1
2218
+ "$venv_python" -m pip install --upgrade --no-cache-dir mempalace >/dev/null 2>&1 || return 1
2219
+
2220
+ [[ -x "$venv_mempalace" ]] || return 1
2221
+
2222
+ ln -sf "$venv_mempalace" "$bin_dir/mempalace"
2223
+
2224
+ if [[ -x "$venv_dir/bin/mempalace-mcp" ]]; then
2225
+ ln -sf "$venv_dir/bin/mempalace-mcp" "$bin_dir/mempalace-mcp"
2226
+ fi
2227
+
2228
+ export PATH="$bin_dir:$PATH"
2229
+
2230
+ command -v mempalace >/dev/null 2>&1
2231
+ }
2232
+
2233
+ initialize_mempalace_project() {
2234
+ local step_prefix="$1"
2235
+ local project_wing shared_docs_wing
2236
+ project_wing="$(mempalace_project_wing)"
2237
+ shared_docs_wing="$(mempalace_shared_docs_wing)"
2238
+ log "$step_prefix [4/4] Initializing project memory at $PROJECT_DIR (wing: $project_wing)"
2239
+ if ! command -v mempalace >/dev/null 2>&1; then
2240
+ warn "mempalace command is unavailable after install; please run setup manually"
2241
+ print_mempalace_project_setup_instructions
2242
+ return 1
2243
+ fi
2244
+
2245
+ if ! run_mempalace_command "MemPalace init" mempalace init "$PROJECT_DIR" --yes --no-llm; then
2246
+ print_mempalace_project_setup_instructions
2247
+ return 1
2248
+ fi
2249
+ if ! run_mempalace_command "MemPalace mine project wing" mempalace mine "$PROJECT_DIR" --wing "$project_wing"; then
2250
+ print_mempalace_project_setup_instructions
2251
+ return 1
2252
+ fi
2253
+ if [[ -d "$PROJECT_DIR/docs" ]]; then
2254
+ if ! run_mempalace_command "MemPalace mine shared docs wing" mempalace mine "$PROJECT_DIR/docs" --wing "$shared_docs_wing"; then
2255
+ print_mempalace_project_setup_instructions
2256
+ return 1
2257
+ fi
2258
+ fi
2259
+ log "$step_prefix [4/4] Initialization step finished"
2260
+ }
2261
+
2262
+ setup_mempalace_for_agentic() {
2263
+ local initialize_project="${1:-false}"
1764
2264
  local step_prefix="MemPalace setup"
1765
2265
 
2266
+ if [[ "${AGENTIC_MEMPALACE_SETUP:-}" == "skip" ]]; then
2267
+ log "$step_prefix skipped by AGENTIC_MEMPALACE_SETUP=skip"
2268
+ if command -v mempalace-mcp >/dev/null 2>&1; then
2269
+ return 0
2270
+ fi
2271
+ return 1
2272
+ fi
2273
+
1766
2274
  log "$step_prefix [1/4] Checking Python availability"
1767
2275
  if ! command -v python3 >/dev/null 2>&1 && ! command -v python >/dev/null 2>&1; then
1768
2276
  warn "Python is not installed. Install Python 3 first, then run: pip install mempalace"
1769
2277
  warn "Install help: https://www.python.org/downloads/"
2278
+ print_mempalace_project_setup_instructions
1770
2279
  return 1
1771
2280
  fi
1772
2281
  log "$step_prefix [1/4] Python check passed"
1773
2282
 
2283
+ if [[ -z "${AGENTIC_TEST_SOURCE_AGENTIC:-}" ]] && command -v mempalace-mcp >/dev/null 2>&1; then
2284
+ if [[ "$initialize_project" != "true" ]] || command -v mempalace >/dev/null 2>&1; then
2285
+ log "$step_prefix [2/4] MemPalace binaries already available; skipping pip install"
2286
+ if [[ "$initialize_project" != "true" ]]; then
2287
+ log "$step_prefix [4/4] Project memory initialization skipped for selected agent target(s)"
2288
+ return 0
2289
+ fi
2290
+ initialize_mempalace_project "$step_prefix"
2291
+ return $?
2292
+ fi
2293
+ fi
2294
+
1774
2295
  log "$step_prefix [2/4] Checking pip availability"
1775
2296
  local pip_bin
1776
2297
  if ! pip_bin="$(pip_command)"; then
1777
2298
  warn "pip is not available. Install pip for Python 3, then run: pip install mempalace"
2299
+ print_mempalace_project_setup_instructions
1778
2300
  return 1
1779
2301
  fi
1780
2302
  log "$step_prefix [2/4] pip check passed"
1781
2303
 
1782
2304
  log "$step_prefix [3/4] Installing mempalace package"
1783
- if $pip_bin install mempalace >/dev/null 2>&1; then
1784
- log "MemPalace package installed via '$pip_bin install mempalace'"
1785
- else
1786
- warn "Unable to auto-install mempalace via pip; continuing with manual setup instructions"
2305
+ if ! install_mempalace_with_pip "$pip_bin"; then
1787
2306
  print_mempalace_project_setup_instructions
1788
2307
  return 1
1789
2308
  fi
1790
2309
 
1791
- log "$step_prefix [4/4] Initializing project memory at $PROJECT_DIR"
1792
- if command -v mempalace >/dev/null 2>&1; then
1793
- if mempalace init "$PROJECT_DIR" --yes --auto-mine >/dev/null 2>&1; then
1794
- log "MemPalace init completed"
1795
- else
1796
- warn "Failed: mempalace init \"$PROJECT_DIR\" --yes --auto-mine"
1797
- fi
1798
- if mempalace mine "$PROJECT_DIR" >/dev/null 2>&1; then
1799
- log "MemPalace mine completed"
1800
- else
1801
- warn "Failed: mempalace mine \"$PROJECT_DIR\""
1802
- fi
1803
- log "$step_prefix [4/4] Initialization step finished"
1804
- else
1805
- warn "mempalace command is unavailable after install; please run setup manually"
1806
- print_mempalace_project_setup_instructions
1807
- return 1
2310
+ if [[ "$initialize_project" != "true" ]]; then
2311
+ log "$step_prefix [4/4] Project memory initialization skipped for selected agent target(s)"
2312
+ return 0
1808
2313
  fi
2314
+
2315
+ initialize_mempalace_project "$step_prefix"
1809
2316
  }
1810
2317
 
1811
2318
  configure_mempalace_if_needed() {
@@ -1832,16 +2339,18 @@ configure_mempalace_if_needed() {
1832
2339
  return
1833
2340
  fi
1834
2341
 
1835
- if selected_agent_os_contains "opencode"; then
1836
- setup_mempalace_for_agentic_opencode || true
1837
- else
1838
- print_mempalace_project_setup_instructions
1839
- fi
2342
+ write_mempalace_ignore_file
1840
2343
 
1841
- if command -v mempalace-mcp >/dev/null 2>&1; then
1842
- log "MemPalace MCP binary found: mempalace-mcp"
2344
+ local initialize_mempalace_project="true"
2345
+ local mempalace_setup_ok="true"
2346
+ setup_mempalace_for_agentic "$initialize_mempalace_project" || mempalace_setup_ok="false"
2347
+
2348
+ if [[ "$mempalace_setup_ok" != "true" ]]; then
2349
+ if ! command -v mempalace-mcp >/dev/null 2>&1; then
2350
+ warn "mempalace-mcp is unavailable; install/repair MemPalace and re-run setup"
2351
+ fi
1843
2352
  else
1844
- warn "mempalace-mcp is unavailable; install/repair MemPalace and re-run setup"
2353
+ log "MemPalace MCP binary found: mempalace-mcp"
1845
2354
  fi
1846
2355
 
1847
2356
  if selected_agent_os_contains "opencode"; then
@@ -1883,7 +2392,6 @@ configure_context7_if_needed() {
1883
2392
  fi
1884
2393
 
1885
2394
  if is_interactive_terminal; then
1886
- local answer
1887
2395
  if [[ -z "$enable_context7" ]]; then
1888
2396
  read -r -p "Enable Context7 MCP configuration? [y/N]: " enable_context7
1889
2397
  enable_context7="$(trim "$enable_context7")"
@@ -1892,10 +2400,12 @@ configure_context7_if_needed() {
1892
2400
  log "Context7 MCP configuration disabled"
1893
2401
  return
1894
2402
  fi
2403
+ configure_context7_key_interactive
1895
2404
 
1896
- if [[ -z "$CONTEXT7_API_KEY" ]]; then
1897
- read -r -p "Context7 API key (optional, empty = no key): " answer
1898
- CONTEXT7_API_KEY="$(trim "$answer")"
2405
+ elif [[ -n "$enable_context7" ]]; then
2406
+ if [[ ! "$enable_context7" =~ ^[Yy]$ ]]; then
2407
+ log "Context7 MCP configuration disabled"
2408
+ return
1899
2409
  fi
1900
2410
  elif [[ -z "$CONTEXT7_API_KEY" ]]; then
1901
2411
  log "Context7 MCP configuration skipped; set CONTEXT7_API_KEY or use an interactive install to enable it"
@@ -1930,10 +2440,17 @@ configure_context7_if_needed() {
1930
2440
  if selected_agent_os_contains "antigravity"; then
1931
2441
  write_context7_antigravity_config
1932
2442
  fi
2443
+
2444
+ print_context7_key_recommendation
1933
2445
  }
1934
2446
 
1935
2447
  write_default_opencode_plugin_config() {
1936
2448
  ensure_dir "$APP_CONFIG_DIR"
2449
+ OPENCODE_TELEGRAM_ENABLED="false"
2450
+ OPENCODE_TELEGRAM_BOT_TOKEN=""
2451
+ OPENCODE_TELEGRAM_CHAT_ID=""
2452
+ OPENCODE_AGENT_MODEL_MAPPER_ENABLED="false"
2453
+ OPENCODE_PLUGINS_CONFIGURED=true
1937
2454
  if [[ "$DRY_RUN" == true ]]; then
1938
2455
  log "DRY-RUN write disabled opencode plugin config to $OPENCODE_PLUGIN_CONFIG_FILE"
1939
2456
  else
@@ -1944,13 +2461,40 @@ from pathlib import Path
1944
2461
 
1945
2462
  path = Path(sys.argv[1])
1946
2463
  path.write_text(json.dumps({
1947
- "telegram": {"enabled": False, "botToken": "", "chatId": ""},
1948
- "modelChecker": {"enabled": False},
2464
+ "telegram": {"enabled": False},
2465
+ "agentModelMapper": {"enabled": False},
1949
2466
  }, indent=2) + "\n", encoding="utf-8")
1950
2467
  PY
1951
2468
  fi
1952
2469
  }
1953
2470
 
2471
+ load_opencode_plugin_config_globals() {
2472
+ [[ -f "$OPENCODE_PLUGIN_CONFIG_FILE" ]] || return 1
2473
+ local values=()
2474
+ readlines values < <(python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" <<'PY'
2475
+ import json
2476
+ import sys
2477
+ from pathlib import Path
2478
+
2479
+ try:
2480
+ data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
2481
+ except Exception:
2482
+ data = {}
2483
+ telegram = data.get("telegram", {}) if isinstance(data, dict) else {}
2484
+ mapper = data.get("agentModelMapper", {}) if isinstance(data, dict) else {}
2485
+ print("true" if telegram.get("enabled") is True else "false")
2486
+ print(telegram.get("botToken") or "")
2487
+ print(telegram.get("chatId") or "")
2488
+ print("true" if mapper.get("enabled") is True else "false")
2489
+ PY
2490
+ )
2491
+ OPENCODE_TELEGRAM_ENABLED="${values[0]:-false}"
2492
+ OPENCODE_TELEGRAM_BOT_TOKEN="${values[1]:-}"
2493
+ OPENCODE_TELEGRAM_CHAT_ID="${values[2]:-}"
2494
+ OPENCODE_AGENT_MODEL_MAPPER_ENABLED="${values[3]:-false}"
2495
+ OPENCODE_PLUGINS_CONFIGURED=true
2496
+ }
2497
+
1954
2498
  configure_opencode_plugins_if_needed() {
1955
2499
  selected_agent_os_contains "opencode" || return 0
1956
2500
 
@@ -1962,14 +2506,27 @@ configure_opencode_plugins_if_needed() {
1962
2506
  ensure_python_available
1963
2507
  ensure_dir "$APP_CONFIG_DIR"
1964
2508
 
2509
+ # During manifest replay/re-install, keep current global plugin settings and avoid prompts.
2510
+ if [[ "$INSTALL_SETTINGS_REPLAY" == true && "$OPENCODE_PLUGINS_CONFIGURED" == true ]]; then
2511
+ log "OpenCode plugin settings loaded from .agentic.json"
2512
+ return
2513
+ fi
2514
+ if [[ "$INSTALL_SETTINGS_REPLAY" == true && -f "$OPENCODE_PLUGIN_CONFIG_FILE" ]]; then
2515
+ load_opencode_plugin_config_globals || true
2516
+ log "OpenCode plugin config already exists; keeping current settings"
2517
+ return
2518
+ fi
2519
+
1965
2520
  if ! is_interactive_terminal; then
1966
2521
  if [[ ! -f "$OPENCODE_PLUGIN_CONFIG_FILE" ]]; then
1967
2522
  write_default_opencode_plugin_config
2523
+ else
2524
+ load_opencode_plugin_config_globals || true
1968
2525
  fi
1969
2526
  return
1970
2527
  fi
1971
2528
 
1972
- local plugin_options=("telegram-opencode-notifier" "llm-quota-checker")
2529
+ local plugin_options=("telegram-notification" "agent-model-mapper")
1973
2530
  local selected_plugins=()
1974
2531
  local use_fzf_plugins=false
1975
2532
  if fzf_available; then
@@ -1986,47 +2543,472 @@ configure_opencode_plugins_if_needed() {
1986
2543
  readlines selected_plugins <<< "$selected_plugins_output"
1987
2544
  fi
1988
2545
 
1989
- local enable_telegram="n" telegram_token telegram_chat enable_model_checker="n"
2546
+ local enable_telegram="n" enable_agent_model_mapper="n"
1990
2547
  local selected_plugin
1991
2548
  for selected_plugin in "${selected_plugins[@]}"; do
1992
2549
  selected_plugin="$(trim "$selected_plugin")"
1993
2550
  [[ -z "$selected_plugin" ]] && continue
1994
2551
  case "$selected_plugin" in
1995
- telegram-opencode-notifier) enable_telegram="y" ;;
1996
- llm-quota-checker) enable_model_checker="y" ;;
2552
+ telegram-notification|telegram-opencode-notifier) enable_telegram="y" ;;
2553
+ agent-model-mapper) enable_agent_model_mapper="y" ;;
1997
2554
  esac
1998
2555
  done
1999
2556
 
2000
- telegram_token=""
2001
- telegram_chat=""
2002
2557
  if [[ "$enable_telegram" =~ ^[Yy]$ ]]; then
2003
- read -r -p "Telegram bot token (empty disables plugin): " telegram_token
2004
- read -r -p "Telegram chat id (empty disables plugin): " telegram_chat
2005
- telegram_token="$(trim "$telegram_token")"
2006
- telegram_chat="$(trim "$telegram_chat")"
2558
+ OPENCODE_TELEGRAM_BOT_TOKEN="$(prompt_text_interactive "Telegram botToken" "$OPENCODE_TELEGRAM_BOT_TOKEN")"
2559
+ OPENCODE_TELEGRAM_CHAT_ID="$(prompt_text_interactive "Telegram chatId" "$OPENCODE_TELEGRAM_CHAT_ID")"
2560
+ if [[ -z "$OPENCODE_TELEGRAM_BOT_TOKEN" || -z "$OPENCODE_TELEGRAM_CHAT_ID" ]]; then
2561
+ warn "Telegram plugin credentials are incomplete; disabling telegram-notification"
2562
+ enable_telegram="n"
2563
+ OPENCODE_TELEGRAM_BOT_TOKEN=""
2564
+ OPENCODE_TELEGRAM_CHAT_ID=""
2565
+ else
2566
+ log "Telegram plugin enabled; credentials will be stored in project .agentic.json"
2567
+ fi
2007
2568
  fi
2569
+ OPENCODE_TELEGRAM_ENABLED=$([[ "$enable_telegram" =~ ^[Yy]$ ]] && echo "true" || echo "false")
2570
+ OPENCODE_AGENT_MODEL_MAPPER_ENABLED=$([[ "$enable_agent_model_mapper" =~ ^[Yy]$ ]] && echo "true" || echo "false")
2571
+ OPENCODE_PLUGINS_CONFIGURED=true
2008
2572
 
2009
- python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" "$telegram_token" "$telegram_chat" "$enable_model_checker" <<'PY'
2573
+ python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" "$enable_telegram" "$enable_agent_model_mapper" <<'PY'
2010
2574
  import json
2011
2575
  import sys
2012
2576
  from pathlib import Path
2013
2577
 
2014
2578
  path = Path(sys.argv[1])
2015
- token = sys.argv[2]
2016
- chat = sys.argv[3]
2017
- enable_model = sys.argv[4].lower() == "y"
2579
+ enable_telegram = sys.argv[2].lower() == "y"
2580
+ enable_mapper = sys.argv[3].lower() == "y"
2018
2581
  data = {
2019
2582
  "telegram": {
2020
- "enabled": bool(token and chat),
2021
- "botToken": token,
2022
- "chatId": chat,
2583
+ "enabled": enable_telegram,
2023
2584
  },
2024
- "modelChecker": {
2025
- "enabled": enable_model,
2585
+ "agentModelMapper": {
2586
+ "enabled": enable_mapper,
2026
2587
  },
2027
2588
  }
2028
2589
  path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
2029
2590
  PY
2591
+
2592
+ }
2593
+
2594
+ opencode_agent_model_mapper_config_enabled() {
2595
+ if [[ "$OPENCODE_AGENT_MODEL_MAPPER_ENABLED" == "true" ]]; then
2596
+ return 0
2597
+ fi
2598
+ if [[ "$OPENCODE_AGENT_MODEL_MAPPER_ENABLED" == "false" ]]; then
2599
+ return 1
2600
+ fi
2601
+ [[ -f "$OPENCODE_PLUGIN_CONFIG_FILE" ]] || return 1
2602
+ python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" <<'PY'
2603
+ import json
2604
+ import sys
2605
+ from pathlib import Path
2606
+
2607
+ try:
2608
+ data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
2609
+ except Exception:
2610
+ raise SystemExit(1)
2611
+ raise SystemExit(0 if data.get("agentModelMapper", {}).get("enabled") is True else 1)
2612
+ PY
2613
+ }
2614
+
2615
+ opencode_mapper_read_roles() {
2616
+ local agents_dir="$PROJECT_DIR/.opencode/agents"
2617
+ [[ -d "$agents_dir" ]] || return 0
2618
+ python3 - "$agents_dir" <<'PY'
2619
+ import sys
2620
+ from pathlib import Path
2621
+
2622
+ agents_dir = Path(sys.argv[1])
2623
+
2624
+ def parse_frontmatter(text):
2625
+ if not text.startswith("---\n"):
2626
+ return {}
2627
+ end = text.find("\n---", 4)
2628
+ if end == -1:
2629
+ return {}
2630
+ result = {}
2631
+ for line in text[4:end].splitlines():
2632
+ if ":" not in line:
2633
+ continue
2634
+ key, value = line.split(":", 1)
2635
+ result[key.strip()] = value.strip().strip("'\"")
2636
+ return result
2637
+
2638
+ for path in sorted(agents_dir.glob("*.md")):
2639
+ frontmatter = parse_frontmatter(path.read_text(encoding="utf-8"))
2640
+ name = path.stem.replace("\t", " ")
2641
+ mode = (frontmatter.get("mode") or "subagent").replace("\t", " ")
2642
+ description = (frontmatter.get("description") or "OpenCode agent").replace("\t", " ")
2643
+ print(f"{name}\t{mode}\t{description}")
2644
+ PY
2645
+ }
2646
+
2647
+ opencode_mapper_discover_models() {
2648
+ local config_path="$HOME/.config/opencode/opencode.json"
2649
+ local auth_path="$HOME/.local/share/opencode/auth.json"
2650
+ local models_cache_path="$HOME/.cache/opencode/models.json"
2651
+ python3 - "$config_path" "$auth_path" "$models_cache_path" <<'PY'
2652
+ import json
2653
+ import sys
2654
+ from pathlib import Path
2655
+
2656
+ fallback = ["opencode/minimax-m2.5-free"]
2657
+ config_path = Path(sys.argv[1])
2658
+ auth_path = Path(sys.argv[2])
2659
+ models_cache_path = Path(sys.argv[3])
2660
+ models = []
2661
+
2662
+ def add_model(model):
2663
+ if isinstance(model, str) and model.strip() and "/" in model:
2664
+ models.append(model.strip())
2665
+
2666
+ def collect_provider_models(data):
2667
+ """Extract models from provider.<name>.models dict keys."""
2668
+ providers = data.get("provider")
2669
+ if not isinstance(providers, dict):
2670
+ return
2671
+ for provider_name, provider_data in providers.items():
2672
+ if not isinstance(provider_data, dict):
2673
+ continue
2674
+ provider_models = provider_data.get("models")
2675
+ if not isinstance(provider_models, dict):
2676
+ continue
2677
+ for model_name in provider_models:
2678
+ if isinstance(model_name, str) and model_name.strip():
2679
+ add_model(f"{provider_name}/{model_name}")
2680
+
2681
+ def collect(value):
2682
+ if isinstance(value, list):
2683
+ for item in value:
2684
+ collect(item)
2685
+ return
2686
+ if not isinstance(value, dict):
2687
+ return
2688
+ for key, item in value.items():
2689
+ if key in {"model", "id"} and isinstance(item, str) and "/" in item:
2690
+ add_model(item)
2691
+ if key == "fallback" and isinstance(item, list):
2692
+ for model in item:
2693
+ add_model(model)
2694
+ collect(item)
2695
+
2696
+ def read_json(path):
2697
+ try:
2698
+ return json.loads(path.read_text(encoding="utf-8"))
2699
+ except Exception:
2700
+ return None
2701
+
2702
+ def is_deprecated(model_data):
2703
+ if not isinstance(model_data, dict):
2704
+ return False
2705
+ status = str(model_data.get("status", "")).lower()
2706
+ lifecycle = str(model_data.get("lifecycle", "")).lower()
2707
+ return (
2708
+ model_data.get("deprecated") is True
2709
+ or status in {"deprecated", "retired", "removed"}
2710
+ or lifecycle in {"deprecated", "retired", "removed"}
2711
+ )
2712
+
2713
+ def collect_authenticated_provider_models(auth_data, cache_data):
2714
+ if not isinstance(auth_data, dict) or not isinstance(cache_data, dict):
2715
+ return
2716
+ for provider_name, auth in auth_data.items():
2717
+ if not isinstance(provider_name, str) or not provider_name.strip():
2718
+ continue
2719
+ if auth in (None, False):
2720
+ continue
2721
+ provider_data = cache_data.get(provider_name)
2722
+ if not isinstance(provider_data, dict):
2723
+ continue
2724
+ provider_models = provider_data.get("models")
2725
+ if isinstance(provider_models, dict):
2726
+ for model_name, model_data in provider_models.items():
2727
+ if isinstance(model_name, str) and model_name.strip() and not is_deprecated(model_data):
2728
+ add_model(f"{provider_name}/{model_name}")
2729
+ elif isinstance(provider_models, list):
2730
+ for item in provider_models:
2731
+ if isinstance(item, str):
2732
+ add_model(f"{provider_name}/{item}")
2733
+ elif isinstance(item, dict) and not is_deprecated(item):
2734
+ model_name = item.get("id") or item.get("name")
2735
+ if isinstance(model_name, str) and model_name.strip():
2736
+ add_model(f"{provider_name}/{model_name}")
2737
+
2738
+ try:
2739
+ data = json.loads(config_path.read_text(encoding="utf-8"))
2740
+ collect_provider_models(data)
2741
+ collect(data)
2742
+ except Exception:
2743
+ pass
2744
+
2745
+ collect_authenticated_provider_models(read_json(auth_path), read_json(models_cache_path))
2746
+
2747
+ seen = set()
2748
+ for model in models or fallback:
2749
+ model = model.strip()
2750
+ if model and model not in seen:
2751
+ seen.add(model)
2752
+ print(model)
2753
+ PY
2754
+ }
2755
+
2756
+ opencode_mapper_has_complete_mapping() {
2757
+ local roles_file="$1"
2758
+ local config_path="$PROJECT_DIR/.opencode/opencode.json"
2759
+ local state_path="$PROJECT_DIR/.opencode/agent-model-mapper.state.json"
2760
+ python3 - "$roles_file" "$config_path" "$state_path" <<'PY'
2761
+ import json
2762
+ import sys
2763
+ from pathlib import Path
2764
+
2765
+ roles_file, config_path, state_path = map(Path, sys.argv[1:])
2766
+ try:
2767
+ state = json.loads(state_path.read_text(encoding="utf-8"))
2768
+ config = json.loads(config_path.read_text(encoding="utf-8"))
2769
+ except Exception:
2770
+ raise SystemExit(1)
2771
+ if not state.get("configured"):
2772
+ raise SystemExit(1)
2773
+ agents = config.get("agent")
2774
+ if not isinstance(agents, dict):
2775
+ raise SystemExit(1)
2776
+ roles = [line.split("\t", 1)[0] for line in roles_file.read_text(encoding="utf-8").splitlines() if line]
2777
+ for role in roles:
2778
+ agent = agents.get(role)
2779
+ if not isinstance(agent, dict) or not str(agent.get("model", "")).strip():
2780
+ raise SystemExit(1)
2781
+ raise SystemExit(0)
2782
+ PY
2783
+ }
2784
+
2785
+ choose_opencode_mapper_model() {
2786
+ local role_name="$1"
2787
+ local role_mode="$2"
2788
+ local role_description="$3"
2789
+ local kind="$4"
2790
+ shift 4
2791
+ local models=("$@")
2792
+
2793
+ if [[ "${AGENTIC_AGENT_MODEL_MAPPER_NO_FZF:-}" != "1" ]] && fzf_available; then
2794
+ local selected selected_model fzf_status
2795
+ set +e
2796
+ selected="$(for i in "${!models[@]}"; do printf '%s\t%s\n' "$((i + 1))" "${models[$i]}"; done | fzf \
2797
+ --ansi \
2798
+ --border \
2799
+ --height=70% \
2800
+ --layout=reverse \
2801
+ --no-sort \
2802
+ --prompt "$role_name $kind> " \
2803
+ --header "Select $kind model for $role_name" \
2804
+ --with-nth=2..)"
2805
+ fzf_status=$?
2806
+ set -e
2807
+ if [[ "$fzf_status" -eq 0 && -n "$(trim "$selected")" ]]; then
2808
+ selected_model="${selected#* }"
2809
+ local model
2810
+ for model in "${models[@]}"; do
2811
+ if [[ "$model" == "$selected_model" ]]; then
2812
+ printf '%s\n' "$selected_model"
2813
+ return 0
2814
+ fi
2815
+ done
2816
+ fi
2817
+ fi
2818
+
2819
+ echo >&2
2820
+ echo "$role_name ($role_mode) - $role_description" >&2
2821
+ local i
2822
+ for i in "${!models[@]}"; do
2823
+ echo " $((i + 1))) ${models[$i]}" >&2
2824
+ done
2825
+ local answer
2826
+ read -r -p "Select $kind model for $role_name [1]: " answer
2827
+ answer="$(trim "$answer")"
2828
+ if [[ -z "$answer" ]]; then
2829
+ printf '%s\n' "${models[0]}"
2830
+ return 0
2831
+ fi
2832
+ if [[ "$answer" =~ ^[0-9]+$ ]] && (( answer >= 1 && answer <= ${#models[@]} )); then
2833
+ printf '%s\n' "${models[$((answer - 1))]}"
2834
+ return 0
2835
+ fi
2836
+ local model
2837
+ for model in "${models[@]}"; do
2838
+ if [[ "$model" == "$answer" ]]; then
2839
+ printf '%s\n' "$answer"
2840
+ return 0
2841
+ fi
2842
+ done
2843
+ warn "Unknown model '$answer', using ${models[0]}"
2844
+ printf '%s\n' "${models[0]}"
2845
+ }
2846
+
2847
+ write_opencode_agent_model_mapping() {
2848
+ local roles_file="$1"
2849
+ local mapping_file="$2"
2850
+ local config_path="$PROJECT_DIR/.opencode/opencode.json"
2851
+ local state_path="$PROJECT_DIR/.opencode/agent-model-mapper.state.json"
2852
+
2853
+ python3 - "$roles_file" "$mapping_file" "$config_path" "$state_path" <<'PY'
2854
+ import json
2855
+ import sys
2856
+ from pathlib import Path
2857
+
2858
+ roles_file, mapping_file, config_path, state_path = map(Path, sys.argv[1:])
2859
+ roles = []
2860
+ for line in roles_file.read_text(encoding="utf-8").splitlines():
2861
+ if not line:
2862
+ continue
2863
+ name, mode, description = (line.split("\t") + ["", "", ""])[:3]
2864
+ roles.append({"name": name, "mode": mode, "description": description})
2865
+
2866
+ mapping = {}
2867
+ for line in mapping_file.read_text(encoding="utf-8").splitlines():
2868
+ if not line:
2869
+ continue
2870
+ name, model, fallback = (line.split("\t") + ["", "", ""])[:3]
2871
+ mapping[name] = {"model": model, "fallback": [fallback] if fallback and fallback != model else []}
2872
+
2873
+ try:
2874
+ data = json.loads(config_path.read_text(encoding="utf-8"))
2875
+ except Exception:
2876
+ data = {}
2877
+ if not isinstance(data, dict):
2878
+ data = {}
2879
+ agents = data.setdefault("agent", {})
2880
+ for role in roles:
2881
+ selected = mapping.get(role["name"])
2882
+ if not selected:
2883
+ continue
2884
+ current = agents.get(role["name"])
2885
+ if not isinstance(current, dict):
2886
+ current = {}
2887
+ current.update({
2888
+ "mode": current.get("mode") or role["mode"],
2889
+ "description": current.get("description") or role["description"],
2890
+ "model": selected["model"],
2891
+ "fallback": selected["fallback"],
2892
+ })
2893
+ agents[role["name"]] = current
2894
+
2895
+ config_path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
2896
+ state_path.write_text(json.dumps({
2897
+ "configured": True,
2898
+ "roles": [role["name"] for role in roles],
2899
+ }, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
2900
+ PY
2901
+
2902
+ register_managed_file "$config_path" "generated:opencode-agent-model-mapper-config" "config"
2903
+ register_managed_file "$state_path" "generated:opencode-agent-model-mapper-state" "config"
2904
+ }
2905
+
2906
+ confirm_opencode_agent_model_mapping() {
2907
+ local mapping_file="$1"
2908
+ if fzf_available; then
2909
+ local summary selected
2910
+ summary="$(python3 - "$mapping_file" <<'PY'
2911
+ import sys
2912
+ from pathlib import Path
2913
+
2914
+ for line in Path(sys.argv[1]).read_text(encoding="utf-8").splitlines():
2915
+ if not line:
2916
+ continue
2917
+ name, model, fallback = (line.split("\t") + ["", "", ""])[:3]
2918
+ print(f"{name}: main={model} fallback={fallback}")
2919
+ PY
2920
+ )"
2921
+ set +e
2922
+ selected="$(printf '%s\n' "Confirm" "Cancel" | fzf \
2923
+ --ansi \
2924
+ --border \
2925
+ --height=70% \
2926
+ --layout=reverse \
2927
+ --no-sort \
2928
+ --prompt "Save OpenCode model mapping? " \
2929
+ --header "$summary")"
2930
+ local fzf_status=$?
2931
+ set -e
2932
+ [[ "$fzf_status" -eq 0 ]] || return 1
2933
+ [[ "$selected" == "Confirm" ]]
2934
+ return
2935
+ fi
2936
+
2937
+ out "agent-model-mapper selected mapping:"
2938
+ python3 - "$mapping_file" <<'PY'
2939
+ import sys
2940
+ from pathlib import Path
2941
+
2942
+ for line in Path(sys.argv[1]).read_text(encoding="utf-8").splitlines():
2943
+ if not line:
2944
+ continue
2945
+ name, model, fallback = (line.split("\t") + ["", "", ""])[:3]
2946
+ print(f" - {name}: main={model} fallback={fallback}")
2947
+ PY
2948
+ confirm_action_interactive "Write .opencode/opencode.json agent model mapping?"
2949
+ }
2950
+
2951
+ configure_opencode_agent_model_mapper_if_needed() {
2952
+ selected_agent_os_contains "opencode" || return 0
2953
+ opencode_agent_model_mapper_config_enabled || return 0
2954
+
2955
+ if ! is_interactive_terminal; then
2956
+ log "agent-model-mapper install-time setup skipped because no interactive terminal is available"
2957
+ return 0
2958
+ fi
2959
+
2960
+ local config_path="$PROJECT_DIR/.opencode/opencode.json"
2961
+ local state_path="$PROJECT_DIR/.opencode/agent-model-mapper.state.json"
2962
+ can_write_managed_file "$config_path" || return 0
2963
+ if [[ -e "$state_path" ]]; then
2964
+ can_write_managed_file "$state_path" || return 0
2965
+ fi
2966
+
2967
+ local roles_file models_file mapping_file
2968
+ roles_file="$(mktemp "${TMPDIR:-/tmp}/agentic-opencode-roles.XXXXXX")"
2969
+ models_file="$(mktemp "${TMPDIR:-/tmp}/agentic-opencode-models.XXXXXX")"
2970
+ mapping_file="$(mktemp "${TMPDIR:-/tmp}/agentic-opencode-mapping.XXXXXX")"
2971
+ opencode_mapper_read_roles > "$roles_file"
2972
+
2973
+ if [[ ! -s "$roles_file" ]]; then
2974
+ log "agent-model-mapper: skipped because .opencode/agents/*.md was not found"
2975
+ rm -f "$roles_file" "$models_file" "$mapping_file"
2976
+ return 0
2977
+ fi
2978
+
2979
+ if opencode_mapper_has_complete_mapping "$roles_file"; then
2980
+ log "agent-model-mapper: skipped because all Agentic roles already have model mappings"
2981
+ rm -f "$roles_file" "$models_file" "$mapping_file"
2982
+ return 0
2983
+ fi
2984
+
2985
+ opencode_mapper_discover_models > "$models_file"
2986
+ local models=()
2987
+ readlines models < "$models_file"
2988
+ if [[ "${#models[@]}" -eq 0 ]]; then
2989
+ models=("opencode/minimax-m2.5-free")
2990
+ fi
2991
+
2992
+ out "agent-model-mapper: choose OpenCode models for Agentic roles"
2993
+ local role_name role_mode role_description model fallback
2994
+ exec 3<&0
2995
+ while IFS=$'\t' read -r role_name role_mode role_description || [[ -n "${role_name:-}" ]]; do
2996
+ [[ -n "$role_name" ]] || continue
2997
+ model="$(choose_opencode_mapper_model "$role_name" "$role_mode" "$role_description" "main" "${models[@]}" <&3)"
2998
+ fallback="$(choose_opencode_mapper_model "$role_name" "$role_mode" "$role_description" "fallback" "${models[@]}" <&3)"
2999
+ printf '%s\t%s\t%s\n' "$role_name" "$model" "$fallback" >> "$mapping_file"
3000
+ done < "$roles_file"
3001
+
3002
+ exec 3<&-
3003
+ if ! confirm_opencode_agent_model_mapping "$mapping_file"; then
3004
+ log "agent-model-mapper: skipped by user; no files changed"
3005
+ rm -f "$roles_file" "$models_file" "$mapping_file"
3006
+ return 0
3007
+ fi
3008
+
3009
+ write_opencode_agent_model_mapping "$roles_file" "$mapping_file"
3010
+ log "agent-model-mapper: updated .opencode/opencode.json"
3011
+ rm -f "$roles_file" "$models_file" "$mapping_file"
2030
3012
  }
2031
3013
 
2032
3014
  normalize_selected_agent_os() {
@@ -2052,7 +3034,6 @@ copy_extension_for_agent() {
2052
3034
  local project_dir="$2"
2053
3035
 
2054
3036
  if [[ "$agent_os" == "$DEFAULT_AGENT_OS" ]] || [[ "$agent_os" == "agents" ]]; then
2055
- log "Agent OS '$agent_os': skipping extension copy"
2056
3037
  return
2057
3038
  fi
2058
3039
 
@@ -2064,7 +3045,6 @@ copy_extension_for_agent() {
2064
3045
  return
2065
3046
  fi
2066
3047
 
2067
- log "Copy extension: $src -> $dest"
2068
3048
  copy_dir_contents "$src" "$dest"
2069
3049
  }
2070
3050
 
@@ -2109,7 +3089,6 @@ copy_specialization_assets() {
2109
3089
  local dest_dir
2110
3090
  dest_dir="$(get_dest_dir "$target" "$bucket")"
2111
3091
  if [[ "$dest_dir" == "-" ]]; then
2112
- log "Skip $spec_key/$bucket (not supported by '$target')"
2113
3092
  continue
2114
3093
  fi
2115
3094
  unique_append "$dest_dir" dest_dirs
@@ -2118,7 +3097,6 @@ copy_specialization_assets() {
2118
3097
  local resolved_dir
2119
3098
  for resolved_dir in "${dest_dirs[@]}"; do
2120
3099
  local dest="$project_dir/$resolved_dir"
2121
- log "Copy $spec_key/$bucket -> $dest"
2122
3100
  copy_dir_contents "$src" "$dest"
2123
3101
  done
2124
3102
  done
@@ -2232,6 +3210,40 @@ generate_agents_md() {
2232
3210
  rm -f "$tmp"
2233
3211
  }
2234
3212
 
3213
+ copy_memory_md() {
3214
+ local project_dir="$1"
3215
+ local src="$REPO_ROOT/MEMORY.md"
3216
+
3217
+ if [[ ! -f "$src" ]]; then
3218
+ warn "MEMORY.md not found in knowledge base at $src; skipping"
3219
+ return
3220
+ fi
3221
+
3222
+ local outputs=()
3223
+
3224
+ if selected_agent_os_contains "opencode"; then
3225
+ unique_append "$project_dir/.opencode/MEMORY.md" outputs
3226
+ fi
3227
+
3228
+ local needs_root=false
3229
+ local agent_os
3230
+ for agent_os in "${SELECTED_AGENT_OS[@]}"; do
3231
+ if [[ "$agent_os" != "opencode" ]]; then
3232
+ needs_root=true
3233
+ break
3234
+ fi
3235
+ done
3236
+
3237
+ if [[ "$needs_root" == true ]] || ! selected_agent_os_contains "opencode"; then
3238
+ unique_append "$project_dir/MEMORY.md" outputs
3239
+ fi
3240
+
3241
+ local out
3242
+ for out in "${outputs[@]}"; do
3243
+ write_file_with_agentic_marker "$src" "$out" "generated:MEMORY.md"
3244
+ done
3245
+ }
3246
+
2235
3247
  validate_inputs() {
2236
3248
  local available_areas
2237
3249
  available_areas="$(list_areas || true)"
@@ -2299,6 +3311,8 @@ validate_inputs() {
2299
3311
  }
2300
3312
 
2301
3313
  print_report() {
3314
+ write_changed_paths_report
3315
+
2302
3316
  out
2303
3317
  out "=== Installation report ===" "$COLOR_HEADER"
2304
3318
  out "Agentic version: $(app_version_label)"
@@ -2310,26 +3324,9 @@ print_report() {
2310
3324
  out "Specializations: ${SELECTED_SPECS[*]}"
2311
3325
 
2312
3326
  out
2313
- out "Created directories:"
2314
- if [[ "${#CREATED_PATHS[@]}" -eq 0 ]]; then
2315
- out "- (none)"
2316
- else
2317
- local created_path
2318
- for created_path in "${CREATED_PATHS[@]}"; do
2319
- out "- $created_path"
2320
- done
2321
- fi
2322
-
2323
- out
2324
- out "Copied/generated paths:"
2325
- if [[ "${#COPIED_PATHS[@]}" -eq 0 ]]; then
2326
- out "- (none)"
2327
- else
2328
- local copied_path
2329
- for copied_path in "${COPIED_PATHS[@]}"; do
2330
- out "- $copied_path"
2331
- done
2332
- fi
3327
+ out "Created directories: ${#CREATED_PATHS[@]}"
3328
+ out "Copied/generated paths: ${#COPIED_PATHS[@]}"
3329
+ out "Changed paths report: $CHANGED_PATHS_REPORT_FILE"
2333
3330
 
2334
3331
  out
2335
3332
  out "Warnings:"
@@ -2469,7 +3466,15 @@ doctor_enabled() {
2469
3466
  }
2470
3467
 
2471
3468
  doctor_prompt() {
2472
- printf '%s\n' "/develop-feature напиши hello world python"
3469
+ printf '%s\n' "Reply with exactly: AGENTIC_DOCTOR_OK"
3470
+ }
3471
+
3472
+ doctor_prompt_for_agent() {
3473
+ doctor_prompt
3474
+ }
3475
+
3476
+ doctor_smoke_label() {
3477
+ printf '%s\n' "lightweight smoke"
2473
3478
  }
2474
3479
 
2475
3480
  doctor_output_has_fatal_patterns() {
@@ -2477,6 +3482,40 @@ doctor_output_has_fatal_patterns() {
2477
3482
  grep -Eiq 'MCP.*(error|failed|failure|connection|connect|startup)|plugin.*(error|failed|failure)|auth.*(required|failed)|login required|permission.*(denied|required)|SyntaxError|Traceback|Invalid regular expression flags|An unexpected critical error occurred|FatalError|RuntimeError|EPERM|EACCES|panic:' "$output_file"
2478
3483
  }
2479
3484
 
3485
+ doctor_timeout_seconds() {
3486
+ local value="${AGENTIC_DOCTOR_TIMEOUT_SECONDS:-10}"
3487
+ if [[ ! "$value" =~ ^[0-9]+$ ]] || (( value < 1 )); then
3488
+ value=10
3489
+ fi
3490
+ printf '%s\n' "$value"
3491
+ }
3492
+
3493
+ run_with_doctor_timeout() {
3494
+ local timeout_seconds="$1"
3495
+ shift
3496
+
3497
+ "$@" &
3498
+ local child_pid=$!
3499
+ local elapsed=0
3500
+ local status=0
3501
+ while kill -0 "$child_pid" 2>/dev/null; do
3502
+ if (( elapsed >= timeout_seconds )); then
3503
+ pkill -TERM -P "$child_pid" 2>/dev/null || true
3504
+ kill "$child_pid" 2>/dev/null || true
3505
+ sleep 1
3506
+ pkill -KILL -P "$child_pid" 2>/dev/null || true
3507
+ kill -9 "$child_pid" 2>/dev/null || true
3508
+ wait "$child_pid" 2>/dev/null || true
3509
+ return 124
3510
+ fi
3511
+ sleep 1
3512
+ elapsed=$((elapsed + 1))
3513
+ done
3514
+ wait "$child_pid"
3515
+ status=$?
3516
+ return "$status"
3517
+ }
3518
+
2480
3519
  doctor_copy_project() {
2481
3520
  local dest="$1"
2482
3521
  mkdir -p "$dest"
@@ -2490,14 +3529,14 @@ run_doctor_command() {
2490
3529
  local work_dir="$2"
2491
3530
  local output_file="$3"
2492
3531
  local prompt
2493
- prompt="$(doctor_prompt)"
3532
+ prompt="$(doctor_prompt_for_agent "$agent_os")"
2494
3533
 
2495
3534
  case "$agent_os" in
2496
3535
  codex)
2497
- codex exec --skip-git-repo-check --full-auto -C "$work_dir" "$prompt" >"$output_file" 2>&1
3536
+ codex exec --skip-git-repo-check --ephemeral --sandbox workspace-write -C "$work_dir" "$prompt" </dev/null >"$output_file" 2>&1
2498
3537
  ;;
2499
3538
  opencode)
2500
- opencode run --dir "$work_dir" --dangerously-skip-permissions --format json --command develop-feature "напиши hello world python" >"$output_file" 2>&1
3539
+ OPENCODE_DISABLE_AUTOUPDATE=1 opencode run --pure --dir "$work_dir" --dangerously-skip-permissions --format json --log-level ERROR "$prompt" >"$output_file" 2>&1
2501
3540
  ;;
2502
3541
  claude)
2503
3542
  (cd "$work_dir" && claude -p --permission-mode bypassPermissions --output-format stream-json "$prompt") >"$output_file" 2>&1
@@ -2522,29 +3561,40 @@ run_doctor_for_agent() {
2522
3561
  return 1
2523
3562
  fi
2524
3563
 
2525
- local work_dir output_file status
3564
+ local work_dir output_file status timeout_seconds started_at elapsed smoke_label
2526
3565
  work_dir="$doctor_root/$agent_os"
2527
3566
  output_file="$doctor_root/$agent_os.log"
3567
+ timeout_seconds="$(doctor_timeout_seconds)"
3568
+ smoke_label="$(doctor_smoke_label "$agent_os")"
2528
3569
  doctor_copy_project "$work_dir"
2529
3570
 
2530
3571
  set +e
2531
- run_doctor_command "$agent_os" "$work_dir" "$output_file"
3572
+ started_at="$(date +%s)"
3573
+ run_with_doctor_timeout "$timeout_seconds" run_doctor_command "$agent_os" "$work_dir" "$output_file"
2532
3574
  status=$?
3575
+ elapsed=$(( $(date +%s) - started_at ))
2533
3576
  set -e
2534
3577
 
3578
+ log "$agent_os doctor finished: timeout=${timeout_seconds}s exit=$status elapsed=${elapsed}s"
3579
+
2535
3580
  log_file_block "doctor $agent_os" "$output_file"
2536
3581
 
3582
+ if [[ "$status" -eq 124 || "$status" -eq 137 ]]; then
3583
+ out "❌ $agent_os: $smoke_label timed out after ${timeout_seconds}s (exit $status, elapsed ${elapsed}s, log: $output_file)"
3584
+ return 1
3585
+ fi
3586
+
2537
3587
  if [[ "$status" -ne 0 ]]; then
2538
- out "❌ $agent_os: /develop-feature smoke failed (exit $status, log: $output_file)"
3588
+ out "❌ $agent_os: $smoke_label failed (exit $status, elapsed ${elapsed}s, log: $output_file)"
2539
3589
  return 1
2540
3590
  fi
2541
3591
 
2542
3592
  if doctor_output_has_fatal_patterns "$output_file"; then
2543
- out "❌ $agent_os: /develop-feature smoke reported integration errors (log: $output_file)"
3593
+ out "❌ $agent_os: $smoke_label reported integration errors (exit $status, elapsed ${elapsed}s, log: $output_file)"
2544
3594
  return 1
2545
3595
  fi
2546
3596
 
2547
- out "✅ $agent_os: /develop-feature smoke passed"
3597
+ out "✅ $agent_os: $smoke_label passed (exit $status, elapsed ${elapsed}s)"
2548
3598
  return 0
2549
3599
  }
2550
3600
 
@@ -2572,6 +3622,7 @@ run_agentic_doctor() {
2572
3622
  out
2573
3623
  out "=== Agentic doctor ===" "$COLOR_HEADER"
2574
3624
  out "Doctor temp root: $doctor_root"
3625
+ out "Doctor timeout: $(doctor_timeout_seconds)s per agent"
2575
3626
 
2576
3627
  local failures=0
2577
3628
  for agent_os in "${selected_doctor_agents[@]}"; do
@@ -2603,8 +3654,10 @@ run_install() {
2603
3654
  ensure_dir "$PROJECT_DIR"
2604
3655
  configure_opencode_plugins_if_needed
2605
3656
  copy_extensions "$PROJECT_DIR"
3657
+ configure_opencode_agent_model_mapper_if_needed
2606
3658
  copy_specialization_assets "$PROJECT_DIR"
2607
3659
  generate_agents_md "$PROJECT_DIR"
3660
+ copy_memory_md "$PROJECT_DIR"
2608
3661
  configure_context7_if_needed
2609
3662
  configure_mempalace_if_needed
2610
3663
  write_agentic_manifest "$PROJECT_DIR"
@@ -2681,6 +3734,84 @@ prompt_with_default_fzf() {
2681
3734
  printf '%s\n' "$default"
2682
3735
  }
2683
3736
 
3737
+ prompt_text_fzf() {
3738
+ local prompt="$1"
3739
+ local default="${2:-}"
3740
+ local header="Type value and press Enter to confirm"
3741
+ if [[ -n "$default" ]]; then
3742
+ header="$header (empty = current value)"
3743
+ fi
3744
+ local fzf_args=(
3745
+ --ansi
3746
+ --border
3747
+ --height=70%
3748
+ --layout=reverse
3749
+ --cycle
3750
+ --no-sort
3751
+ --phony
3752
+ --print-query
3753
+ --bind "enter:accept"
3754
+ --prompt "$prompt "
3755
+ --header "$header"
3756
+ )
3757
+ if [[ "${#FZF_COLOR_ARGS[@]}" -gt 0 ]]; then
3758
+ fzf_args+=("${FZF_COLOR_ARGS[@]}")
3759
+ fi
3760
+
3761
+ local output selected
3762
+ if ! output="$(printf '%s\n' "<press Enter to confirm>" | fzf "${fzf_args[@]}")"; then
3763
+ return 1
3764
+ fi
3765
+ selected="$(printf '%s\n' "$output" | sed -n '1p')"
3766
+ selected="$(trim "$selected")"
3767
+ if [[ -n "$selected" ]]; then
3768
+ printf '%s\n' "$selected"
3769
+ else
3770
+ printf '%s\n' "$default"
3771
+ fi
3772
+ }
3773
+
3774
+ prompt_text_tui() {
3775
+ local prompt="$1"
3776
+ local default="${2:-}"
3777
+ local answer
3778
+ if [[ -n "$default" ]]; then
3779
+ read -r -p "$prompt [keep current]: " answer
3780
+ else
3781
+ read -r -p "$prompt: " answer
3782
+ fi
3783
+ answer="$(trim "$answer")"
3784
+ if [[ -n "$answer" ]]; then
3785
+ printf '%s\n' "$answer"
3786
+ else
3787
+ printf '%s\n' "$default"
3788
+ fi
3789
+ }
3790
+
3791
+ prompt_text_interactive() {
3792
+ local prompt="$1"
3793
+ local default="${2:-}"
3794
+ if fzf_available; then
3795
+ prompt_text_fzf "$prompt" "$default"
3796
+ else
3797
+ prompt_text_tui "$prompt" "$default"
3798
+ fi
3799
+ }
3800
+
3801
+ confirm_action_interactive() {
3802
+ local prompt="$1"
3803
+ local selected
3804
+ if fzf_available; then
3805
+ selected="$(choose_single_fzf "$prompt" "Confirm" "Cancel" || true)"
3806
+ [[ "$selected" == "Confirm" ]]
3807
+ return
3808
+ fi
3809
+ local answer
3810
+ read -r -p "$prompt [y/N]: " answer
3811
+ answer="$(trim "$answer")"
3812
+ [[ "$answer" =~ ^[Yy]([Ee][Ss])?$ ]]
3813
+ }
3814
+
2684
3815
  choose_single_by_index() {
2685
3816
  local prompt="$1"
2686
3817
  shift
@@ -3237,6 +4368,45 @@ sync_current_project_after_upgrade() {
3237
4368
  load_install_settings_from_manifest "$manifest"
3238
4369
  ensure_repo_layout
3239
4370
  run_install
4371
+ upgrade_mempalace_graph
4372
+ }
4373
+
4374
+ upgrade_mempalace_graph() {
4375
+ local project_wing shared_docs_wing
4376
+ # Only run if mempalace was enabled for this project
4377
+ if [[ ! "${AGENTIC_ENABLE_MEMPALACE:-}" =~ ^[Yy] ]]; then
4378
+ return
4379
+ fi
4380
+
4381
+ if ! command -v mempalace >/dev/null 2>&1; then
4382
+ return
4383
+ fi
4384
+
4385
+ project_wing="$(mempalace_project_wing)"
4386
+ shared_docs_wing="$(mempalace_shared_docs_wing)"
4387
+ if [[ "$DRY_RUN" == true ]]; then
4388
+ log "DRY-RUN mempalace mine \"$PROJECT_DIR\" --wing \"$project_wing\""
4389
+ if [[ -d "$PROJECT_DIR/docs" ]]; then
4390
+ log "DRY-RUN mempalace mine \"$PROJECT_DIR/docs\" --wing \"$shared_docs_wing\""
4391
+ fi
4392
+ return
4393
+ fi
4394
+
4395
+ log "Refreshing MemPalace knowledge graph for $PROJECT_DIR (wing: $project_wing)"
4396
+ if mempalace mine "$PROJECT_DIR" --wing "$project_wing" >/dev/null 2>&1; then
4397
+ log "MemPalace graph updated"
4398
+ else
4399
+ warn "mempalace mine failed; graph may be stale — run manually: mempalace mine \"$PROJECT_DIR\" --wing \"$project_wing\""
4400
+ fi
4401
+
4402
+ if [[ -d "$PROJECT_DIR/docs" ]]; then
4403
+ log "Refreshing shared MemPalace docs wing from $PROJECT_DIR/docs"
4404
+ if mempalace mine "$PROJECT_DIR/docs" --wing "$shared_docs_wing" >/dev/null 2>&1; then
4405
+ log "MemPalace shared docs wing updated"
4406
+ else
4407
+ warn "mempalace docs mine failed; shared docs may be stale — run manually: mempalace mine \"$PROJECT_DIR/docs\" --wing \"$shared_docs_wing\""
4408
+ fi
4409
+ fi
3240
4410
  }
3241
4411
 
3242
4412
  parse_theme_option() {