@jetrabbits/agentic 0.0.4 → 0.1.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.
Files changed (49) hide show
  1. package/AGENTS.md +16 -0
  2. package/Makefile +40 -0
  3. package/README.md +3 -0
  4. package/UPGRADE.md +61 -0
  5. package/agentic +1236 -13
  6. package/areas/software/full-stack/AGENTS.md +1 -4
  7. package/areas/software/full-stack/workflows/debug-issue.md +2 -2
  8. package/docs/agentic-lifecycle.md +114 -0
  9. package/docs/agentic-token-minimization/README.md +81 -0
  10. package/docs/agentic-usage.md +157 -0
  11. package/docs/catalog.schema.json +203 -0
  12. package/docs/guidance-updates/2026-04-10-software-devops-best-practices.md +26 -0
  13. package/docs/opencode_prepare_agents.md +40 -0
  14. package/docs/opencode_setup.md +48 -0
  15. package/docs/prompt-format.md +80 -0
  16. package/docs/site/README.md +44 -0
  17. package/docs/site/app.js +127 -0
  18. package/docs/site/catalog.json +5002 -0
  19. package/docs/site/index.html +52 -0
  20. package/docs/site/styles.css +177 -0
  21. package/extensions/codex/agents/developer.toml +1 -1
  22. package/extensions/codex/agents/devops-engineer.toml +1 -1
  23. package/extensions/codex/agents/product-owner.toml +1 -1
  24. package/extensions/codex/agents/team-lead.toml +1 -1
  25. package/extensions/opencode/plugins/model-checker.json +2 -3
  26. package/extensions/opencode/plugins/model-checker.ts +23 -0
  27. package/extensions/opencode/plugins/telegram-notification.ts +33 -5
  28. package/package.json +6 -2
  29. package/scripts/assess_area_quality.py +216 -0
  30. package/scripts/build_docs_catalog.py +283 -0
  31. package/scripts/lint_prompts.py +113 -0
  32. package/areas/software/full-stack/skills/bash-pro/SKILL.md +0 -310
  33. package/areas/software/full-stack/skills/python-pro/SKILL.md +0 -158
  34. package/areas/software/full-stack/skills/skill-creator/LICENSE.txt +0 -202
  35. package/areas/software/full-stack/skills/skill-creator/SKILL.md +0 -356
  36. package/areas/software/full-stack/skills/skill-creator/references/output-patterns.md +0 -82
  37. package/areas/software/full-stack/skills/skill-creator/references/workflows.md +0 -28
  38. package/areas/software/full-stack/skills/skill-creator/scripts/init_skill.py +0 -303
  39. package/areas/software/full-stack/skills/skill-creator/scripts/package_skill.py +0 -110
  40. package/areas/software/full-stack/skills/skill-creator/scripts/quick_validate.py +0 -95
  41. package/extensions/codex/skills/babysit-pr/SKILL.md +0 -187
  42. package/extensions/codex/skills/babysit-pr/agents/openai.yaml +0 -4
  43. package/extensions/codex/skills/babysit-pr/references/github-api-notes.md +0 -72
  44. package/extensions/codex/skills/babysit-pr/references/heuristics.md +0 -58
  45. package/extensions/codex/skills/babysit-pr/scripts/gh_pr_watch.py +0 -806
  46. package/extensions/codex/skills/babysit-pr/scripts/test_gh_pr_watch.py +0 -155
  47. package/extensions/opencode/skills/code_review_expert/SKILL.md +0 -144
  48. package/extensions/opencode/skills/design_expert/SKILL.md +0 -42
  49. package/extensions/opencode/skills/qa_expert/SKILL.md +0 -116
package/agentic CHANGED
@@ -11,6 +11,8 @@ APP_NAME="agentic"
11
11
  APP_TITLE="Agentic Installer"
12
12
  APP_TUI_TITLE="Agentic installer (TUI mode)"
13
13
  APP_REPO_URL="https://github.com/sawrus/agent-guides.git"
14
+ APP_REPO_LINK="https://github.com/sawrus/agent-guides"
15
+ PROJECT_MANIFEST_NAME=".agentic.json"
14
16
 
15
17
  DEFAULT_AGENT_OS="default"
16
18
  STATIC_AGENT_OS=(default opencode codex claude antigravity cursor agents)
@@ -21,6 +23,7 @@ XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
21
23
  XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
22
24
  APP_CONFIG_DIR="$XDG_CONFIG_HOME/$APP_NAME"
23
25
  APP_CONFIG_FILE="$APP_CONFIG_DIR/config"
26
+ OPENCODE_PLUGIN_CONFIG_FILE="$APP_CONFIG_DIR/opencode-plugins.json"
24
27
  APP_DATA_DIR="$XDG_DATA_HOME/$APP_NAME"
25
28
  APP_REPO_DIR="$APP_DATA_DIR/repo"
26
29
 
@@ -47,8 +50,12 @@ SELF_INSTALL_WITH_FZF=false
47
50
 
48
51
  CREATED_PATHS=()
49
52
  COPIED_PATHS=()
53
+ MANAGED_RECORDS=()
54
+ SKIPPED_MANAGED_PATHS=()
50
55
  WARNINGS=()
51
56
 
57
+ CONTEXT7_API_KEY="${CONTEXT7_API_KEY:-}"
58
+
52
59
  COLOR_RESET=""
53
60
  COLOR_HEADER=""
54
61
  COLOR_INFO=""
@@ -505,6 +512,385 @@ ensure_dir() {
505
512
  unique_append "$path" CREATED_PATHS
506
513
  }
507
514
 
