@jetrabbits/agentic 0.0.4 → 0.0.5

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 (48) hide show
  1. package/AGENTS.md +9 -0
  2. package/Makefile +40 -0
  3. package/UPGRADE.md +61 -0
  4. package/agentic +948 -10
  5. package/areas/software/full-stack/AGENTS.md +1 -4
  6. package/areas/software/full-stack/workflows/debug-issue.md +2 -2
  7. package/docs/agentic-lifecycle.md +103 -0
  8. package/docs/agentic-token-minimization/README.md +79 -0
  9. package/docs/agentic-usage.md +145 -0
  10. package/docs/catalog.schema.json +203 -0
  11. package/docs/guidance-updates/2026-04-10-software-devops-best-practices.md +26 -0
  12. package/docs/opencode_prepare_agents.md +40 -0
  13. package/docs/opencode_setup.md +45 -0
  14. package/docs/prompt-format.md +80 -0
  15. package/docs/site/README.md +44 -0
  16. package/docs/site/app.js +127 -0
  17. package/docs/site/catalog.json +5002 -0
  18. package/docs/site/index.html +52 -0
  19. package/docs/site/styles.css +177 -0
  20. package/extensions/codex/agents/developer.toml +1 -1
  21. package/extensions/codex/agents/devops-engineer.toml +1 -1
  22. package/extensions/codex/agents/product-owner.toml +1 -1
  23. package/extensions/codex/agents/team-lead.toml +1 -1
  24. package/extensions/opencode/plugins/model-checker.json +2 -3
  25. package/extensions/opencode/plugins/model-checker.ts +23 -0
  26. package/extensions/opencode/plugins/telegram-notification.ts +33 -5
  27. package/package.json +6 -2
  28. package/scripts/assess_area_quality.py +216 -0
  29. package/scripts/build_docs_catalog.py +283 -0
  30. package/scripts/lint_prompts.py +113 -0
  31. package/areas/software/full-stack/skills/bash-pro/SKILL.md +0 -310
  32. package/areas/software/full-stack/skills/python-pro/SKILL.md +0 -158
  33. package/areas/software/full-stack/skills/skill-creator/LICENSE.txt +0 -202
  34. package/areas/software/full-stack/skills/skill-creator/SKILL.md +0 -356
  35. package/areas/software/full-stack/skills/skill-creator/references/output-patterns.md +0 -82
  36. package/areas/software/full-stack/skills/skill-creator/references/workflows.md +0 -28
  37. package/areas/software/full-stack/skills/skill-creator/scripts/init_skill.py +0 -303
  38. package/areas/software/full-stack/skills/skill-creator/scripts/package_skill.py +0 -110
  39. package/areas/software/full-stack/skills/skill-creator/scripts/quick_validate.py +0 -95
  40. package/extensions/codex/skills/babysit-pr/SKILL.md +0 -187
  41. package/extensions/codex/skills/babysit-pr/agents/openai.yaml +0 -4
  42. package/extensions/codex/skills/babysit-pr/references/github-api-notes.md +0 -72
  43. package/extensions/codex/skills/babysit-pr/references/heuristics.md +0 -58
  44. package/extensions/codex/skills/babysit-pr/scripts/gh_pr_watch.py +0 -806
  45. package/extensions/codex/skills/babysit-pr/scripts/test_gh_pr_watch.py +0 -155
  46. package/extensions/opencode/skills/code_review_expert/SKILL.md +0 -144
  47. package/extensions/opencode/skills/design_expert/SKILL.md +0 -42
  48. 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,390 @@ 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