515
+ ensure_python_available() {
516
+ if ! command -v python3 >/dev/null 2>&1; then
517
+ error "python3 is required for managed install metadata and generated markers"
518
+ exit 1
519
+ fi
520
+ }
521
+
522
+ selected_agent_os_contains() {
523
+ local expected="$1"
524
+ local agent
525
+ for agent in "${SELECTED_AGENT_OS[@]}"; do
526
+ if [[ "$agent" == "$expected" ]]; then
527
+ return 0
528
+ fi
529
+ done
530
+ return 1
531
+ }
532
+
533
+ project_manifest_path() {
534
+ printf '%s\n' "$PROJECT_DIR/$PROJECT_MANIFEST_NAME"
535
+ }
536
+
537
+ project_rel_path() {
538
+ local path="$1"
539
+ local rel="${path#"$PROJECT_DIR"/}"
540
+ printf '%s\n' "$rel"
541
+ }
542
+
543
+ hash_file() {
544
+ local path="$1"
545
+ if command -v shasum >/dev/null 2>&1; then
546
+ shasum -a 256 "$path" | awk '{print $1}'
547
+ elif command -v sha256sum >/dev/null 2>&1; then
548
+ sha256sum "$path" | awk '{print $1}'
549
+ else
550
+ error "shasum or sha256sum is required to track managed files"
551
+ exit 1
552
+ fi
553
+ }
554
+
555
+ manifest_has_path() {
556
+ local rel="$1"
557
+ local manifest
558
+ manifest="$(project_manifest_path)"
559
+ [[ -f "$manifest" ]] || return 1
560
+ python3 - "$manifest" "$rel" <<'PY'
561
+ import json
562
+ import sys
563
+ from pathlib import Path
564
+
565
+ manifest = Path(sys.argv[1])
566
+ rel = sys.argv[2]
567
+ try:
568
+ data = json.loads(manifest.read_text(encoding="utf-8"))
569
+ except Exception:
570
+ sys.exit(1)
571
+ for item in data.get("managed_files", []):
572
+ if item.get("path") == rel:
573
+ sys.exit(0)
574
+ sys.exit(1)
575
+ PY
576
+ }
577
+
578
+ manifest_hash_for_path() {
579
+ local rel="$1"
580
+ local manifest
581
+ manifest="$(project_manifest_path)"
582
+ [[ -f "$manifest" ]] || return 0
583
+ python3 - "$manifest" "$rel" <<'PY'
584
+ import json
585
+ import sys
586
+ from pathlib import Path
587
+
588
+ manifest = Path(sys.argv[1])
589
+ rel = sys.argv[2]
590
+ try:
591
+ data = json.loads(manifest.read_text(encoding="utf-8"))
592
+ except Exception:
593
+ sys.exit(0)
594
+ for item in data.get("managed_files", []):
595
+ if item.get("path") == rel:
596
+ print(item.get("content_hash", ""))
597
+ break
598
+ PY
599
+ }
600
+
601
+ can_write_managed_file() {
602
+ local dest="$1"
603
+ local rel
604
+ rel="$(project_rel_path "$dest")"
605
+
606
+ if [[ -f "$(project_manifest_path)" ]]; then
607
+ if ! manifest_has_path "$rel"; then
608
+ if [[ -e "$dest" ]]; then
609
+ warn "Skipping unmanaged target on rerun: $rel"
610
+ unique_append "$rel" SKIPPED_MANAGED_PATHS
611
+ return 1
612
+ fi
613
+ return 0
614
+ fi
615
+
616
+ if [[ -f "$dest" ]]; then
617
+ local expected_hash current_hash
618
+ expected_hash="$(manifest_hash_for_path "$rel")"
619
+ if [[ -n "$expected_hash" ]]; then
620
+ current_hash="$(hash_file "$dest")"
621
+ if [[ "$current_hash" != "$expected_hash" ]]; then
622
+ warn "Skipping user-modified managed file: $rel"
623
+ unique_append "$rel" SKIPPED_MANAGED_PATHS
624
+ return 1
625
+ fi
626
+ fi
627
+ fi
628
+ fi
629
+
630
+ return 0
631
+ }
632
+
633
+ register_managed_file() {
634
+ local dest="$1"
635
+ local source_ref="$2"
636
+ local marker="$3"
637
+ local rel
638
+ rel="$(project_rel_path "$dest")"
639
+ local digest
640
+ digest="$(hash_file "$dest")"
641
+ MANAGED_RECORDS+=("$rel|$source_ref|$digest|$marker")
642
+ unique_append "$dest" COPIED_PATHS
643
+ }
644
+
645
+ record_agentic_event() {
646
+ local kind="$1"
647
+ local value="${2:-}"
648
+
649
+ case "$kind" in
650
+ DIR)
651
+ unique_append "$value" CREATED_PATHS
652
+ ;;
653
+ COPIED)
654
+ unique_append "$value" COPIED_PATHS
655
+ ;;
656
+ RECORD)
657
+ MANAGED_RECORDS+=("$value")
658
+ ;;
659
+ SKIP)
660
+ unique_append "$value" SKIPPED_MANAGED_PATHS
661
+ ;;
662
+ WARN)
663
+ warn "$value"
664
+ ;;
665
+ esac
666
+ }
667
+
668
+ write_file_with_agentic_marker() {
669
+ local src="$1"
670
+ local dest="$2"
671
+ local source_ref="$3"
672
+
673
+ if [[ "$DRY_RUN" == true ]]; then
674
+ log "DRY-RUN write managed file $dest"
675
+ unique_append "$dest" COPIED_PATHS
676
+ return
677
+ fi
678
+
679
+ can_write_managed_file "$dest" || return 0
680
+
681
+ ensure_dir "$(dirname -- "$dest")"
682
+ python3 - "$src" "$dest" "$source_ref" "$APP_REPO_LINK" <<'PY'
683
+ import json
684
+ import sys
685
+ from pathlib import Path
686
+
687
+ src = Path(sys.argv[1])
688
+ dest = Path(sys.argv[2])
689
+ source_ref = sys.argv[3]
690
+ repo = sys.argv[4]
691
+ text = src.read_text(encoding="utf-8")
692
+ suffix = dest.suffix.lower()
693
+ marker = f"Generated by agentic; source: {source_ref}; repository: {repo}"
694
+
695
+
696
+ def yaml_quote(value: str) -> str:
697
+ return json.dumps(value, ensure_ascii=False)
698
+
699
+
700
+ def markdown_with_marker(body: str) -> str:
701
+ block = (
702
+ "agentic:\n"
703
+ " generated_by: agentic\n"
704
+ f" source: {yaml_quote(source_ref)}\n"
705
+ f" repository: {yaml_quote(repo)}\n"
706
+ )
707
+ if body.startswith("---\n"):
708
+ end = body.find("\n---", 4)
709
+ if end != -1:
710
+ return body[: end + 1] + block + body[end + 1 :]
711
+ return (
712
+ "---\n"
713
+ + block
714
+ + "---\n"
715
+ + body
716
+ )
717
+
718
+
719
+ def commented(body: str, prefix: str) -> str:
720
+ line = f"{prefix} {marker}\n"
721
+ if body.startswith("#!"):
722
+ first, sep, rest = body.partition("\n")
723
+ if sep:
724
+ return first + sep + line + rest
725
+ return line + body
726
+
727
+
728
+ if suffix == ".md":
729
+ output = markdown_with_marker(text)
730
+ elif suffix == ".json":
731
+ data = json.loads(text)
732
+ if not isinstance(data, dict):
733
+ raise SystemExit(f"Cannot add agentic metadata to non-object JSON: {dest}")
734
+ output = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
735
+ elif suffix in {".ts", ".tsx", ".js", ".jsx", ".css"}:
736
+ output = commented(text, "//")
737
+ elif suffix in {".sh", ".toml", ".py", ".yml", ".yaml"}:
738
+ output = commented(text, "#")
739
+ else:
740
+ output = commented(text, "#")
741
+
742
+ dest.write_text(output, encoding="utf-8")
743
+ PY
744
+ register_managed_file "$dest" "$source_ref" "internal"
745
+ }
746
+
747
+ write_agentic_manifest() {
748
+ local project_dir="$1"
749
+ local manifest="$project_dir/$PROJECT_MANIFEST_NAME"
750
+
751
+ if [[ "$DRY_RUN" == true ]]; then
752
+ log "DRY-RUN write $manifest"
753
+ return
754
+ fi
755
+
756
+ local records_file skipped_file
757
+ records_file="$(mktemp "${TMPDIR:-/tmp}/agentic-records.XXXXXX")"
758
+ skipped_file="$(mktemp "${TMPDIR:-/tmp}/agentic-skipped.XXXXXX")"
759
+ if [[ "${#MANAGED_RECORDS[@]}" -gt 0 ]]; then
760
+ printf '%s\n' "${MANAGED_RECORDS[@]}" > "$records_file"
761
+ else
762
+ : > "$records_file"
763
+ fi
764
+ if [[ "${#SKIPPED_MANAGED_PATHS[@]}" -gt 0 ]]; then
765
+ printf '%s\n' "${SKIPPED_MANAGED_PATHS[@]}" > "$skipped_file"
766
+ else
767
+ : > "$skipped_file"
768
+ fi
769
+
770
+ local agent_os_csv areas_csv specs_csv
771
+ local old_ifs="$IFS"
772
+ IFS=,
773
+ agent_os_csv="${SELECTED_AGENT_OS[*]}"
774
+ areas_csv="${SELECTED_AREAS[*]}"
775
+ specs_csv="${SELECTED_SPECS[*]}"
776
+ IFS="$old_ifs"
777
+
778
+ python3 - "$manifest" "$records_file" "$skipped_file" "$APP_REPO_LINK" "$REPO_ROOT" "$agent_os_csv" "$areas_csv" "$specs_csv" <<'PY'
779
+ import json
780
+ import sys
781
+ from datetime import datetime, timezone
782
+ from pathlib import Path
783
+
784
+ manifest = Path(sys.argv[1])
785
+ records_file = Path(sys.argv[2])
786
+ skipped_file = Path(sys.argv[3])
787
+ repo_link = sys.argv[4]
788
+ repo_root = sys.argv[5]
789
+ agent_os = [x for x in sys.argv[6].split(",") if x]
790
+ areas = [x for x in sys.argv[7].split(",") if x]
791
+ specs = [x for x in sys.argv[8].split(",") if x]
792
+ now = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
793
+
794
+ existing = {}
795
+ created_at = now
796
+ if manifest.exists():
797
+ try:
798
+ old = json.loads(manifest.read_text(encoding="utf-8"))
799
+ created_at = old.get("created_at", created_at)
800
+ for item in old.get("managed_files", []):
801
+ if item.get("path"):
802
+ existing[item["path"]] = item
803
+ except Exception:
804
+ existing = {}
805
+
806
+ for line in records_file.read_text(encoding="utf-8").splitlines():
807
+ if not line:
808
+ continue
809
+ path, source, digest, marker = (line.split("|", 3) + ["", "", "", ""])[:4]
810
+ existing[path] = {
811
+ "path": path,
812
+ "source": source,
813
+ "content_hash": digest,
814
+ "marker": marker,
815
+ "updated_at": now,
816
+ }
817
+
818
+ skipped = [x for x in skipped_file.read_text(encoding="utf-8").splitlines() if x]
819
+ data = {
820
+ "_agentic": {
821
+ "generated_by": "agentic",
822
+ "repository": repo_link,
823
+ },
824
+ "version": 1,
825
+ "created_at": created_at,
826
+ "updated_at": now,
827
+ "settings": {
828
+ "agent_os": agent_os,
829
+ "areas": areas,
830
+ "specializations": specs,
831
+ "source_repo": repo_link,
832
+ "source_checkout": repo_root,
833
+ },
834
+ "managed_files": sorted(existing.values(), key=lambda x: x["path"]),
835
+ "skipped_files": skipped,
836
+ }
837
+ manifest.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
838
+ PY
839
+ rm -f "$records_file" "$skipped_file"
840
+ unique_append "$manifest" COPIED_PATHS
841
+ }
842
+
843
+ load_install_settings_from_manifest() {
844
+ local manifest="$1"
845
+ [[ -f "$manifest" ]] || return 1
846
+ ensure_python_available
847
+
848
+ local values=()
849
+ readlines values < <(python3 - "$manifest" <<'PY'
850
+ import json
851
+ import sys
852
+ from pathlib import Path
853
+
854
+ data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
855
+ settings = data.get("settings", {})
856
+ for key in ("agent_os", "areas", "specializations"):
857
+ print("::" + key)
858
+ for value in settings.get(key, []):
859
+ print(value)
860
+ PY
861
+ )
862
+
863
+ local section=""
864
+ local loaded_agent_os=()
865
+ local loaded_areas=()
866
+ local loaded_specs=()
867
+ local value
868
+ for value in "${values[@]}"; do
869
+ case "$value" in
870
+ "::agent_os") section="agent_os" ;;
871
+ "::areas") section="areas" ;;
872
+ "::specializations") section="specializations" ;;
873
+ *)
874
+ case "$section" in
875
+ agent_os) loaded_agent_os+=("$value") ;;
876
+ areas) loaded_areas+=("$value") ;;
877
+ specializations) loaded_specs+=("$value") ;;
878
+ esac
879
+ ;;
880
+ esac
881
+ done
882
+
883
+ if [[ "${#SELECTED_AGENT_OS[@]}" -eq 1 && "${SELECTED_AGENT_OS[0]}" == "$DEFAULT_AGENT_OS" && "${#loaded_agent_os[@]}" -gt 0 ]]; then
884
+ SELECTED_AGENT_OS=("${loaded_agent_os[@]}")
885
+ fi
886
+ if [[ "${#SELECTED_AREAS[@]}" -eq 0 && "${#loaded_areas[@]}" -gt 0 ]]; then
887
+ SELECTED_AREAS=("${loaded_areas[@]}")
888
+ fi
889
+ if [[ "${#SELECTED_SPECS[@]}" -eq 0 && "${#loaded_specs[@]}" -gt 0 ]]; then
890
+ SELECTED_SPECS=("${loaded_specs[@]}")
891
+ fi
892
+ }
893
+
508
894
  path_ref_for_shell_export() {
509
895
  local dir="$1"
510
896
  if [[ "$dir" == "$HOME/"* ]]; then
@@ -583,11 +969,676 @@ copy_dir_contents() {
583
969
  local dest="$2"
584
970
  ensure_dir "$dest"
585
971
  if [[ "$DRY_RUN" == true ]]; then
586
- log "DRY-RUN cp -a $src/. $dest/"
972
+ log "DRY-RUN copy managed contents $src -> $dest"
973
+ unique_append "$dest" COPIED_PATHS
974
+ return
975
+ fi
976
+
977
+ local event kind value events_file
978
+ events_file="$(mktemp "${TMPDIR:-/tmp}/agentic-copy-events.XXXXXX")"
979
+ python3 - "$src" "$dest" "$REPO_ROOT" "$PROJECT_DIR" "$(project_manifest_path)" "$APP_REPO_LINK" > "$events_file" <<'PY'
980
+ import hashlib
981
+ import json
982
+ import sys
983
+ from pathlib import Path
984
+
985
+ src = Path(sys.argv[1])
986
+ dest_root = Path(sys.argv[2])
987
+ repo_root = Path(sys.argv[3])
988
+ project_dir = Path(sys.argv[4])
989
+ manifest = Path(sys.argv[5])
990
+ repo = sys.argv[6]
991
+
992
+
993
+ def emit(kind: str, value: str) -> None:
994
+ print(f"{kind}\t{value}")
995
+
996
+
997
+ def sha256(path: Path) -> str:
998
+ h = hashlib.sha256()
999
+ with path.open("rb") as fh:
1000
+ for chunk in iter(lambda: fh.read(1024 * 1024), b""):
1001
+ h.update(chunk)
1002
+ return h.hexdigest()
1003
+
1004
+
1005
+ managed = None
1006
+ if manifest.exists():
1007
+ managed = {}
1008
+ try:
1009
+ data = json.loads(manifest.read_text(encoding="utf-8"))
1010
+ for item in data.get("managed_files", []):
1011
+ rel = item.get("path")
1012
+ if rel:
1013
+ managed[rel] = item.get("content_hash", "")
1014
+ except Exception:
1015
+ managed = {}
1016
+
1017
+
1018
+ def rel_to_project(path: Path) -> str:
1019
+ try:
1020
+ return str(path.relative_to(project_dir))
1021
+ except ValueError:
1022
+ return str(path)
1023
+
1024
+
1025
+ def rel_to_repo(path: Path) -> str:
1026
+ try:
1027
+ return str(path.relative_to(repo_root))
1028
+ except ValueError:
1029
+ return str(path)
1030
+
1031
+
1032
+ def yaml_quote(value: str) -> str:
1033
+ return json.dumps(value, ensure_ascii=False)
1034
+
1035
+
1036
+ def markdown_with_marker(body: str, source_ref: str) -> str:
1037
+ block = (
1038
+ "agentic:\n"
1039
+ " generated_by: agentic\n"
1040
+ f" source: {yaml_quote(source_ref)}\n"
1041
+ f" repository: {yaml_quote(repo)}\n"
1042
+ )
1043
+ if body.startswith("---\n"):
1044
+ end = body.find("\n---", 4)
1045
+ if end != -1:
1046
+ return body[: end + 1] + block + body[end + 1 :]
1047
+ return "---\n" + block + "---\n" + body
1048
+
1049
+
1050
+ def commented(body: str, prefix: str, source_ref: str) -> str:
1051
+ marker = f"Generated by agentic; source: {source_ref}; repository: {repo}"
1052
+ line = f"{prefix} {marker}\n"
1053
+ if body.startswith("#!"):
1054
+ first, sep, rest = body.partition("\n")
1055
+ if sep:
1056
+ return first + sep + line + rest
1057
+ return line + body
1058
+
1059
+
1060
+ def add_marker(file_path: Path, target: Path, source_ref: str) -> str:
1061
+ text = file_path.read_text(encoding="utf-8")
1062
+ suffix = target.suffix.lower()
1063
+ if suffix == ".md":
1064
+ return markdown_with_marker(text, source_ref)
1065
+ if suffix == ".json":
1066
+ data = json.loads(text)
1067
+ if not isinstance(data, dict):
1068
+ raise SystemExit(f"Cannot add agentic metadata to non-object JSON: {target}")
1069
+ return json.dumps(data, indent=2, ensure_ascii=False) + "\n"
1070
+ if suffix in {".ts", ".tsx", ".js", ".jsx", ".css"}:
1071
+ return commented(text, "//", source_ref)
1072
+ if suffix in {".sh", ".toml", ".py", ".yml", ".yaml"}:
1073
+ return commented(text, "#", source_ref)
1074
+ return commented(text, "#", source_ref)
1075
+
1076
+
1077
+ emit("DIR", str(dest_root))
1078
+ for file_path in sorted(p for p in src.rglob("*") if p.is_file()):
1079
+ rel = file_path.relative_to(src)
1080
+ target = dest_root / rel
1081
+ project_rel = rel_to_project(target)
1082
+ source_ref = rel_to_repo(file_path)
1083
+
1084
+ if managed is not None:
1085
+ if project_rel not in managed:
1086
+ if target.exists():
1087
+ emit("WARN", f"Skipping unmanaged target on rerun: {project_rel}")
1088
+ emit("SKIP", project_rel)
1089
+ continue
1090
+ expected_hash = managed.get(project_rel, "")
1091
+ if target.exists() and expected_hash and sha256(target) != expected_hash:
1092
+ emit("WARN", f"Skipping user-modified managed file: {project_rel}")
1093
+ emit("SKIP", project_rel)
1094
+ continue
1095
+
1096
+ target.parent.mkdir(parents=True, exist_ok=True)
1097
+ emit("DIR", str(target.parent))
1098
+ target.write_text(add_marker(file_path, target, source_ref), encoding="utf-8")
1099
+ digest = sha256(target)
1100
+ emit("RECORD", f"{project_rel}|{source_ref}|{digest}|internal")
1101
+ emit("COPIED", str(target))
1102
+ PY
1103
+ while IFS= read -r event || [[ -n "$event" ]]; do
1104
+ kind="${event%% *}"
1105
+ if [[ "$kind" == "$event" ]]; then
1106
+ value=""
1107
+ else
1108
+ value="${event#* }"
1109
+ fi
1110
+ record_agentic_event "$kind" "$value"
1111
+ done < "$events_file"
1112
+ rm -f "$events_file"
1113
+ }
1114
+
1115
+ write_generated_text_file() {
1116
+ local dest="$1"
1117
+ local source_ref="$2"
1118
+ local content="$3"
1119
+
1120
+ if [[ "$DRY_RUN" == true ]]; then
1121
+ log "DRY-RUN write generated file $dest"
1122
+ unique_append "$dest" COPIED_PATHS
1123
+ return
1124
+ fi
1125
+
1126
+ can_write_managed_file "$dest" || return 0
1127
+ ensure_dir "$(dirname -- "$dest")"
1128
+
1129
+ local tmp
1130
+ tmp="$(mktemp "${TMPDIR:-/tmp}/agentic-generated.XXXXXX")"
1131
+ printf '%s' "$content" > "$tmp"
1132
+ write_file_with_agentic_marker "$tmp" "$dest" "$source_ref"
1133
+ rm -f "$tmp"
1134
+ }
1135
+
1136
+ write_json_file_with_agentic_metadata() {
1137
+ local dest="$1"
1138
+ local source_ref="$2"
1139
+ local python_body="$3"
1140
+
1141
+ if [[ "$DRY_RUN" == true ]]; then
1142
+ log "DRY-RUN update JSON managed file $dest"
1143
+ unique_append "$dest" COPIED_PATHS
1144
+ return
1145
+ fi
1146
+
1147
+ can_write_managed_file "$dest" || return 0
1148
+ ensure_dir "$(dirname -- "$dest")"
1149
+ python3 - "$dest" "$source_ref" "$APP_REPO_LINK" "$CONTEXT7_API_KEY" "$python_body" <<'PY'
1150
+ import json
1151
+ import sys
1152
+ from pathlib import Path
1153
+
1154
+ path = Path(sys.argv[1])
1155
+ source_ref = sys.argv[2]
1156
+ repo = sys.argv[3]
1157
+ context7_api_key = sys.argv[4]
1158
+ body = sys.argv[5]
1159
+
1160
+ data = {}
1161
+ if path.exists():
1162
+ try:
1163
+ data = json.loads(path.read_text(encoding="utf-8"))
1164
+ except Exception:
1165
+ data = {}
1166
+ if not isinstance(data, dict):
1167
+ data = {}
1168
+
1169
+ namespace = {
1170
+ "data": data,
1171
+ "context7_api_key": context7_api_key,
1172
+ }
1173
+ exec(body, namespace)
1174
+ data = namespace["data"]
1175
+ path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
1176
+ PY
1177
+ register_managed_file "$dest" "$source_ref" "internal"
1178
+ }
1179
+
1180
+ write_json_config_file() {
1181
+ local dest="$1"
1182
+ local source_ref="$2"
1183
+ local python_body="$3"
1184
+
1185
+ if [[ "$DRY_RUN" == true ]]; then
1186
+ log "DRY-RUN update JSON config file $dest"
1187
+ unique_append "$dest" COPIED_PATHS
1188
+ return
1189
+ fi
1190
+
1191
+ can_write_managed_file "$dest" || return 0
1192
+ ensure_dir "$(dirname -- "$dest")"
1193
+ python3 - "$dest" "$CONTEXT7_API_KEY" "$python_body" <<'PY'
1194
+ import json
1195
+ import sys
1196
+ from pathlib import Path
1197
+
1198
+ path = Path(sys.argv[1])
1199
+ context7_api_key = sys.argv[2]
1200
+ body = sys.argv[3]
1201
+
1202
+ data = {}
1203
+ if path.exists():
1204
+ try:
1205
+ data = json.loads(path.read_text(encoding="utf-8"))
1206
+ except Exception:
1207
+ data = {}
1208
+ if not isinstance(data, dict):
1209
+ data = {}
1210
+
1211
+ namespace = {
1212
+ "data": data,
1213
+ "context7_api_key": context7_api_key,
1214
+ }
1215
+ exec(body, namespace)
1216
+ data = namespace["data"]
1217
+ path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
1218
+ PY
1219
+ register_managed_file "$dest" "$source_ref" "config"
1220
+ }
1221
+
1222
+ write_context7_opencode_config() {
1223
+ local dest="$PROJECT_DIR/opencode.json"
1224
+ local body
1225
+ body='
1226
+ mcp = data.setdefault("mcp", {})
1227
+ context7 = {
1228
+ "type": "remote",
1229
+ "url": "https://mcp.context7.com/mcp",
1230
+ "enabled": True,
1231
+ }
1232
+ if context7_api_key:
1233
+ context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
1234
+ mcp["context7"] = context7
1235
+ '
1236
+ write_json_config_file "$dest" "generated:context7-opencode-config" "$body"
1237
+ }
1238
+
1239
+ write_context7_opencode_legacy_config() {
1240
+ local dest="$PROJECT_DIR/.opencode/opencode.json"
1241
+ local body
1242
+ body='
1243
+ mcp = data.setdefault("mcp", {})
1244
+ context7 = {
1245
+ "type": "remote",
1246
+ "url": "https://mcp.context7.com/mcp",
1247
+ "enabled": True,
1248
+ }
1249
+ if context7_api_key:
1250
+ context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
1251
+ mcp["context7"] = context7
1252
+ '
1253
+ write_json_file_with_agentic_metadata "$dest" "generated:context7-opencode-legacy-config" "$body"
1254
+ }
1255
+
1256
+ write_context7_codex_config() {
1257
+ local dest="$PROJECT_DIR/.codex/config.toml"
1258
+ local headers=""
1259
+ if [[ -n "$CONTEXT7_API_KEY" ]]; then
1260
+ local escaped_key
1261
+ escaped_key="${CONTEXT7_API_KEY//\\/\\\\}"
1262
+ escaped_key="${escaped_key//\"/\\\"}"
1263
+ headers="http_headers = { \"CONTEXT7_API_KEY\" = \"$escaped_key\" }
1264
+ "
1265
+ fi
1266
+ write_generated_text_file "$dest" "generated:context7-codex-config" "[mcp_servers.context7]
1267
+ url = \"https://mcp.context7.com/mcp\"
1268
+ ${headers}"
1269
+ }
1270
+
1271
+ write_context7_claude_config() {
1272
+ local dest="$PROJECT_DIR/.mcp.json"
1273
+ local body
1274
+ body='
1275
+ mcp_servers = data.setdefault("mcpServers", {})
1276
+ context7 = {
1277
+ "type": "http",
1278
+ "url": "https://mcp.context7.com/mcp",
1279
+ }
1280
+ if context7_api_key:
1281
+ context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
1282
+ mcp_servers["context7"] = context7
1283
+ '
1284
+ write_json_config_file "$dest" "generated:context7-claude-config" "$body"
1285
+ }
1286
+
1287
+ write_context7_cursor_config() {
1288
+ local dest="$PROJECT_DIR/.cursor/mcp.json"
1289
+ local body
1290
+ body='
1291
+ mcp_servers = data.setdefault("mcpServers", {})
1292
+ context7 = {
1293
+ "url": "https://mcp.context7.com/mcp",
1294
+ }
1295
+ if context7_api_key:
1296
+ context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
1297
+ mcp_servers["context7"] = context7
1298
+ '
1299
+ write_json_config_file "$dest" "generated:context7-cursor-config" "$body"
1300
+ }
1301
+
1302
+
1303
+ write_context7_kilocode_config() {
1304
+ local dest="$PROJECT_DIR/.kilocode/mcp.json"
1305
+ local body
1306
+ body='
1307
+ mcp_servers = data.setdefault("mcpServers", {})
1308
+ context7 = {
1309
+ "url": "https://mcp.context7.com/mcp",
1310
+ }
1311
+ if context7_api_key:
1312
+ context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
1313
+ mcp_servers["context7"] = context7
1314
+ '
1315
+ write_json_config_file "$dest" "generated:context7-kilocode-config" "$body"
1316
+ }
1317
+
1318
+ write_context7_antigravity_config() {
1319
+ local dest="$HOME/.gemini/antigravity/mcp_config.json"
1320
+ local body
1321
+ body='
1322
+ mcp_servers = data.setdefault("mcpServers", {})
1323
+ context7 = {
1324
+ "url": "https://mcp.context7.com/mcp",
1325
+ }
1326
+ if context7_api_key:
1327
+ context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
1328
+ mcp_servers["context7"] = context7
1329
+ '
1330
+ write_json_config_file "$dest" "generated:context7-antigravity-config" "$body"
1331
+ }
1332
+
1333
+ write_context7_gemini_config() {
1334
+ local dest="$PROJECT_DIR/.gemini/settings.json"
1335
+ local body
1336
+ body='
1337
+ mcp_servers = data.setdefault("mcpServers", {})
1338
+ context7 = {
1339
+ "httpUrl": "https://mcp.context7.com/mcp",
1340
+ }
1341
+ if context7_api_key:
1342
+ context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
1343
+ mcp_servers["context7"] = context7
1344
+ '
1345
+ write_json_config_file "$dest" "generated:context7-gemini-config" "$body"
1346
+ }
1347
+
1348
+ write_mempalace_opencode_config() {
1349
+ local dest="$1"
1350
+ local body
1351
+ body='
1352
+ mcp = data.setdefault("mcp", {})
1353
+ mcp["mempalace"] = {"type": "local", "command": ["mempalace-mcp", "--palace", ".mempalace"]}
1354
+ '
1355
+ write_json_config_file "$dest" "generated:mempalace-opencode-config" "$body"
1356
+ }
1357
+
1358
+ write_mempalace_codex_config() {
1359
+ local dest="$PROJECT_DIR/.codex/config.toml"
1360
+ local body
1361
+ body="$(python3 - "$dest" <<'PYCODE'
1362
+ import pathlib, sys
1363
+ path = pathlib.Path(sys.argv[1])
1364
+ text = path.read_text(encoding='utf-8') if path.exists() else ''
1365
+ block = "[mcp_servers.mempalace]\ncommand = \"mempalace-mcp\"\nargs = [\"--palace\", \".mempalace\"]\n"
1366
+ if block not in text:
1367
+ if text and not text.endswith("\n"):
1368
+ text += "\n"
1369
+ text += block
1370
+ print(text, end="")
1371
+ PYCODE
1372
+ )"
1373
+ write_generated_text_file "$dest" "generated:mempalace-codex-config" "$body"
1374
+ }
1375
+
1376
+ write_mempalace_generic_json_config() {
1377
+ local dest="$1"
1378
+ local marker="$2"
1379
+ local body
1380
+ body='
1381
+ servers = data.setdefault("mcpServers", {})
1382
+ servers["mempalace"] = {"command": "mempalace-mcp", "args": ["--palace", ".mempalace"]}
1383
+ '
1384
+ write_json_config_file "$dest" "$marker" "$body"
1385
+ }
1386
+
1387
+ print_mempalace_project_setup_instructions() {
1388
+ log "MemPalace setup instructions for target project: $PROJECT_DIR"
1389
+ cat <<EOF
1390
+ 1) Ensure Python is installed and available in PATH.
1391
+ 2) Install MemPalace:
1392
+ pip install mempalace
1393
+ 3) Initialize project-local MemPalace cache:
1394
+ mkdir -p "$PROJECT_DIR/.mempalace"
1395
+ mempalace init "$PROJECT_DIR/.mempalace" --yes --auto-mine
1396
+ 4) Index existing project memory:
1397
+ # optional if --auto-mine was skipped
1398
+ mempalace mine "$PROJECT_DIR/.mempalace"
1399
+ 5) Verify in your IDE/agent that MemPalace MCP tools are connected.
1400
+ Note: Ollama at localhost:11434 is optional; MemPalace can run heuristics-only without it.
1401
+ EOF
1402
+ }
1403
+
1404
+ setup_mempalace_for_agentic_opencode() {
1405
+ local step_prefix="MemPalace setup"
1406
+
1407
+ log "$step_prefix [1/4] Checking Python availability"
1408
+ if ! command -v python3 >/dev/null 2>&1 && ! command -v python >/dev/null 2>&1; then
1409
+ warn "Python is not installed. Install Python 3 first, then run: pip install mempalace"
1410
+ warn "Install help: https://www.python.org/downloads/"
1411
+ return 1
1412
+ fi
1413
+ log "$step_prefix [1/4] Python check passed"
1414
+
1415
+ log "$step_prefix [2/4] Checking pip availability"
1416
+ if ! command -v pip >/dev/null 2>&1 && ! command -v pip3 >/dev/null 2>&1; then
1417
+ warn "pip is not available. Install pip for Python 3, then run: pip install mempalace"
1418
+ return 1
1419
+ fi
1420
+ log "$step_prefix [2/4] pip check passed"
1421
+
1422
+ log "$step_prefix [3/4] Installing mempalace package"
1423
+ if pip install mempalace >/dev/null 2>&1; then
1424
+ log "MemPalace package installed via 'pip install mempalace'"
587
1425
  else
588
- cp -a "$src/." "$dest/"
1426
+ warn "Unable to auto-install mempalace via pip; continuing with manual setup instructions"
1427
+ print_mempalace_project_setup_instructions
1428
+ return 1
1429
+ fi
1430
+
1431
+ log "$step_prefix [4/4] Initializing project memory at $PROJECT_DIR/.mempalace"
1432
+ if command -v mempalace >/dev/null 2>&1; then
1433
+ mkdir -p "$PROJECT_DIR/.mempalace" || warn "Failed: mkdir -p \"$PROJECT_DIR/.mempalace\""
1434
+ if mempalace init "$PROJECT_DIR/.mempalace" --yes --auto-mine >/dev/null 2>&1; then
1435
+ log "MemPalace init completed"
1436
+ else
1437
+ warn "Failed: mempalace init \"$PROJECT_DIR/.mempalace\" --yes --auto-mine"
1438
+ fi
1439
+ if mempalace mine "$PROJECT_DIR/.mempalace" >/dev/null 2>&1; then
1440
+ log "MemPalace mine completed"
1441
+ else
1442
+ warn "Failed: mempalace mine \"$PROJECT_DIR/.mempalace\""
1443
+ fi
1444
+ log "$step_prefix [4/4] Initialization step finished"
1445
+ else
1446
+ warn "mempalace command is unavailable after install; please run setup manually"
1447
+ print_mempalace_project_setup_instructions
1448
+ return 1
1449
+ fi
1450
+ }
1451
+
1452
+ configure_mempalace_if_needed() {
1453
+ if ! selected_agent_os_contains "opencode" \
1454
+ && ! selected_agent_os_contains "codex" \
1455
+ && ! selected_agent_os_contains "claude" \
1456
+ && ! selected_agent_os_contains "cursor" \
1457
+ && ! selected_agent_os_contains "gemini" \
1458
+ && ! selected_agent_os_contains "antigravity"; then
1459
+ return
1460
+ fi
1461
+
1462
+ local enable_mempalace="N"
1463
+ if [[ -n "${AGENTIC_ENABLE_MEMPALACE:-}" ]]; then
1464
+ enable_mempalace="$(trim "${AGENTIC_ENABLE_MEMPALACE}")"
1465
+ elif is_interactive_terminal && [[ -z "${AGENTIC_TEST_SOURCE_AGENTIC:-}" ]]; then
1466
+ read -r -p "Enable MemPalace MCP memory integration? [y/N]: " enable_mempalace
1467
+ enable_mempalace="$(trim "${enable_mempalace:-y}")"
1468
+ if [[ -z "$enable_mempalace" ]]; then enable_mempalace="y"; fi
1469
+ fi
1470
+ if [[ "$enable_mempalace" =~ ^[Nn]$ ]]; then
1471
+ log "Skipped MemPalace MCP configuration"
1472
+ return
1473
+ fi
1474
+
1475
+ if selected_agent_os_contains "opencode"; then
1476
+ setup_mempalace_for_agentic_opencode || true
1477
+ else
1478
+ print_mempalace_project_setup_instructions
1479
+ fi
1480
+
1481
+ if command -v mempalace-mcp >/dev/null 2>&1; then
1482
+ if mempalace-mcp --help >/dev/null 2>&1; then
1483
+ log "MemPalace MCP runtime check succeeded via 'mempalace-mcp'"
1484
+ else
1485
+ warn "MemPalace MCP runtime check failed; continuing without runtime validation"
1486
+ fi
1487
+ else
1488
+ warn "mempalace-mcp is unavailable; install/repair MemPalace and re-run setup"
1489
+ fi
1490
+
1491
+ if selected_agent_os_contains "opencode"; then
1492
+ write_mempalace_opencode_config "$PROJECT_DIR/opencode.json"
1493
+ write_mempalace_opencode_config "$PROJECT_DIR/.opencode/opencode.json"
1494
+ fi
1495
+ if selected_agent_os_contains "codex"; then write_mempalace_codex_config; fi
1496
+ if selected_agent_os_contains "claude"; then
1497
+ write_mempalace_generic_json_config "$PROJECT_DIR/.mcp.json" "generated:mempalace-claude-config"
1498
+ fi
1499
+ if selected_agent_os_contains "cursor"; then
1500
+ write_mempalace_generic_json_config "$PROJECT_DIR/.cursor/mcp.json" "generated:mempalace-cursor-config"
1501
+ fi
1502
+ if selected_agent_os_contains "gemini"; then
1503
+ write_mempalace_generic_json_config "$PROJECT_DIR/.gemini/settings.json" "generated:mempalace-gemini-config"
1504
+ fi
1505
+ if selected_agent_os_contains "antigravity"; then
1506
+ write_mempalace_generic_json_config "$HOME/.gemini/antigravity/mcp_config.json" "generated:mempalace-antigravity-config"
589
1507
  fi
590
- unique_append "$dest" COPIED_PATHS
1508
+ }
1509
+
1510
+ configure_context7_if_needed() {
1511
+ if ! selected_agent_os_contains "opencode" \
1512
+ && ! selected_agent_os_contains "codex" \
1513
+ && ! selected_agent_os_contains "claude" \
1514
+ && ! selected_agent_os_contains "cursor" \
1515
+ && ! selected_agent_os_contains "gemini" \
1516
+ && ! selected_agent_os_contains "kilocode" \
1517
+ && ! selected_agent_os_contains "antigravity"; then
1518
+ return
1519
+ fi
1520
+
1521
+ if is_interactive_terminal; then
1522
+ local enable_context7 answer
1523
+ read -r -p "Enable Context7 MCP configuration? [y/N]: " enable_context7
1524
+ enable_context7="$(trim "$enable_context7")"
1525
+ if [[ ! "$enable_context7" =~ ^[Yy]$ ]]; then
1526
+ log "Context7 MCP configuration disabled"
1527
+ return
1528
+ fi
1529
+
1530
+ if [[ -z "$CONTEXT7_API_KEY" ]]; then
1531
+ read -r -p "Context7 API key (optional, empty = no key): " answer
1532
+ CONTEXT7_API_KEY="$(trim "$answer")"
1533
+ fi
1534
+ elif [[ -z "$CONTEXT7_API_KEY" ]]; then
1535
+ log "Context7 MCP configuration skipped; set CONTEXT7_API_KEY or use an interactive install to enable it"
1536
+ return
1537
+ fi
1538
+
1539
+ if selected_agent_os_contains "opencode"; then
1540
+ write_context7_opencode_config
1541
+ write_context7_opencode_legacy_config
1542
+ fi
1543
+
1544
+ if selected_agent_os_contains "codex"; then
1545
+ write_context7_codex_config
1546
+ fi
1547
+
1548
+ if selected_agent_os_contains "claude"; then
1549
+ write_context7_claude_config
1550
+ fi
1551
+
1552
+ if selected_agent_os_contains "cursor"; then
1553
+ write_context7_cursor_config
1554
+ fi
1555
+
1556
+ if selected_agent_os_contains "gemini"; then
1557
+ write_context7_gemini_config
1558
+ fi
1559
+
1560
+ if selected_agent_os_contains "kilocode"; then
1561
+ write_context7_kilocode_config
1562
+ fi
1563
+
1564
+ if selected_agent_os_contains "antigravity"; then
1565
+ write_context7_antigravity_config
1566
+ fi
1567
+ }
1568
+
1569
+ write_default_opencode_plugin_config() {
1570
+ ensure_dir "$APP_CONFIG_DIR"
1571
+ if [[ "$DRY_RUN" == true ]]; then
1572
+ log "DRY-RUN write disabled opencode plugin config to $OPENCODE_PLUGIN_CONFIG_FILE"
1573
+ else
1574
+ python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" <<'PY'
1575
+ import json
1576
+ import sys
1577
+ from pathlib import Path
1578
+
1579
+ path = Path(sys.argv[1])
1580
+ path.write_text(json.dumps({
1581
+ "telegram": {"enabled": False, "botToken": "", "chatId": ""},
1582
+ "modelChecker": {"enabled": False},
1583
+ }, indent=2) + "\n", encoding="utf-8")
1584
+ PY
1585
+ fi
1586
+ }
1587
+
1588
+ configure_opencode_plugins_if_needed() {
1589
+ selected_agent_os_contains "opencode" || return 0
1590
+
1591
+ if [[ "$DRY_RUN" == true ]]; then
1592
+ log "DRY-RUN configure optional opencode plugins"
1593
+ return
1594
+ fi
1595
+
1596
+ ensure_python_available
1597
+ ensure_dir "$APP_CONFIG_DIR"
1598
+
1599
+ if ! is_interactive_terminal; then
1600
+ if [[ ! -f "$OPENCODE_PLUGIN_CONFIG_FILE" ]]; then
1601
+ write_default_opencode_plugin_config
1602
+ fi
1603
+ return
1604
+ fi
1605
+
1606
+ local enable_telegram telegram_token telegram_chat enable_model_checker
1607
+ read -r -p "Enable OpenCode Telegram notifications? [y/N]: " enable_telegram
1608
+ enable_telegram="$(trim "$enable_telegram")"
1609
+ telegram_token=""
1610
+ telegram_chat=""
1611
+ if [[ "$enable_telegram" =~ ^[Yy]$ ]]; then
1612
+ read -r -p "Telegram bot token (empty disables plugin): " telegram_token
1613
+ read -r -p "Telegram chat id (empty disables plugin): " telegram_chat
1614
+ telegram_token="$(trim "$telegram_token")"
1615
+ telegram_chat="$(trim "$telegram_chat")"
1616
+ fi
1617
+
1618
+ read -r -p "Enable OpenCode model checker plugin? [y/N]: " enable_model_checker
1619
+ enable_model_checker="$(trim "$enable_model_checker")"
1620
+
1621
+ python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" "$telegram_token" "$telegram_chat" "$enable_model_checker" <<'PY'
1622
+ import json
1623
+ import sys
1624
+ from pathlib import Path
1625
+
1626
+ path = Path(sys.argv[1])
1627
+ token = sys.argv[2]
1628
+ chat = sys.argv[3]
1629
+ enable_model = sys.argv[4].lower() == "y"
1630
+ data = {
1631
+ "telegram": {
1632
+ "enabled": bool(token and chat),
1633
+ "botToken": token,
1634
+ "chatId": chat,
1635
+ },
1636
+ "modelChecker": {
1637
+ "enabled": enable_model,
1638
+ },
1639
+ }
1640
+ path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
1641
+ PY
591
1642
  }