+ data["_agentic"] = {
735
+ "generated_by": "agentic",
736
+ "source": source_ref,
737
+ "repository": repo,
738
+ }
739
+ output = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
740
+ elif suffix in {".ts", ".tsx", ".js", ".jsx", ".css"}:
741
+ output = commented(text, "//")
742
+ elif suffix in {".sh", ".toml", ".py", ".yml", ".yaml"}:
743
+ output = commented(text, "#")
744
+ else:
745
+ output = commented(text, "#")
746
+
747
+ dest.write_text(output, encoding="utf-8")
748
+ PY
749
+ register_managed_file "$dest" "$source_ref" "internal"
750
+ }
751
+
752
+ write_agentic_manifest() {
753
+ local project_dir="$1"
754
+ local manifest="$project_dir/$PROJECT_MANIFEST_NAME"
755
+
756
+ if [[ "$DRY_RUN" == true ]]; then
757
+ log "DRY-RUN write $manifest"
758
+ return
759
+ fi
760
+
761
+ local records_file skipped_file
762
+ records_file="$(mktemp "${TMPDIR:-/tmp}/agentic-records.XXXXXX")"
763
+ skipped_file="$(mktemp "${TMPDIR:-/tmp}/agentic-skipped.XXXXXX")"
764
+ if [[ "${#MANAGED_RECORDS[@]}" -gt 0 ]]; then
765
+ printf '%s\n' "${MANAGED_RECORDS[@]}" > "$records_file"
766
+ else
767
+ : > "$records_file"
768
+ fi
769
+ if [[ "${#SKIPPED_MANAGED_PATHS[@]}" -gt 0 ]]; then
770
+ printf '%s\n' "${SKIPPED_MANAGED_PATHS[@]}" > "$skipped_file"
771
+ else
772
+ : > "$skipped_file"
773
+ fi
774
+
775
+ local agent_os_csv areas_csv specs_csv
776
+ local old_ifs="$IFS"
777
+ IFS=,
778
+ agent_os_csv="${SELECTED_AGENT_OS[*]}"
779
+ areas_csv="${SELECTED_AREAS[*]}"
780
+ specs_csv="${SELECTED_SPECS[*]}"
781
+ IFS="$old_ifs"
782
+
783
+ python3 - "$manifest" "$records_file" "$skipped_file" "$APP_REPO_LINK" "$REPO_ROOT" "$agent_os_csv" "$areas_csv" "$specs_csv" <<'PY'
784
+ import json
785
+ import sys
786
+ from datetime import datetime, timezone
787
+ from pathlib import Path
788
+
789
+ manifest = Path(sys.argv[1])
790
+ records_file = Path(sys.argv[2])
791
+ skipped_file = Path(sys.argv[3])
792
+ repo_link = sys.argv[4]
793
+ repo_root = sys.argv[5]
794
+ agent_os = [x for x in sys.argv[6].split(",") if x]
795
+ areas = [x for x in sys.argv[7].split(",") if x]
796
+ specs = [x for x in sys.argv[8].split(",") if x]
797
+ now = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
798
+
799
+ existing = {}
800
+ created_at = now
801
+ if manifest.exists():
802
+ try:
803
+ old = json.loads(manifest.read_text(encoding="utf-8"))
804
+ created_at = old.get("created_at", created_at)
805
+ for item in old.get("managed_files", []):
806
+ if item.get("path"):
807
+ existing[item["path"]] = item
808
+ except Exception:
809
+ existing = {}
810
+
811
+ for line in records_file.read_text(encoding="utf-8").splitlines():
812
+ if not line:
813
+ continue
814
+ path, source, digest, marker = (line.split("|", 3) + ["", "", "", ""])[:4]
815
+ existing[path] = {
816
+ "path": path,
817
+ "source": source,
818
+ "content_hash": digest,
819
+ "marker": marker,
820
+ "updated_at": now,
821
+ }
822
+
823
+ skipped = [x for x in skipped_file.read_text(encoding="utf-8").splitlines() if x]
824
+ data = {
825
+ "_agentic": {
826
+ "generated_by": "agentic",
827
+ "repository": repo_link,
828
+ },
829
+ "version": 1,
830
+ "created_at": created_at,
831
+ "updated_at": now,
832
+ "settings": {
833
+ "agent_os": agent_os,
834
+ "areas": areas,
835
+ "specializations": specs,
836
+ "source_repo": repo_link,
837
+ "source_checkout": repo_root,
838
+ },
839
+ "managed_files": sorted(existing.values(), key=lambda x: x["path"]),
840
+ "skipped_files": skipped,
841
+ }
842
+ manifest.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
843
+ PY
844
+ rm -f "$records_file" "$skipped_file"
845
+ unique_append "$manifest" COPIED_PATHS
846
+ }
847
+
848
+ load_install_settings_from_manifest() {
849
+ local manifest="$1"
850
+ [[ -f "$manifest" ]] || return 1
851
+ ensure_python_available
852
+
853
+ local values=()
854
+ readlines values < <(python3 - "$manifest" <<'PY'
855
+ import json
856
+ import sys
857
+ from pathlib import Path
858
+
859
+ data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
860
+ settings = data.get("settings", {})
861
+ for key in ("agent_os", "areas", "specializations"):
862
+ print("::" + key)
863
+ for value in settings.get(key, []):
864
+ print(value)
865
+ PY
866
+ )
867
+
868
+ local section=""
869
+ local loaded_agent_os=()
870
+ local loaded_areas=()
871
+ local loaded_specs=()
872
+ local value
873
+ for value in "${values[@]}"; do
874
+ case "$value" in
875
+ "::agent_os") section="agent_os" ;;
876
+ "::areas") section="areas" ;;
877
+ "::specializations") section="specializations" ;;
878
+ *)
879
+ case "$section" in
880
+ agent_os) loaded_agent_os+=("$value") ;;
881
+ areas) loaded_areas+=("$value") ;;
882
+ specializations) loaded_specs+=("$value") ;;
883
+ esac
884
+ ;;
885
+ esac
886
+ done
887
+
888
+ if [[ "${#SELECTED_AGENT_OS[@]}" -eq 1 && "${SELECTED_AGENT_OS[0]}" == "$DEFAULT_AGENT_OS" && "${#loaded_agent_os[@]}" -gt 0 ]]; then
889
+ SELECTED_AGENT_OS=("${loaded_agent_os[@]}")
890
+ fi
891
+ if [[ "${#SELECTED_AREAS[@]}" -eq 0 && "${#loaded_areas[@]}" -gt 0 ]]; then
892
+ SELECTED_AREAS=("${loaded_areas[@]}")
893
+ fi
894
+ if [[ "${#SELECTED_SPECS[@]}" -eq 0 && "${#loaded_specs[@]}" -gt 0 ]]; then
895
+ SELECTED_SPECS=("${loaded_specs[@]}")
896
+ fi
897
+ }
898
+
508
899
  path_ref_for_shell_export() {
509
900
  local dir="$1"
510
901
  if [[ "$dir" == "$HOME/"* ]]; then
@@ -583,11 +974,483 @@ copy_dir_contents() {
583
974
  local dest="$2"
584
975
  ensure_dir "$dest"
585
976
  if [[ "$DRY_RUN" == true ]]; then
586
- log "DRY-RUN cp -a $src/. $dest/"
977
+ log "DRY-RUN copy managed contents $src -> $dest"
978
+ unique_append "$dest" COPIED_PATHS
979
+ return
980
+ fi
981
+
982
+ local event kind value events_file
983
+ events_file="$(mktemp "${TMPDIR:-/tmp}/agentic-copy-events.XXXXXX")"
984
+ python3 - "$src" "$dest" "$REPO_ROOT" "$PROJECT_DIR" "$(project_manifest_path)" "$APP_REPO_LINK" > "$events_file" <<'PY'
985
+ import hashlib
986
+ import json
987
+ import sys
988
+ from pathlib import Path
989
+
990
+ src = Path(sys.argv[1])
991
+ dest_root = Path(sys.argv[2])
992
+ repo_root = Path(sys.argv[3])
993
+ project_dir = Path(sys.argv[4])
994
+ manifest = Path(sys.argv[5])
995
+ repo = sys.argv[6]
996
+
997
+
998
+ def emit(kind: str, value: str) -> None:
999
+ print(f"{kind}\t{value}")
1000
+
1001
+
1002
+ def sha256(path: Path) -> str:
1003
+ h = hashlib.sha256()
1004
+ with path.open("rb") as fh:
1005
+ for chunk in iter(lambda: fh.read(1024 * 1024), b""):
1006
+ h.update(chunk)
1007
+ return h.hexdigest()
1008
+
1009
+
1010
+ managed = None
1011
+ if manifest.exists():
1012
+ managed = {}
1013
+ try:
1014
+ data = json.loads(manifest.read_text(encoding="utf-8"))
1015
+ for item in data.get("managed_files", []):
1016
+ rel = item.get("path")
1017
+ if rel:
1018
+ managed[rel] = item.get("content_hash", "")
1019
+ except Exception:
1020
+ managed = {}
1021
+
1022
+
1023
+ def rel_to_project(path: Path) -> str:
1024
+ try:
1025
+ return str(path.relative_to(project_dir))
1026
+ except ValueError:
1027
+ return str(path)
1028
+
1029
+
1030
+ def rel_to_repo(path: Path) -> str:
1031
+ try:
1032
+ return str(path.relative_to(repo_root))
1033
+ except ValueError:
1034
+ return str(path)
1035
+
1036
+
1037
+ def yaml_quote(value: str) -> str:
1038
+ return json.dumps(value, ensure_ascii=False)
1039
+
1040
+
1041
+ def markdown_with_marker(body: str, source_ref: str) -> str:
1042
+ block = (
1043
+ "agentic:\n"
1044
+ " generated_by: agentic\n"
1045
+ f" source: {yaml_quote(source_ref)}\n"
1046
+ f" repository: {yaml_quote(repo)}\n"
1047
+ )
1048
+ if body.startswith("---\n"):
1049
+ end = body.find("\n---", 4)
1050
+ if end != -1:
1051
+ return body[: end + 1] + block + body[end + 1 :]
1052
+ return "---\n" + block + "---\n" + body
1053
+
1054
+
1055
+ def commented(body: str, prefix: str, source_ref: str) -> str:
1056
+ marker = f"Generated by agentic; source: {source_ref}; repository: {repo}"
1057
+ line = f"{prefix} {marker}\n"
1058
+ if body.startswith("#!"):
1059
+ first, sep, rest = body.partition("\n")
1060
+ if sep:
1061
+ return first + sep + line + rest
1062
+ return line + body
1063
+
1064
+
1065
+ def add_marker(file_path: Path, target: Path, source_ref: str) -> str:
1066
+ text = file_path.read_text(encoding="utf-8")
1067
+ suffix = target.suffix.lower()
1068
+ if suffix == ".md":
1069
+ return markdown_with_marker(text, source_ref)
1070
+ if suffix == ".json":
1071
+ data = json.loads(text)
1072
+ if not isinstance(data, dict):
1073
+ raise SystemExit(f"Cannot add agentic metadata to non-object JSON: {target}")
1074
+ data["_agentic"] = {
1075
+ "generated_by": "agentic",
1076
+ "source": source_ref,
1077
+ "repository": repo,
1078
+ }
1079
+ return json.dumps(data, indent=2, ensure_ascii=False) + "\n"
1080
+ if suffix in {".ts", ".tsx", ".js", ".jsx", ".css"}:
1081
+ return commented(text, "//", source_ref)
1082
+ if suffix in {".sh", ".toml", ".py", ".yml", ".yaml"}:
1083
+ return commented(text, "#", source_ref)
1084
+ return commented(text, "#", source_ref)
1085
+
1086
+
1087
+ emit("DIR", str(dest_root))
1088
+ for file_path in sorted(p for p in src.rglob("*") if p.is_file()):
1089
+ rel = file_path.relative_to(src)
1090
+ target = dest_root / rel
1091
+ project_rel = rel_to_project(target)
1092
+ source_ref = rel_to_repo(file_path)
1093
+
1094
+ if managed is not None:
1095
+ if project_rel not in managed:
1096
+ if target.exists():
1097
+ emit("WARN", f"Skipping unmanaged target on rerun: {project_rel}")
1098
+ emit("SKIP", project_rel)
1099
+ continue
1100
+ expected_hash = managed.get(project_rel, "")
1101
+ if target.exists() and expected_hash and sha256(target) != expected_hash:
1102
+ emit("WARN", f"Skipping user-modified managed file: {project_rel}")
1103
+ emit("SKIP", project_rel)
1104
+ continue
1105
+
1106
+ target.parent.mkdir(parents=True, exist_ok=True)
1107
+ emit("DIR", str(target.parent))
1108
+ target.write_text(add_marker(file_path, target, source_ref), encoding="utf-8")
1109
+ digest = sha256(target)
1110
+ emit("RECORD", f"{project_rel}|{source_ref}|{digest}|internal")
1111
+ emit("COPIED", str(target))
1112
+ PY
1113
+ while IFS= read -r event || [[ -n "$event" ]]; do
1114
+ kind="${event%% *}"
1115
+ if [[ "$kind" == "$event" ]]; then
1116
+ value=""
1117
+ else
1118
+ value="${event#* }"
1119
+ fi
1120
+ record_agentic_event "$kind" "$value"
1121
+ done < "$events_file"
1122
+ rm -f "$events_file"
1123
+ }
1124
+
1125
+ write_generated_text_file() {
1126
+ local dest="$1"
1127
+ local source_ref="$2"
1128
+ local content="$3"
1129
+
1130
+ if [[ "$DRY_RUN" == true ]]; then
1131
+ log "DRY-RUN write generated file $dest"
1132
+ unique_append "$dest" COPIED_PATHS
1133
+ return
1134
+ fi
1135
+
1136
+ can_write_managed_file "$dest" || return 0
1137
+ ensure_dir "$(dirname -- "$dest")"
1138
+
1139
+ local tmp
1140
+ tmp="$(mktemp "${TMPDIR:-/tmp}/agentic-generated.XXXXXX")"
1141
+ printf '%s' "$content" > "$tmp"
1142
+ write_file_with_agentic_marker "$tmp" "$dest" "$source_ref"
1143
+ rm -f "$tmp"
1144
+ }
1145
+
1146
+ write_json_file_with_agentic_metadata() {
1147
+ local dest="$1"
1148
+ local source_ref="$2"
1149
+ local python_body="$3"
1150
+
1151
+ if [[ "$DRY_RUN" == true ]]; then
1152
+ log "DRY-RUN update JSON managed file $dest"
1153
+ unique_append "$dest" COPIED_PATHS
1154
+ return
1155
+ fi
1156
+
1157
+ can_write_managed_file "$dest" || return 0
1158
+ ensure_dir "$(dirname -- "$dest")"
1159
+ python3 - "$dest" "$source_ref" "$APP_REPO_LINK" "$CONTEXT7_API_KEY" "$python_body" <<'PY'
1160
+ import json
1161
+ import sys
1162
+ from pathlib import Path
1163
+
1164
+ path = Path(sys.argv[1])
1165
+ source_ref = sys.argv[2]
1166
+ repo = sys.argv[3]
1167
+ context7_api_key = sys.argv[4]
1168
+ body = sys.argv[5]
1169
+
1170
+ data = {}
1171
+ if path.exists():
1172
+ try:
1173
+ data = json.loads(path.read_text(encoding="utf-8"))
1174
+ except Exception:
1175
+ data = {}
1176
+ if not isinstance(data, dict):
1177
+ data = {}
1178
+
1179
+ namespace = {
1180
+ "data": data,
1181
+ "context7_api_key": context7_api_key,
1182
+ }
1183
+ exec(body, namespace)
1184
+ data = namespace["data"]
1185
+ data["_agentic"] = {
1186
+ "generated_by": "agentic",
1187
+ "source": source_ref,
1188
+ "repository": repo,
1189
+ }
1190
+ path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
1191
+ PY
1192
+ register_managed_file "$dest" "$source_ref" "internal"
1193
+ }
1194
+
1195
+ write_json_config_file() {
1196
+ local dest="$1"
1197
+ local source_ref="$2"
1198
+ local python_body="$3"
1199
+
1200
+ if [[ "$DRY_RUN" == true ]]; then
1201
+ log "DRY-RUN update JSON config file $dest"
1202
+ unique_append "$dest" COPIED_PATHS
1203
+ return
1204
+ fi
1205
+
1206
+ can_write_managed_file "$dest" || return 0
1207
+ ensure_dir "$(dirname -- "$dest")"
1208
+ python3 - "$dest" "$CONTEXT7_API_KEY" "$python_body" <<'PY'
1209
+ import json
1210
+ import sys
1211
+ from pathlib import Path
1212
+
1213
+ path = Path(sys.argv[1])
1214
+ context7_api_key = sys.argv[2]
1215
+ body = sys.argv[3]
1216
+
1217
+ data = {}
1218
+ if path.exists():
1219
+ try:
1220
+ data = json.loads(path.read_text(encoding="utf-8"))
1221
+ except Exception:
1222
+ data = {}
1223
+ if not isinstance(data, dict):
1224
+ data = {}
1225
+
1226
+ namespace = {
1227
+ "data": data,
1228
+ "context7_api_key": context7_api_key,
1229
+ }
1230
+ exec(body, namespace)
1231
+ data = namespace["data"]
1232
+ path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
1233
+ PY
1234
+ register_managed_file "$dest" "$source_ref" "config"
1235
+ }
1236
+
1237
+ write_context7_opencode_config() {
1238
+ local dest="$PROJECT_DIR/opencode.json"
1239
+ local body
1240
+ body='
1241
+ mcp = data.setdefault("mcp", {})
1242
+ context7 = {
1243
+ "type": "remote",
1244
+ "url": "https://mcp.context7.com/mcp",
1245
+ "enabled": True,
1246
+ }
1247
+ if context7_api_key:
1248
+ context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
1249
+ mcp["context7"] = context7
1250
+ '
1251
+ write_json_config_file "$dest" "generated:context7-opencode-config" "$body"
1252
+ }
1253
+
1254
+ write_context7_opencode_legacy_config() {
1255
+ local dest="$PROJECT_DIR/.opencode/opencode.json"
1256
+ local body
1257
+ body='
1258
+ mcp = data.setdefault("mcp", {})
1259
+ context7 = {
1260
+ "type": "remote",
1261
+ "url": "https://mcp.context7.com/mcp",
1262
+ "enabled": True,
1263
+ }
1264
+ if context7_api_key:
1265
+ context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
1266
+ mcp["context7"] = context7
1267
+ '
1268
+ write_json_file_with_agentic_metadata "$dest" "generated:context7-opencode-legacy-config" "$body"
1269
+ }
1270
+
1271
+ write_context7_codex_config() {
1272
+ local dest="$PROJECT_DIR/.codex/config.toml"
1273
+ local headers=""
1274
+ if [[ -n "$CONTEXT7_API_KEY" ]]; then
1275
+ local escaped_key
1276
+ escaped_key="${CONTEXT7_API_KEY//\\/\\\\}"
1277
+ escaped_key="${escaped_key//\"/\\\"}"
1278
+ headers="http_headers = { \"CONTEXT7_API_KEY\" = \"$escaped_key\" }
1279
+ "
1280
+ fi
1281
+ write_generated_text_file "$dest" "generated:context7-codex-config" "[mcp_servers.context7]
1282
+ url = \"https://mcp.context7.com/mcp\"
1283
+ ${headers}"
1284
+ }
1285
+
1286
+ write_context7_claude_config() {
1287
+ local dest="$PROJECT_DIR/.mcp.json"
1288
+ local body
1289
+ body='
1290
+ mcp_servers = data.setdefault("mcpServers", {})
1291
+ context7 = {
1292
+ "type": "http",
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-claude-config" "$body"
1300
+ }
1301
+
1302
+ write_context7_cursor_config() {
1303
+ local dest="$PROJECT_DIR/.cursor/mcp.json"
1304
+ local body
1305
+ body='
1306
+ mcp_servers = data.setdefault("mcpServers", {})
1307
+ context7 = {
1308
+ "url": "https://mcp.context7.com/mcp",
1309
+ }
1310
+ if context7_api_key:
1311
+ context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
1312
+ mcp_servers["context7"] = context7
1313
+ '
1314
+ write_json_config_file "$dest" "generated:context7-cursor-config" "$body"
1315
+ }
1316
+
1317
+ write_context7_gemini_config() {
1318
+ local dest="$PROJECT_DIR/.gemini/settings.json"
1319
+ local body
1320
+ body='
1321
+ mcp_servers = data.setdefault("mcpServers", {})
1322
+ context7 = {
1323
+ "httpUrl": "https://mcp.context7.com/mcp",
1324
+ }
1325
+ if context7_api_key:
1326
+ context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
1327
+ mcp_servers["context7"] = context7
1328
+ '
1329
+ write_json_config_file "$dest" "generated:context7-gemini-config" "$body"
1330
+ }
1331
+
1332
+ configure_context7_if_needed() {
1333
+ if ! selected_agent_os_contains "opencode" \
1334
+ && ! selected_agent_os_contains "codex" \
1335
+ && ! selected_agent_os_contains "claude" \
1336
+ && ! selected_agent_os_contains "cursor" \
1337
+ && ! selected_agent_os_contains "gemini"; then
1338
+ return
1339
+ fi
1340
+
1341
+ if is_interactive_terminal; then
1342
+ local enable_context7 answer
1343
+ read -r -p "Enable Context7 MCP configuration? [y/N]: " enable_context7
1344
+ enable_context7="$(trim "$enable_context7")"
1345
+ if [[ ! "$enable_context7" =~ ^[Yy]$ ]]; then
1346
+ log "Context7 MCP configuration disabled"
1347
+ return
1348
+ fi
1349
+
1350
+ if [[ -z "$CONTEXT7_API_KEY" ]]; then
1351
+ read -r -p "Context7 API key (optional, empty = no key): " answer
1352
+ CONTEXT7_API_KEY="$(trim "$answer")"
1353
+ fi
1354
+ elif [[ -z "$CONTEXT7_API_KEY" ]]; then
1355
+ log "Context7 MCP configuration skipped; set CONTEXT7_API_KEY or use an interactive install to enable it"
1356
+ return
1357
+ fi
1358
+
1359
+ if selected_agent_os_contains "opencode"; then
1360
+ write_context7_opencode_config
1361
+ write_context7_opencode_legacy_config
1362
+ fi
1363
+
1364
+ if selected_agent_os_contains "codex"; then
1365
+ write_context7_codex_config
1366
+ fi
1367
+
1368
+ if selected_agent_os_contains "claude"; then
1369
+ write_context7_claude_config
1370
+ fi
1371
+
1372
+ if selected_agent_os_contains "cursor"; then
1373
+ write_context7_cursor_config
1374
+ fi
1375
+
1376
+ if selected_agent_os_contains "gemini"; then
1377
+ write_context7_gemini_config
1378
+ fi
1379
+ }
1380
+
1381
+ write_default_opencode_plugin_config() {
1382
+ ensure_dir "$APP_CONFIG_DIR"
1383
+ if [[ "$DRY_RUN" == true ]]; then
1384
+ log "DRY-RUN write disabled opencode plugin config to $OPENCODE_PLUGIN_CONFIG_FILE"
587
1385
  else
588
- cp -a "$src/." "$dest/"
1386
+ python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" <<'PY'
1387
+ import json
1388
+ import sys
1389
+ from pathlib import Path
1390
+
1391
+ path = Path(sys.argv[1])
1392
+ path.write_text(json.dumps({
1393
+ "telegram": {"enabled": False, "botToken": "", "chatId": ""},
1394
+ "modelChecker": {"enabled": False},
1395
+ }, indent=2) + "\n", encoding="utf-8")
1396
+ PY
589
1397
  fi
590
- unique_append "$dest" COPIED_PATHS
1398
+ }
1399
+
1400
+ configure_opencode_plugins_if_needed() {
1401
+ selected_agent_os_contains "opencode" || return 0
1402
+
1403
+ if [[ "$DRY_RUN" == true ]]; then
1404
+ log "DRY-RUN configure optional opencode plugins"
1405
+ return
1406
+ fi
1407
+
1408
+ ensure_python_available
1409
+ ensure_dir "$APP_CONFIG_DIR"
1410
+
1411
+ if ! is_interactive_terminal; then
1412
+ if [[ ! -f "$OPENCODE_PLUGIN_CONFIG_FILE" ]]; then
1413
+ write_default_opencode_plugin_config
1414
+ fi
1415
+ return
1416
+ fi
1417
+
1418
+ local enable_telegram telegram_token telegram_chat enable_model_checker
1419
+ read -r -p "Enable OpenCode Telegram notifications? [y/N]: " enable_telegram
1420
+ enable_telegram="$(trim "$enable_telegram")"
1421
+ telegram_token=""
1422
+ telegram_chat=""
1423
+ if [[ "$enable_telegram" =~ ^[Yy]$ ]]; then
1424
+ read -r -p "Telegram bot token (empty disables plugin): " telegram_token
1425
+ read -r -p "Telegram chat id (empty disables plugin): " telegram_chat
1426
+ telegram_token="$(trim "$telegram_token")"
1427
+ telegram_chat="$(trim "$telegram_chat")"
1428
+ fi
1429
+
1430
+ read -r -p "Enable OpenCode model checker plugin? [y/N]: " enable_model_checker
1431
+ enable_model_checker="$(trim "$enable_model_checker")"
1432
+
1433
+ python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" "$telegram_token" "$telegram_chat" "$enable_model_checker" <<'PY'
1434
+ import json
1435
+ import sys
1436
+ from pathlib import Path
1437
+
1438
+ path = Path(sys.argv[1])
1439
+ token = sys.argv[2]
1440
+ chat = sys.argv[3]
1441
+ enable_model = sys.argv[4].lower() == "y"
1442
+ data = {
1443
+ "telegram": {
1444
+ "enabled": bool(token and chat),
1445
+ "botToken": token,
1446
+ "chatId": chat,
1447
+ },
1448
+ "modelChecker": {
1449
+ "enabled": enable_model,
1450
+ },
1451
+ }
1452
+ path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
1453
+ PY
591
1454
  }
592
1455
 
593
1456
  normalize_selected_agent_os() {
@@ -756,15 +1619,18 @@ generate_agents_md() {
756
1619
  fi
757
1620
 
758
1621
  ensure_dir "$project_dir"
759
- build_header "$out"
760
- append_root_agents_template "$out"
1622
+ local tmp
1623
+ tmp="$(mktemp "${TMPDIR:-/tmp}/agentic-agents.XXXXXX")"
1624
+ build_header "$tmp"
1625
+ append_root_agents_template "$tmp"
761
1626
 
762
1627
  local spec_key
763
1628
  for spec_key in "${SELECTED_SPECS[@]}"; do
764
- append_specialization_template "$out" "$spec_key"
1629
+ append_specialization_template "$tmp" "$spec_key"
765
1630
  done
766
1631
 
767
- unique_append "$out" COPIED_PATHS
1632
+ write_file_with_agentic_marker "$tmp" "$out" "generated:AGENTS.md"
1633
+ rm -f "$tmp"
768
1634
  }
769
1635
 
770
1636
  validate_inputs() {
@@ -870,13 +1736,17 @@ print_report() {
870
1736
 
871
1737
  run_install() {
872
1738
  ensure_repo_layout
1739
+ ensure_python_available
873
1740
  normalize_selected_agent_os
874
1741
  validate_inputs
875
1742
 
876
1743
  ensure_dir "$PROJECT_DIR"
1744
+ configure_opencode_plugins_if_needed
877
1745
  copy_extensions "$PROJECT_DIR"
878
1746
  copy_specialization_assets "$PROJECT_DIR"
879
1747
  generate_agents_md "$PROJECT_DIR"
1748
+ configure_context7_if_needed
1749
+ write_agentic_manifest "$PROJECT_DIR"
880
1750
  print_report
881
1751
  }
882
1752
 
@@ -1256,7 +2126,9 @@ run_tui() {
1256
2126
  if [[ "$use_fzf" == true ]]; then
1257
2127
  readlines picked_agent_os < <(choose_multi_fzf "Select Agent OS target(s):" "${agentos_choices[@]}")
1258
2128
  else
1259
- readlines picked_agent_os < <(choose_multi_by_index "Select Agent OS target(s):" "${agentos_choices[@]}")
2129
+ local picked_agent_os_output
2130
+ picked_agent_os_output="$(choose_multi_by_index "Select Agent OS target(s):" "${agentos_choices[@]}")"
2131
+ readlines picked_agent_os <<< "$picked_agent_os_output"
1260
2132
  fi
1261
2133
  if [[ "${#picked_agent_os[@]}" -eq 0 ]]; then
1262
2134
  SELECTED_AGENT_OS=("$DEFAULT_AGENT_OS")
@@ -1271,7 +2143,9 @@ run_tui() {
1271
2143
  if [[ "$use_fzf" == true ]]; then
1272
2144
  readlines picked_areas < <(choose_multi_fzf "Select area(s):" "${areas[@]}")
1273
2145
  else
1274
- readlines picked_areas < <(choose_multi_by_index "Select area(s):" "${areas[@]}")
2146
+ local picked_areas_output
2147
+ picked_areas_output="$(choose_multi_by_index "Select area(s):" "${areas[@]}")"
2148
+ readlines picked_areas <<< "$picked_areas_output"
1275
2149
  fi
1276
2150
 
1277
2151
  if [[ "${#picked_areas[@]}" -eq 0 ]]; then
@@ -1290,7 +2164,9 @@ run_tui() {
1290
2164
  if [[ "$use_fzf" == true ]]; then
1291
2165
  readlines chosen_specs < <(choose_multi_fzf "Select specialization(s) for '$area':" "${specs[@]}")
1292
2166
  else
1293
- readlines chosen_specs < <(choose_multi_by_index "Select specialization(s) for '$area':" "${specs[@]}")
2167
+ local chosen_specs_output
2168
+ chosen_specs_output="$(choose_multi_by_index "Select specialization(s) for '$area':" "${specs[@]}")"
2169
+ readlines chosen_specs <<< "$chosen_specs_output"
1294
2170
  fi
1295
2171
 
1296
2172
  if [[ "${#chosen_specs[@]}" -eq 0 ]]; then
@@ -1325,6 +2201,22 @@ self_install() {
1325
2201
 
1326
2202
  ensure_dir "$bin_dir"
1327
2203
 
2204
+ if [[ -e "$source_path" && -e "$target" ]]; then
2205
+ local source_real target_real
2206
+ source_real="$(cd -- "$(dirname -- "$source_path")" && pwd -P)/$(basename -- "$source_path")"
2207
+ target_real="$(cd -- "$(dirname -- "$target")" && pwd -P)/$(basename -- "$target")"
2208
+ if [[ "$source_real" == "$target_real" ]]; then
2209
+ if [[ -e "$APP_REPO_DIR/agentic" ]]; then
2210
+ source_path="$APP_REPO_DIR/agentic"
2211
+ else
2212
+ log "Source and target are already the same file: $target"
2213
+ log "Nothing to copy. Run '$APP_NAME upgrade' first to refresh the knowledge base checkout, or run self-install from a checkout."
2214
+ self_install_fzf_optional
2215
+ return
2216
+ fi
2217
+ fi
2218
+ fi
2219
+
1328
2220
  if [[ -e "$target" ]] && [[ "$SELF_INSTALL_FORCE" != true ]]; then
1329
2221
  error "Target already exists: $target"
1330
2222
  error "Use --force to overwrite"
@@ -1363,6 +2255,30 @@ self_install() {
1363
2255
  echo "Dry-run: $DRY_RUN"
1364
2256
  }
1365
2257
 
2258
+ update_installed_binary_from_repo() {
2259
+ local source_path="$APP_REPO_DIR/agentic"
2260
+ local target="$SCRIPT_SOURCE"
2261
+
2262
+ if [[ ! -r "$source_path" ]]; then
2263
+ warn "Cannot self-update installed binary; source not found: $source_path"
2264
+ return
2265
+ fi
2266
+
2267
+ if [[ "$DRY_RUN" == true ]]; then
2268
+ log "DRY-RUN self-update installed binary $target from $source_path"
2269
+ return
2270
+ fi
2271
+
2272
+ if cmp -s "$source_path" "$target"; then
2273
+ log "Installed binary already up to date: $target"
2274
+ return
2275
+ fi
2276
+
2277
+ cp "$source_path" "$target"
2278
+ chmod +x "$target"
2279
+ log "Updated installed binary: $target"
2280
+ }
2281
+
1366
2282
  upgrade_repo_checkout() {
1367
2283
  refresh_repo_paths
1368
2284
 
@@ -1392,6 +2308,21 @@ upgrade_repo_checkout() {
1392
2308
  git -C "$APP_REPO_DIR" pull --ff-only
1393
2309
  refresh_repo_paths
1394
2310
  ensure_repo_layout
2311
+ update_installed_binary_from_repo
2312
+ }
2313
+
2314
+ sync_current_project_after_upgrade() {
2315
+ local manifest="$PWD/$PROJECT_MANIFEST_NAME"
2316
+ if [[ ! -f "$manifest" ]]; then
2317
+ log "No $PROJECT_MANIFEST_NAME in current directory; knowledge base upgrade complete"
2318
+ return
2319
+ fi
2320
+
2321
+ log "Detected managed project in $PWD; syncing from upgraded knowledge base"
2322
+ PROJECT_DIR="$(pwd -P)"
2323
+ load_install_settings_from_manifest "$manifest"
2324
+ ensure_repo_layout
2325
+ run_install
1395
2326
  }
1396
2327
 
1397
2328
  parse_theme_option() {
@@ -1499,6 +2430,12 @@ case "$COMMAND" in
1499
2430
  ;;
1500
2431
  esac
1501
2432
  done
2433
+ if [[ -z "$PROJECT_DIR" && -f "$PWD/$PROJECT_MANIFEST_NAME" ]]; then
2434
+ PROJECT_DIR="$PWD"
2435
+ load_install_settings_from_manifest "$PWD/$PROJECT_MANIFEST_NAME"
2436
+ elif [[ -n "$PROJECT_DIR" && -f "$PROJECT_DIR/$PROJECT_MANIFEST_NAME" ]]; then
2437
+ load_install_settings_from_manifest "$PROJECT_DIR/$PROJECT_MANIFEST_NAME"
2438
+ fi
1502
2439
  ensure_repo_available_for_command
1503
2440
  ensure_repo_layout
1504
2441
  set_theme_colors
@@ -1557,6 +2494,7 @@ case "$COMMAND" in
1557
2494
  esac
1558
2495
  done
1559
2496
  upgrade_repo_checkout
2497
+ sync_current_project_after_upgrade
1560
2498
  ;;
1561
2499
 
1562
2500
  self-install)