592
1643
 
593
1644
  normalize_selected_agent_os() {
@@ -747,24 +1798,50 @@ append_root_agents_template() {
747
1798
 
748
1799
  generate_agents_md() {
749
1800
  local project_dir="$1"
750
- local out="$project_dir/AGENTS.md"
1801
+ local outputs=()
1802
+ local needs_root=false
1803
+ local agent_os
1804
+
1805
+ if selected_agent_os_contains "opencode"; then
1806
+ unique_append "$project_dir/.opencode/AGENTS.md" outputs
1807
+ fi
1808
+
1809
+ for agent_os in "${SELECTED_AGENT_OS[@]}"; do
1810
+ if [[ "$agent_os" != "opencode" ]]; then
1811
+ needs_root=true
1812
+ break
1813
+ fi
1814
+ done
1815
+
1816
+ if [[ "$needs_root" == true ]] || ! selected_agent_os_contains "opencode"; then
1817
+ unique_append "$project_dir/AGENTS.md" outputs
1818
+ fi
751
1819
 
752
1820
  if [[ "$DRY_RUN" == true ]]; then
753
- log "DRY-RUN generate $out"
754
- unique_append "$out" COPIED_PATHS
1821
+ local dry_run_out
1822
+ for dry_run_out in "${outputs[@]}"; do
1823
+ log "DRY-RUN generate $dry_run_out"
1824
+ unique_append "$dry_run_out" COPIED_PATHS
1825
+ done
755
1826
  return
756
1827
  fi
757
1828
 
758
1829
  ensure_dir "$project_dir"
759
- build_header "$out"
760
- append_root_agents_template "$out"
1830
+ local tmp
1831
+ tmp="$(mktemp "${TMPDIR:-/tmp}/agentic-agents.XXXXXX")"
1832
+ build_header "$tmp"
1833
+ append_root_agents_template "$tmp"
761
1834
 
762
1835
  local spec_key
763
1836
  for spec_key in "${SELECTED_SPECS[@]}"; do
764
- append_specialization_template "$out" "$spec_key"
1837
+ append_specialization_template "$tmp" "$spec_key"
765
1838
  done
766
1839
 
767
- unique_append "$out" COPIED_PATHS
1840
+ local out
1841
+ for out in "${outputs[@]}"; do
1842
+ write_file_with_agentic_marker "$tmp" "$out" "generated:AGENTS.md"
1843
+ done
1844
+ rm -f "$tmp"
768
1845
  }
769
1846
 
770
1847
  validate_inputs() {
@@ -868,16 +1945,86 @@ print_report() {
868
1945
  fi
869
1946
  }
870
1947
 
1948
+ detect_runtime_platform_label() {
1949
+ local platform
1950
+ platform="$(detect_platform)"
1951
+ if [[ "$platform" == "linux" ]] && grep -qiE "(microsoft|wsl)" /proc/version 2>/dev/null; then
1952
+ echo "wsl"
1953
+ return
1954
+ fi
1955
+ echo "$platform"
1956
+ }
1957
+
1958
+ get_agent_binary_name() {
1959
+ local agent_os="$1"
1960
+ case "$agent_os" in
1961
+ codex) echo "codex" ;;
1962
+ claude) echo "claude" ;;
1963
+ opencode) echo "opencode" ;;
1964
+ cursor) echo "cursor-agent" ;;
1965
+ gemini) echo "gemini" ;;
1966
+ antigravity) echo "antigravity" ;;
1967
+ *) echo "" ;;
1968
+ esac
1969
+ }
1970
+
1971
+ print_missing_agent_binary_guides() {
1972
+ local platform_label
1973
+ platform_label="$(detect_runtime_platform_label)"
1974
+ local missing_lines=()
1975
+ local agent_os
1976
+
1977
+ for agent_os in "${SELECTED_AGENT_OS[@]}"; do
1978
+ local binary_name
1979
+ binary_name="$(get_agent_binary_name "$agent_os")"
1980
+ if [[ -z "$binary_name" ]]; then
1981
+ continue
1982
+ fi
1983
+ if command -v "$binary_name" >/dev/null 2>&1; then
1984
+ continue
1985
+ fi
1986
+
1987
+ local install_link=""
1988
+ case "$agent_os" in
1989
+ codex) install_link="https://github.com/openai/codex" ;;
1990
+ claude) install_link="https://docs.anthropic.com/en/docs/claude-code/quickstart" ;;
1991
+ opencode) install_link="https://opencode.ai/docs" ;;
1992
+ cursor) install_link="https://docs.cursor.com/get-started/installation" ;;
1993
+ gemini) install_link="https://github.com/google-gemini/gemini-cli" ;;
1994
+ antigravity) install_link="https://github.com/getantigravity/antigravity" ;;
1995
+ esac
1996
+
1997
+ missing_lines+=("- $agent_os: binary '$binary_name' is not installed on $platform_label.")
1998
+ if [[ -n "$install_link" ]]; then
1999
+ missing_lines+=(" Install guide: $install_link")
2000
+ fi
2001
+ done
2002
+
2003
+ if [[ "${#missing_lines[@]}" -eq 0 ]]; then
2004
+ return
2005
+ fi
2006
+
2007
+ echo
2008
+ echo "${COLOR_HEADER}=== Agent binary setup recommendations ===${COLOR_RESET}"
2009
+ printf '%s\n' "${missing_lines[@]}"
2010
+ }
2011
+
871
2012
  run_install() {
872
2013
  ensure_repo_layout
2014
+ ensure_python_available
873
2015
  normalize_selected_agent_os
874
2016
  validate_inputs
875
2017
 
876
2018
  ensure_dir "$PROJECT_DIR"
2019
+ configure_opencode_plugins_if_needed
877
2020
  copy_extensions "$PROJECT_DIR"
878
2021
  copy_specialization_assets "$PROJECT_DIR"
879
2022
  generate_agents_md "$PROJECT_DIR"
2023
+ configure_context7_if_needed
2024
+ configure_mempalace_if_needed
2025
+ write_agentic_manifest "$PROJECT_DIR"
880
2026
  print_report
2027
+ print_missing_agent_binary_guides
881
2028
  }
882
2029
 
883
2030
  ascii_banner() {
@@ -1256,7 +2403,9 @@ run_tui() {
1256
2403
  if [[ "$use_fzf" == true ]]; then
1257
2404
  readlines picked_agent_os < <(choose_multi_fzf "Select Agent OS target(s):" "${agentos_choices[@]}")
1258
2405
  else
1259
- readlines picked_agent_os < <(choose_multi_by_index "Select Agent OS target(s):" "${agentos_choices[@]}")
2406
+ local picked_agent_os_output
2407
+ picked_agent_os_output="$(choose_multi_by_index "Select Agent OS target(s):" "${agentos_choices[@]}")"
2408
+ readlines picked_agent_os <<< "$picked_agent_os_output"
1260
2409
  fi
1261
2410
  if [[ "${#picked_agent_os[@]}" -eq 0 ]]; then
1262
2411
  SELECTED_AGENT_OS=("$DEFAULT_AGENT_OS")
@@ -1271,7 +2420,9 @@ run_tui() {
1271
2420
  if [[ "$use_fzf" == true ]]; then
1272
2421
  readlines picked_areas < <(choose_multi_fzf "Select area(s):" "${areas[@]}")
1273
2422
  else
1274
- readlines picked_areas < <(choose_multi_by_index "Select area(s):" "${areas[@]}")
2423
+ local picked_areas_output
2424
+ picked_areas_output="$(choose_multi_by_index "Select area(s):" "${areas[@]}")"
2425
+ readlines picked_areas <<< "$picked_areas_output"
1275
2426
  fi
1276
2427
 
1277
2428
  if [[ "${#picked_areas[@]}" -eq 0 ]]; then
@@ -1290,7 +2441,9 @@ run_tui() {
1290
2441
  if [[ "$use_fzf" == true ]]; then
1291
2442
  readlines chosen_specs < <(choose_multi_fzf "Select specialization(s) for '$area':" "${specs[@]}")
1292
2443
  else
1293
- readlines chosen_specs < <(choose_multi_by_index "Select specialization(s) for '$area':" "${specs[@]}")
2444
+ local chosen_specs_output
2445
+ chosen_specs_output="$(choose_multi_by_index "Select specialization(s) for '$area':" "${specs[@]}")"
2446
+ readlines chosen_specs <<< "$chosen_specs_output"
1294
2447
  fi
1295
2448
 
1296
2449
  if [[ "${#chosen_specs[@]}" -eq 0 ]]; then
@@ -1325,6 +2478,22 @@ self_install() {
1325
2478
 
1326
2479
  ensure_dir "$bin_dir"
1327
2480
 
2481
+ if [[ -e "$source_path" && -e "$target" ]]; then
2482
+ local source_real target_real
2483
+ source_real="$(cd -- "$(dirname -- "$source_path")" && pwd -P)/$(basename -- "$source_path")"
2484
+ target_real="$(cd -- "$(dirname -- "$target")" && pwd -P)/$(basename -- "$target")"
2485
+ if [[ "$source_real" == "$target_real" ]]; then
2486
+ if [[ -e "$APP_REPO_DIR/agentic" ]]; then
2487
+ source_path="$APP_REPO_DIR/agentic"
2488
+ else
2489
+ log "Source and target are already the same file: $target"
2490
+ log "Nothing to copy. Run '$APP_NAME upgrade' first to refresh the knowledge base checkout, or run self-install from a checkout."
2491
+ self_install_fzf_optional
2492
+ return
2493
+ fi
2494
+ fi
2495
+ fi
2496
+
1328
2497
  if [[ -e "$target" ]] && [[ "$SELF_INSTALL_FORCE" != true ]]; then
1329
2498
  error "Target already exists: $target"
1330
2499
  error "Use --force to overwrite"
@@ -1363,6 +2532,35 @@ self_install() {
1363
2532
  echo "Dry-run: $DRY_RUN"
1364
2533
  }
1365
2534
 
2535
+ update_installed_binary_from_repo() {
2536
+ local source_path="$APP_REPO_DIR/agentic"
2537
+ local target="$SCRIPT_SOURCE"
2538
+
2539
+ if [[ ! -r "$source_path" ]]; then
2540
+ warn "Cannot self-update installed binary; source not found: $source_path"
2541
+ return
2542
+ fi
2543
+
2544
+ if [[ "$DRY_RUN" == true ]]; then
2545
+ log "DRY-RUN self-update installed binary $target from $source_path"
2546
+ return
2547
+ fi
2548
+
2549
+ if cmp -s "$source_path" "$target"; then
2550
+ log "Installed binary already up to date: $target"
2551
+ return
2552
+ fi
2553
+
2554
+ local target_dir target_tmp
2555
+ target_dir="$(dirname "$target")"
2556
+ target_tmp="$(mktemp "$target_dir/.agentic-update.XXXXXX")"
2557
+
2558
+ cp "$source_path" "$target_tmp"
2559
+ chmod +x "$target_tmp"
2560
+ mv -f "$target_tmp" "$target"
2561
+ log "Updated installed binary: $target"
2562
+ }
2563
+
1366
2564
  upgrade_repo_checkout() {
1367
2565
  refresh_repo_paths
1368
2566
 
@@ -1392,6 +2590,21 @@ upgrade_repo_checkout() {
1392
2590
  git -C "$APP_REPO_DIR" pull --ff-only
1393
2591
  refresh_repo_paths
1394
2592
  ensure_repo_layout
2593
+ update_installed_binary_from_repo
2594
+ }
2595
+
2596
+ sync_current_project_after_upgrade() {
2597
+ local manifest="$PWD/$PROJECT_MANIFEST_NAME"
2598
+ if [[ ! -f "$manifest" ]]; then
2599
+ log "No $PROJECT_MANIFEST_NAME in current directory; knowledge base upgrade complete"
2600
+ return
2601
+ fi
2602
+
2603
+ log "Detected managed project in $PWD; syncing from upgraded knowledge base"
2604
+ PROJECT_DIR="$(pwd -P)"
2605
+ load_install_settings_from_manifest "$manifest"
2606
+ ensure_repo_layout
2607
+ run_install
1395
2608
  }
1396
2609
 
1397
2610
  parse_theme_option() {
@@ -1465,6 +2678,9 @@ case "$COMMAND" in
1465
2678
  shift 2
1466
2679
  ;;
1467
2680
  --agent-os)
2681
+ if [[ "${#SELECTED_AGENT_OS[@]}" -eq 1 && "${SELECTED_AGENT_OS[0]}" == "$DEFAULT_AGENT_OS" ]]; then
2682
+ SELECTED_AGENT_OS=()
2683
+ fi
1468
2684
  split_csv "$2" SELECTED_AGENT_OS
1469
2685
  shift 2
1470
2686
  ;;
@@ -1499,6 +2715,12 @@ case "$COMMAND" in
1499
2715
  ;;
1500
2716
  esac
1501
2717
  done
2718
+ if [[ -z "$PROJECT_DIR" && -f "$PWD/$PROJECT_MANIFEST_NAME" ]]; then
2719
+ PROJECT_DIR="$PWD"
2720
+ load_install_settings_from_manifest "$PWD/$PROJECT_MANIFEST_NAME"
2721
+ elif [[ -n "$PROJECT_DIR" && -f "$PROJECT_DIR/$PROJECT_MANIFEST_NAME" ]]; then
2722
+ load_install_settings_from_manifest "$PROJECT_DIR/$PROJECT_MANIFEST_NAME"
2723
+ fi
1502
2724
  ensure_repo_available_for_command
1503
2725
  ensure_repo_layout
1504
2726
  set_theme_colors
@@ -1557,6 +2779,7 @@ case "$COMMAND" in
1557
2779
  esac
1558
2780
  done
1559
2781
  upgrade_repo_checkout
2782
+ sync_current_project_after_upgrade
1560
2783
  ;;
1561
2784
 
1562
2785
  self-install)