ragtime-cli 0.2.18__py3-none-any.whl → 0.3.0__py3-none-any.whl
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.
Potentially problematic release.
This version of ragtime-cli might be problematic. Click here for more details.
- {ragtime_cli-0.2.18.dist-info → ragtime_cli-0.3.0.dist-info}/METADATA +61 -28
- ragtime_cli-0.3.0.dist-info/RECORD +16 -0
- src/cli.py +862 -105
- src/config.py +17 -1
- src/indexers/docs.py +86 -8
- ragtime_cli-0.2.18.dist-info/RECORD +0 -26
- src/commands/audit.md +0 -151
- src/commands/create-pr.md +0 -389
- src/commands/generate-docs.md +0 -325
- src/commands/handoff.md +0 -176
- src/commands/import-docs.md +0 -259
- src/commands/pr-graduate.md +0 -192
- src/commands/recall.md +0 -175
- src/commands/remember.md +0 -168
- src/commands/save.md +0 -10
- src/commands/start.md +0 -206
- {ragtime_cli-0.2.18.dist-info → ragtime_cli-0.3.0.dist-info}/WHEEL +0 -0
- {ragtime_cli-0.2.18.dist-info → ragtime_cli-0.3.0.dist-info}/entry_points.txt +0 -0
- {ragtime_cli-0.2.18.dist-info → ragtime_cli-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {ragtime_cli-0.2.18.dist-info → ragtime_cli-0.3.0.dist-info}/top_level.txt +0 -0
src/cli.py
CHANGED
|
@@ -817,16 +817,81 @@ def forget(memory_id: str, path: Path):
|
|
|
817
817
|
|
|
818
818
|
|
|
819
819
|
@main.command()
|
|
820
|
-
@click.argument("memory_id")
|
|
820
|
+
@click.argument("memory_id", required=False)
|
|
821
821
|
@click.option("--path", type=click.Path(exists=True, path_type=Path), default=".")
|
|
822
|
+
@click.option("--list", "list_candidates", is_flag=True, help="List graduation candidates")
|
|
823
|
+
@click.option("--branch", "-b", help="Branch name or slug (default: current branch)")
|
|
822
824
|
@click.option("--confidence", default="high",
|
|
823
825
|
type=click.Choice(["high", "medium", "low"]),
|
|
824
826
|
help="Confidence level for graduated memory")
|
|
825
|
-
def graduate(memory_id: str, path: Path, confidence: str):
|
|
826
|
-
"""Graduate a branch memory to app namespace.
|
|
827
|
+
def graduate(memory_id: str, path: Path, list_candidates: bool, branch: str, confidence: str):
|
|
828
|
+
"""Graduate a branch memory to app namespace.
|
|
829
|
+
|
|
830
|
+
With --list: Show branch memories that are candidates for graduation.
|
|
831
|
+
With MEMORY_ID: Graduate a specific memory to app namespace.
|
|
832
|
+
|
|
833
|
+
Examples:
|
|
834
|
+
ragtime graduate --list # List candidates for current branch
|
|
835
|
+
ragtime graduate --list -b feature/auth # List candidates for specific branch
|
|
836
|
+
ragtime graduate abc123 # Graduate specific memory
|
|
837
|
+
"""
|
|
827
838
|
path = Path(path).resolve()
|
|
828
839
|
store = get_memory_store(path)
|
|
829
840
|
|
|
841
|
+
# List mode: show graduation candidates
|
|
842
|
+
if list_candidates or not memory_id:
|
|
843
|
+
# Determine branch
|
|
844
|
+
if not branch:
|
|
845
|
+
branch = get_current_branch(path)
|
|
846
|
+
if not branch:
|
|
847
|
+
click.echo("✗ Not in a git repository or no branch specified", err=True)
|
|
848
|
+
return
|
|
849
|
+
|
|
850
|
+
# Create namespace pattern (handle both original and slugified)
|
|
851
|
+
branch_slug = branch.replace("/", "-")
|
|
852
|
+
namespace = f"branch-{branch_slug}"
|
|
853
|
+
|
|
854
|
+
# Get memories for this branch
|
|
855
|
+
memories = store.list_memories(namespace=namespace, status="active")
|
|
856
|
+
|
|
857
|
+
if not memories:
|
|
858
|
+
click.echo(f"No active memories found for branch: {branch}")
|
|
859
|
+
click.echo(f" (namespace: {namespace})")
|
|
860
|
+
return
|
|
861
|
+
|
|
862
|
+
# Filter to graduation candidates (exclude context type)
|
|
863
|
+
candidates = [m for m in memories if m.type != "context"]
|
|
864
|
+
|
|
865
|
+
if not candidates:
|
|
866
|
+
click.echo(f"No graduation candidates for branch: {branch}")
|
|
867
|
+
click.echo(f" (found {len(memories)} memories, but all are context type)")
|
|
868
|
+
return
|
|
869
|
+
|
|
870
|
+
click.echo(f"\nGraduation candidates for branch: {branch}")
|
|
871
|
+
click.echo(f"{'─' * 50}")
|
|
872
|
+
|
|
873
|
+
for i, mem in enumerate(candidates, 1):
|
|
874
|
+
type_badge = f"[{mem.type}]" if mem.type else "[unknown]"
|
|
875
|
+
confidence_badge = f"({mem.confidence})" if hasattr(mem, 'confidence') and mem.confidence else ""
|
|
876
|
+
|
|
877
|
+
click.echo(f"\n {i}. {type_badge} {confidence_badge}")
|
|
878
|
+
click.echo(f" ID: {mem.id}")
|
|
879
|
+
|
|
880
|
+
# Show preview
|
|
881
|
+
preview = mem.content[:150].replace("\n", " ").strip()
|
|
882
|
+
if len(mem.content) > 150:
|
|
883
|
+
preview += "..."
|
|
884
|
+
click.echo(f" {preview}")
|
|
885
|
+
|
|
886
|
+
if hasattr(mem, 'added') and mem.added:
|
|
887
|
+
click.echo(f" Added: {mem.added}")
|
|
888
|
+
|
|
889
|
+
click.echo(f"\n{'─' * 50}")
|
|
890
|
+
click.echo(f"{len(candidates)} candidate(s) found.")
|
|
891
|
+
click.echo(f"\nTo graduate: ragtime graduate <ID>")
|
|
892
|
+
return
|
|
893
|
+
|
|
894
|
+
# Graduate mode: graduate specific memory
|
|
830
895
|
try:
|
|
831
896
|
graduated = store.graduate(memory_id, confidence)
|
|
832
897
|
if graduated:
|
|
@@ -893,6 +958,568 @@ def reindex(path: Path):
|
|
|
893
958
|
click.echo(f"✓ Reindexed {count} memory files")
|
|
894
959
|
|
|
895
960
|
|
|
961
|
+
@main.command("check-conventions")
|
|
962
|
+
@click.option("--path", type=click.Path(exists=True, path_type=Path), default=".")
|
|
963
|
+
@click.option("--files", "-f", help="Files to check (comma-separated or JSON array)")
|
|
964
|
+
@click.option("--branch", "-b", help="Branch to diff against main (default: current branch)")
|
|
965
|
+
@click.option("--event-file", type=click.Path(exists=True, path_type=Path), help="GHP event file (JSON)")
|
|
966
|
+
@click.option("--include-memories", is_flag=True, default=True, help="Include convention memories")
|
|
967
|
+
@click.option("--all", "return_all", is_flag=True, help="Return ALL conventions (for AI workflows)")
|
|
968
|
+
@click.option("--json", "json_output", is_flag=True, help="Output as JSON (for MCP/hooks)")
|
|
969
|
+
def check_conventions(path: Path, files: str, branch: str, event_file: Path,
|
|
970
|
+
include_memories: bool, return_all: bool, json_output: bool):
|
|
971
|
+
"""Show conventions applicable to changed files.
|
|
972
|
+
|
|
973
|
+
Used by pre-PR hooks to check code against team conventions.
|
|
974
|
+
|
|
975
|
+
By default, returns only conventions RELEVANT to the changed files (semantic search).
|
|
976
|
+
Use --all to return ALL conventions (useful for AI workflows that analyze edge cases).
|
|
977
|
+
|
|
978
|
+
Examples:
|
|
979
|
+
ragtime check-conventions # Relevant conventions only
|
|
980
|
+
ragtime check-conventions --all # ALL conventions (for AI)
|
|
981
|
+
ragtime check-conventions -f "src/auth.ts" # Conventions relevant to specific file
|
|
982
|
+
ragtime check-conventions --event-file /tmp/ghp-event.json --all # From GHP hook
|
|
983
|
+
"""
|
|
984
|
+
import json as json_module
|
|
985
|
+
|
|
986
|
+
path = Path(path).resolve()
|
|
987
|
+
config = RagtimeConfig.load(path)
|
|
988
|
+
|
|
989
|
+
# Load from event file if provided (GHP hook pattern)
|
|
990
|
+
if event_file:
|
|
991
|
+
try:
|
|
992
|
+
event_data = json_module.loads(Path(event_file).read_text())
|
|
993
|
+
# Event file can provide changed_files and branch
|
|
994
|
+
if not files and "changed_files" in event_data:
|
|
995
|
+
files = event_data["changed_files"]
|
|
996
|
+
if not branch and "branch" in event_data:
|
|
997
|
+
branch = event_data["branch"]
|
|
998
|
+
except (json_module.JSONDecodeError, IOError) as e:
|
|
999
|
+
click.echo(f"⚠ Could not read event file: {e}", err=True)
|
|
1000
|
+
|
|
1001
|
+
# Determine files to check
|
|
1002
|
+
changed_files = []
|
|
1003
|
+
if files:
|
|
1004
|
+
# Handle list (from event file) or string (from CLI)
|
|
1005
|
+
if isinstance(files, list):
|
|
1006
|
+
changed_files = files
|
|
1007
|
+
elif files.startswith("["):
|
|
1008
|
+
try:
|
|
1009
|
+
changed_files = json_module.loads(files)
|
|
1010
|
+
except json_module.JSONDecodeError:
|
|
1011
|
+
changed_files = [f.strip() for f in files.split(",")]
|
|
1012
|
+
else:
|
|
1013
|
+
changed_files = [f.strip() for f in files.split(",")]
|
|
1014
|
+
else:
|
|
1015
|
+
# Get changed files from git diff
|
|
1016
|
+
if not branch:
|
|
1017
|
+
branch = get_current_branch(path)
|
|
1018
|
+
if branch and branch not in ("main", "master"):
|
|
1019
|
+
result = subprocess.run(
|
|
1020
|
+
["git", "diff", "--name-only", "main...HEAD"],
|
|
1021
|
+
cwd=path,
|
|
1022
|
+
capture_output=True,
|
|
1023
|
+
text=True,
|
|
1024
|
+
)
|
|
1025
|
+
if result.returncode == 0:
|
|
1026
|
+
changed_files = [f for f in result.stdout.strip().split("\n") if f]
|
|
1027
|
+
|
|
1028
|
+
# Gather conventions from files
|
|
1029
|
+
conventions_data = {
|
|
1030
|
+
"files": [],
|
|
1031
|
+
"memories": [],
|
|
1032
|
+
"changed_files": changed_files,
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
# Read convention files from config
|
|
1036
|
+
for conv_file in config.conventions.files:
|
|
1037
|
+
conv_path = path / conv_file
|
|
1038
|
+
if conv_path.exists():
|
|
1039
|
+
if conv_path.is_file():
|
|
1040
|
+
content = conv_path.read_text()
|
|
1041
|
+
conventions_data["files"].append({
|
|
1042
|
+
"path": str(conv_file),
|
|
1043
|
+
"content": content,
|
|
1044
|
+
})
|
|
1045
|
+
elif conv_path.is_dir():
|
|
1046
|
+
# If it's a directory, read all files in it
|
|
1047
|
+
for f in conv_path.rglob("*"):
|
|
1048
|
+
if f.is_file() and f.suffix in (".md", ".txt", ".yaml", ".yml"):
|
|
1049
|
+
content = f.read_text()
|
|
1050
|
+
conventions_data["files"].append({
|
|
1051
|
+
"path": str(f.relative_to(path)),
|
|
1052
|
+
"content": content,
|
|
1053
|
+
})
|
|
1054
|
+
|
|
1055
|
+
# Search memories for conventions
|
|
1056
|
+
if include_memories and config.conventions.also_search_memories:
|
|
1057
|
+
store = get_memory_store(path)
|
|
1058
|
+
db = get_db(path)
|
|
1059
|
+
|
|
1060
|
+
if return_all:
|
|
1061
|
+
# Return ALL convention/pattern memories (for AI workflows)
|
|
1062
|
+
for ns in ["team", "app"]:
|
|
1063
|
+
memories = store.list_memories(namespace=ns, type_filter="convention", status="active")
|
|
1064
|
+
for mem in memories:
|
|
1065
|
+
conventions_data["memories"].append({
|
|
1066
|
+
"id": mem.id,
|
|
1067
|
+
"namespace": mem.namespace,
|
|
1068
|
+
"content": mem.content,
|
|
1069
|
+
"component": mem.component,
|
|
1070
|
+
})
|
|
1071
|
+
# Also get pattern-type memories
|
|
1072
|
+
patterns = store.list_memories(namespace=ns, type_filter="pattern", status="active")
|
|
1073
|
+
for mem in patterns:
|
|
1074
|
+
conventions_data["memories"].append({
|
|
1075
|
+
"id": mem.id,
|
|
1076
|
+
"namespace": mem.namespace,
|
|
1077
|
+
"type": "pattern",
|
|
1078
|
+
"content": mem.content,
|
|
1079
|
+
"component": mem.component,
|
|
1080
|
+
})
|
|
1081
|
+
else:
|
|
1082
|
+
# Semantic search for RELEVANT conventions based on changed files
|
|
1083
|
+
if changed_files:
|
|
1084
|
+
# Build search query from file paths and names
|
|
1085
|
+
# Extract meaningful terms: paths, extensions, inferred components
|
|
1086
|
+
search_terms = set()
|
|
1087
|
+
for f in changed_files:
|
|
1088
|
+
parts = Path(f).parts
|
|
1089
|
+
search_terms.update(parts) # Add path components
|
|
1090
|
+
search_terms.add(Path(f).suffix.lstrip(".")) # Add extension
|
|
1091
|
+
search_terms.add(Path(f).stem) # Add filename without ext
|
|
1092
|
+
|
|
1093
|
+
# Remove common noise
|
|
1094
|
+
noise = {"src", "lib", "test", "tests", "spec", "specs", "", "js", "ts", "py", "md"}
|
|
1095
|
+
search_terms = search_terms - noise
|
|
1096
|
+
|
|
1097
|
+
query = f"conventions patterns rules standards for {' '.join(search_terms)}"
|
|
1098
|
+
|
|
1099
|
+
# Search both namespaces using the db directly
|
|
1100
|
+
seen_ids = set()
|
|
1101
|
+
for ns in ["team", "app"]:
|
|
1102
|
+
results = db.search(query, namespace=ns, limit=50)
|
|
1103
|
+
for result in results:
|
|
1104
|
+
meta = result.get("metadata", {})
|
|
1105
|
+
result_type = meta.get("type", "")
|
|
1106
|
+
result_category = meta.get("category", "")
|
|
1107
|
+
# Include convention/pattern types OR docs with category="convention"
|
|
1108
|
+
is_convention_content = (
|
|
1109
|
+
result_type in ("convention", "pattern") or
|
|
1110
|
+
result_category == "convention"
|
|
1111
|
+
)
|
|
1112
|
+
if is_convention_content:
|
|
1113
|
+
mem_id = meta.get("id") or meta.get("file", "")
|
|
1114
|
+
if mem_id not in seen_ids:
|
|
1115
|
+
seen_ids.add(mem_id)
|
|
1116
|
+
distance = result.get("distance", 1)
|
|
1117
|
+
score = 1 - distance if distance else 0
|
|
1118
|
+
conventions_data["memories"].append({
|
|
1119
|
+
"id": mem_id,
|
|
1120
|
+
"namespace": ns,
|
|
1121
|
+
"type": result_type,
|
|
1122
|
+
"content": result.get("content", ""),
|
|
1123
|
+
"component": meta.get("component"),
|
|
1124
|
+
"relevance": score,
|
|
1125
|
+
})
|
|
1126
|
+
else:
|
|
1127
|
+
# No changed files - fall back to listing all
|
|
1128
|
+
for ns in ["team", "app"]:
|
|
1129
|
+
memories = store.list_memories(namespace=ns, type_filter="convention", status="active")
|
|
1130
|
+
for mem in memories:
|
|
1131
|
+
conventions_data["memories"].append({
|
|
1132
|
+
"id": mem.id,
|
|
1133
|
+
"namespace": mem.namespace,
|
|
1134
|
+
"content": mem.content,
|
|
1135
|
+
"component": mem.component,
|
|
1136
|
+
})
|
|
1137
|
+
|
|
1138
|
+
# Output
|
|
1139
|
+
if json_output:
|
|
1140
|
+
click.echo(json_module.dumps(conventions_data, indent=2))
|
|
1141
|
+
return
|
|
1142
|
+
|
|
1143
|
+
# Human-friendly output
|
|
1144
|
+
click.echo(f"\nConventions Check")
|
|
1145
|
+
click.echo(f"{'═' * 50}")
|
|
1146
|
+
|
|
1147
|
+
if changed_files:
|
|
1148
|
+
click.echo(f"\nFiles to check ({len(changed_files)}):")
|
|
1149
|
+
for f in changed_files[:10]:
|
|
1150
|
+
click.echo(f" • {f}")
|
|
1151
|
+
if len(changed_files) > 10:
|
|
1152
|
+
click.echo(f" ... and {len(changed_files) - 10} more")
|
|
1153
|
+
|
|
1154
|
+
click.echo(f"\n{'─' * 50}")
|
|
1155
|
+
click.echo(f"Convention Files ({len(conventions_data['files'])}):")
|
|
1156
|
+
|
|
1157
|
+
if not conventions_data["files"]:
|
|
1158
|
+
click.echo(" No convention files found.")
|
|
1159
|
+
click.echo(f" Configure in .ragtime/config.yaml under 'conventions.files'")
|
|
1160
|
+
else:
|
|
1161
|
+
for conv in conventions_data["files"]:
|
|
1162
|
+
click.echo(f"\n 📄 {conv['path']}")
|
|
1163
|
+
# Show first few lines as preview
|
|
1164
|
+
lines = conv["content"].strip().split("\n")[:5]
|
|
1165
|
+
for line in lines:
|
|
1166
|
+
if line.strip():
|
|
1167
|
+
click.echo(f" {line[:70]}{'...' if len(line) > 70 else ''}")
|
|
1168
|
+
|
|
1169
|
+
if conventions_data["memories"]:
|
|
1170
|
+
click.echo(f"\n{'─' * 50}")
|
|
1171
|
+
mode_label = "All" if return_all else "Relevant"
|
|
1172
|
+
click.echo(f"Convention Memories - {mode_label} ({len(conventions_data['memories'])}):")
|
|
1173
|
+
|
|
1174
|
+
for mem in conventions_data["memories"]:
|
|
1175
|
+
type_str = mem.get("type", "convention")
|
|
1176
|
+
relevance = mem.get("relevance")
|
|
1177
|
+
relevance_str = f" (relevance: {relevance:.2f})" if relevance else ""
|
|
1178
|
+
click.echo(f"\n [{mem['id'][:8] if mem['id'] else '?'}] {mem['namespace']} / {type_str}{relevance_str}")
|
|
1179
|
+
if mem.get("component"):
|
|
1180
|
+
click.echo(f" Component: {mem['component']}")
|
|
1181
|
+
preview = mem["content"][:100].replace("\n", " ")
|
|
1182
|
+
click.echo(f" {preview}...")
|
|
1183
|
+
|
|
1184
|
+
click.echo(f"\n{'═' * 50}")
|
|
1185
|
+
total = len(conventions_data["files"]) + len(conventions_data["memories"])
|
|
1186
|
+
mode_note = " (all)" if return_all else " (filtered by relevance)"
|
|
1187
|
+
click.echo(f"Total: {total} convention source(s){mode_note}")
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
@main.command("add-convention")
|
|
1191
|
+
@click.argument("content", required=False)
|
|
1192
|
+
@click.option("--path", type=click.Path(exists=True, path_type=Path), default=".")
|
|
1193
|
+
@click.option("--component", "-c", help="Component this convention applies to")
|
|
1194
|
+
@click.option("--to-file", type=click.Path(path_type=Path), help="Add to specific file")
|
|
1195
|
+
@click.option("--to-memory", is_flag=True, help="Store as memory only (not in git)")
|
|
1196
|
+
@click.option("--quiet", "-q", is_flag=True, help="Non-interactive mode")
|
|
1197
|
+
def add_convention(content: str | None, path: Path, component: str | None,
|
|
1198
|
+
to_file: Path | None, to_memory: bool, quiet: bool):
|
|
1199
|
+
"""Add a new convention with smart storage routing.
|
|
1200
|
+
|
|
1201
|
+
Finds the best place to store the convention:
|
|
1202
|
+
- Existing doc with ## Conventions section (if component matches)
|
|
1203
|
+
- Convention folder (.ragtime/conventions/)
|
|
1204
|
+
- Central conventions file (.ragtime/CONVENTIONS.md)
|
|
1205
|
+
- Memory only (searchable but not committed)
|
|
1206
|
+
|
|
1207
|
+
Examples:
|
|
1208
|
+
ragtime add-convention "Always use async/await, never .then()"
|
|
1209
|
+
ragtime add-convention --component auth "JWT tokens expire after 15 minutes"
|
|
1210
|
+
ragtime add-convention --to-file docs/api.md "Use snake_case for JSON"
|
|
1211
|
+
ragtime add-convention --to-memory "Prefer composition over inheritance"
|
|
1212
|
+
"""
|
|
1213
|
+
path = Path(path).resolve()
|
|
1214
|
+
config = RagtimeConfig.load(path)
|
|
1215
|
+
|
|
1216
|
+
# Get content interactively if not provided
|
|
1217
|
+
if not content:
|
|
1218
|
+
content = click.prompt("Convention to add")
|
|
1219
|
+
|
|
1220
|
+
if not content.strip():
|
|
1221
|
+
click.echo("✗ No content provided", err=True)
|
|
1222
|
+
return
|
|
1223
|
+
|
|
1224
|
+
# If explicit destination provided, use it
|
|
1225
|
+
if to_memory:
|
|
1226
|
+
_store_convention_as_memory(path, content, component)
|
|
1227
|
+
return
|
|
1228
|
+
|
|
1229
|
+
if to_file:
|
|
1230
|
+
_append_convention_to_file(path, to_file, content)
|
|
1231
|
+
return
|
|
1232
|
+
|
|
1233
|
+
# Auto-routing based on config
|
|
1234
|
+
storage_mode = config.conventions.storage
|
|
1235
|
+
|
|
1236
|
+
if storage_mode == "memory":
|
|
1237
|
+
_store_convention_as_memory(path, content, component)
|
|
1238
|
+
return
|
|
1239
|
+
|
|
1240
|
+
# Find storage options
|
|
1241
|
+
options = _find_convention_storage_options(path, config, component)
|
|
1242
|
+
|
|
1243
|
+
if storage_mode == "file" or (storage_mode == "auto" and options):
|
|
1244
|
+
# Use first available file option, or default
|
|
1245
|
+
if options:
|
|
1246
|
+
target = options[0]
|
|
1247
|
+
else:
|
|
1248
|
+
target = {
|
|
1249
|
+
"type": "default_file",
|
|
1250
|
+
"path": config.conventions.default_file,
|
|
1251
|
+
"description": "Central conventions file",
|
|
1252
|
+
}
|
|
1253
|
+
_store_convention_to_target(path, target, content, component)
|
|
1254
|
+
return
|
|
1255
|
+
|
|
1256
|
+
if storage_mode == "ask" or (storage_mode == "auto" and not quiet):
|
|
1257
|
+
# Present options to user
|
|
1258
|
+
click.echo("\nWhere should this convention be stored?\n")
|
|
1259
|
+
|
|
1260
|
+
choices = []
|
|
1261
|
+
for i, opt in enumerate(options, 1):
|
|
1262
|
+
icon = "📄" if opt["type"] == "existing_section" else "📁" if opt["type"] == "folder" else "📋"
|
|
1263
|
+
click.echo(f" {i}. {icon} {opt['description']}")
|
|
1264
|
+
click.echo(f" {opt['path']}")
|
|
1265
|
+
choices.append(opt)
|
|
1266
|
+
|
|
1267
|
+
# Add default options if not already present
|
|
1268
|
+
default_file_present = any(o["path"] == config.conventions.default_file for o in choices)
|
|
1269
|
+
if not default_file_present:
|
|
1270
|
+
i = len(choices) + 1
|
|
1271
|
+
click.echo(f" {i}. 📋 Central conventions file")
|
|
1272
|
+
click.echo(f" {config.conventions.default_file}")
|
|
1273
|
+
choices.append({
|
|
1274
|
+
"type": "default_file",
|
|
1275
|
+
"path": config.conventions.default_file,
|
|
1276
|
+
"description": "Central conventions file",
|
|
1277
|
+
})
|
|
1278
|
+
|
|
1279
|
+
i = len(choices) + 1
|
|
1280
|
+
click.echo(f" {i}. 🧠 Memory only (not committed)")
|
|
1281
|
+
click.echo(f" Stored in team namespace, searchable but not in git")
|
|
1282
|
+
choices.append({"type": "memory", "path": None, "description": "Memory only"})
|
|
1283
|
+
|
|
1284
|
+
click.echo()
|
|
1285
|
+
choice = click.prompt("Choice", type=int, default=1)
|
|
1286
|
+
|
|
1287
|
+
if choice < 1 or choice > len(choices):
|
|
1288
|
+
click.echo("✗ Invalid choice", err=True)
|
|
1289
|
+
return
|
|
1290
|
+
|
|
1291
|
+
target = choices[choice - 1]
|
|
1292
|
+
if target["type"] == "memory":
|
|
1293
|
+
_store_convention_as_memory(path, content, component)
|
|
1294
|
+
else:
|
|
1295
|
+
_store_convention_to_target(path, target, content, component)
|
|
1296
|
+
return
|
|
1297
|
+
|
|
1298
|
+
# Fallback: memory only
|
|
1299
|
+
_store_convention_as_memory(path, content, component)
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
def _find_convention_storage_options(path: Path, config: RagtimeConfig, component: str | None) -> list[dict]:
|
|
1303
|
+
"""Find potential storage locations for a convention."""
|
|
1304
|
+
import re
|
|
1305
|
+
options = []
|
|
1306
|
+
|
|
1307
|
+
# 1. Check for existing docs with ## Conventions sections
|
|
1308
|
+
for scan_path in config.conventions.scan_docs_for_sections:
|
|
1309
|
+
scan_dir = path / scan_path
|
|
1310
|
+
if scan_dir.exists() and scan_dir.is_dir():
|
|
1311
|
+
for md_file in scan_dir.rglob("*.md"):
|
|
1312
|
+
content = md_file.read_text()
|
|
1313
|
+
# Look for ## Conventions header
|
|
1314
|
+
if re.search(r'^##\s+(Conventions?|Rules|Standards|Guidelines)', content, re.MULTILINE | re.IGNORECASE):
|
|
1315
|
+
# If component specified, check if file relates to it
|
|
1316
|
+
file_component = md_file.stem.lower()
|
|
1317
|
+
rel_path = md_file.relative_to(path)
|
|
1318
|
+
|
|
1319
|
+
if component:
|
|
1320
|
+
if component.lower() in str(rel_path).lower():
|
|
1321
|
+
options.insert(0, { # Prioritize component match
|
|
1322
|
+
"type": "existing_section",
|
|
1323
|
+
"path": str(rel_path),
|
|
1324
|
+
"description": f"Add to existing Conventions section ({file_component})",
|
|
1325
|
+
})
|
|
1326
|
+
else:
|
|
1327
|
+
options.append({
|
|
1328
|
+
"type": "existing_section",
|
|
1329
|
+
"path": str(rel_path),
|
|
1330
|
+
"description": f"Add to existing Conventions section",
|
|
1331
|
+
})
|
|
1332
|
+
|
|
1333
|
+
# 2. Check if convention folder exists
|
|
1334
|
+
folder = path / config.conventions.folder
|
|
1335
|
+
if folder.exists() and folder.is_dir():
|
|
1336
|
+
if component:
|
|
1337
|
+
target_file = f"{component.lower()}.md"
|
|
1338
|
+
options.append({
|
|
1339
|
+
"type": "folder",
|
|
1340
|
+
"path": f"{config.conventions.folder}{target_file}",
|
|
1341
|
+
"description": f"Create/append to {target_file} in conventions folder",
|
|
1342
|
+
})
|
|
1343
|
+
else:
|
|
1344
|
+
options.append({
|
|
1345
|
+
"type": "folder",
|
|
1346
|
+
"path": f"{config.conventions.folder}general.md",
|
|
1347
|
+
"description": "Add to general.md in conventions folder",
|
|
1348
|
+
})
|
|
1349
|
+
|
|
1350
|
+
# 3. Check if default conventions file exists
|
|
1351
|
+
default_file = path / config.conventions.default_file
|
|
1352
|
+
if default_file.exists():
|
|
1353
|
+
options.append({
|
|
1354
|
+
"type": "default_file",
|
|
1355
|
+
"path": config.conventions.default_file,
|
|
1356
|
+
"description": "Append to central conventions file",
|
|
1357
|
+
})
|
|
1358
|
+
|
|
1359
|
+
return options
|
|
1360
|
+
|
|
1361
|
+
|
|
1362
|
+
def _store_convention_as_memory(path: Path, content: str, component: str | None):
|
|
1363
|
+
"""Store convention as a memory in team namespace."""
|
|
1364
|
+
from datetime import date
|
|
1365
|
+
store = get_memory_store(path)
|
|
1366
|
+
|
|
1367
|
+
memory = Memory(
|
|
1368
|
+
content=content,
|
|
1369
|
+
namespace="team",
|
|
1370
|
+
type="convention",
|
|
1371
|
+
component=component,
|
|
1372
|
+
confidence="high",
|
|
1373
|
+
confidence_reason="user-added",
|
|
1374
|
+
source="add-convention",
|
|
1375
|
+
status="active",
|
|
1376
|
+
added=date.today().isoformat(),
|
|
1377
|
+
author=get_author(),
|
|
1378
|
+
)
|
|
1379
|
+
|
|
1380
|
+
file_path = store.save(memory)
|
|
1381
|
+
click.echo(f"✓ Convention stored as memory")
|
|
1382
|
+
click.echo(f" ID: {memory.id}")
|
|
1383
|
+
click.echo(f" File: {file_path.relative_to(path)}")
|
|
1384
|
+
click.echo(f" Namespace: team")
|
|
1385
|
+
|
|
1386
|
+
|
|
1387
|
+
def _store_convention_to_target(path: Path, target: dict, content: str, component: str | None):
|
|
1388
|
+
"""Store convention to a file target."""
|
|
1389
|
+
import re
|
|
1390
|
+
|
|
1391
|
+
target_path = path / target["path"]
|
|
1392
|
+
target_type = target["type"]
|
|
1393
|
+
|
|
1394
|
+
if target_type == "existing_section":
|
|
1395
|
+
# Append to existing ## Conventions section
|
|
1396
|
+
if not target_path.exists():
|
|
1397
|
+
click.echo(f"✗ File not found: {target['path']}", err=True)
|
|
1398
|
+
return
|
|
1399
|
+
|
|
1400
|
+
file_content = target_path.read_text()
|
|
1401
|
+
|
|
1402
|
+
# Find the Conventions section and append
|
|
1403
|
+
pattern = r'(^##\s+(?:Conventions?|Rules|Standards|Guidelines).*?)(\n##|\Z)'
|
|
1404
|
+
match = re.search(pattern, file_content, re.MULTILINE | re.IGNORECASE | re.DOTALL)
|
|
1405
|
+
|
|
1406
|
+
if match:
|
|
1407
|
+
section_end = match.end(1)
|
|
1408
|
+
new_content = file_content[:section_end] + f"\n\n- {content}" + file_content[section_end:]
|
|
1409
|
+
target_path.write_text(new_content)
|
|
1410
|
+
click.echo(f"✓ Convention added to {target['path']}")
|
|
1411
|
+
click.echo(f" Added to ## Conventions section")
|
|
1412
|
+
else:
|
|
1413
|
+
click.echo(f"✗ Could not find Conventions section in {target['path']}", err=True)
|
|
1414
|
+
|
|
1415
|
+
elif target_type == "folder":
|
|
1416
|
+
# Create or append to file in conventions folder
|
|
1417
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1418
|
+
|
|
1419
|
+
if target_path.exists():
|
|
1420
|
+
# Append to existing file
|
|
1421
|
+
existing = target_path.read_text()
|
|
1422
|
+
new_content = existing.rstrip() + f"\n\n- {content}\n"
|
|
1423
|
+
else:
|
|
1424
|
+
# Create new file
|
|
1425
|
+
title = target_path.stem.replace("-", " ").replace("_", " ").title()
|
|
1426
|
+
new_content = f"""---
|
|
1427
|
+
namespace: team
|
|
1428
|
+
type: convention
|
|
1429
|
+
component: {component or ''}
|
|
1430
|
+
---
|
|
1431
|
+
|
|
1432
|
+
# {title} Conventions
|
|
1433
|
+
|
|
1434
|
+
- {content}
|
|
1435
|
+
"""
|
|
1436
|
+
target_path.write_text(new_content)
|
|
1437
|
+
click.echo(f"✓ Convention added to {target['path']}")
|
|
1438
|
+
|
|
1439
|
+
elif target_type == "default_file":
|
|
1440
|
+
# Create or append to default conventions file
|
|
1441
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1442
|
+
|
|
1443
|
+
if target_path.exists():
|
|
1444
|
+
existing = target_path.read_text()
|
|
1445
|
+
# Check if there's a component section
|
|
1446
|
+
if component:
|
|
1447
|
+
section_pattern = rf'^##\s+{re.escape(component)}.*?(?=\n##|\Z)'
|
|
1448
|
+
match = re.search(section_pattern, existing, re.MULTILINE | re.IGNORECASE | re.DOTALL)
|
|
1449
|
+
if match:
|
|
1450
|
+
# Append to component section
|
|
1451
|
+
section_end = match.end()
|
|
1452
|
+
new_content = existing[:section_end] + f"\n- {content}" + existing[section_end:]
|
|
1453
|
+
else:
|
|
1454
|
+
# Create new component section
|
|
1455
|
+
new_content = existing.rstrip() + f"\n\n## {component.title()}\n\n- {content}\n"
|
|
1456
|
+
else:
|
|
1457
|
+
# Append to general section or end
|
|
1458
|
+
new_content = existing.rstrip() + f"\n\n- {content}\n"
|
|
1459
|
+
else:
|
|
1460
|
+
# Create new file
|
|
1461
|
+
header = f"## {component.title()}\n\n" if component else ""
|
|
1462
|
+
new_content = f"""# Team Conventions
|
|
1463
|
+
|
|
1464
|
+
{header}- {content}
|
|
1465
|
+
"""
|
|
1466
|
+
target_path.write_text(new_content)
|
|
1467
|
+
click.echo(f"✓ Convention added to {target['path']}")
|
|
1468
|
+
|
|
1469
|
+
# Reindex the file
|
|
1470
|
+
_reindex_convention_file(path, target_path)
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
def _append_convention_to_file(path: Path, to_file: Path, content: str):
|
|
1474
|
+
"""Append a convention directly to a user-specified file."""
|
|
1475
|
+
import re
|
|
1476
|
+
|
|
1477
|
+
target_path = path / to_file if not to_file.is_absolute() else to_file
|
|
1478
|
+
|
|
1479
|
+
if not target_path.exists():
|
|
1480
|
+
click.echo(f"✗ File not found: {to_file}", err=True)
|
|
1481
|
+
return
|
|
1482
|
+
|
|
1483
|
+
file_content = target_path.read_text()
|
|
1484
|
+
|
|
1485
|
+
# Try to find a Conventions section to append to
|
|
1486
|
+
pattern = r'(^##\s+(?:Conventions?|Rules|Standards|Guidelines).*?)(\n##|\Z)'
|
|
1487
|
+
match = re.search(pattern, file_content, re.MULTILINE | re.IGNORECASE | re.DOTALL)
|
|
1488
|
+
|
|
1489
|
+
if match:
|
|
1490
|
+
# Append to existing section
|
|
1491
|
+
section_end = match.end(1)
|
|
1492
|
+
new_content = file_content[:section_end] + f"\n\n- {content}" + file_content[section_end:]
|
|
1493
|
+
target_path.write_text(new_content)
|
|
1494
|
+
click.echo(f"✓ Convention added to {to_file}")
|
|
1495
|
+
click.echo(f" Added to ## Conventions section")
|
|
1496
|
+
else:
|
|
1497
|
+
# Append at end of file
|
|
1498
|
+
new_content = file_content.rstrip() + f"\n\n- {content}\n"
|
|
1499
|
+
target_path.write_text(new_content)
|
|
1500
|
+
click.echo(f"✓ Convention appended to {to_file}")
|
|
1501
|
+
|
|
1502
|
+
# Reindex the file
|
|
1503
|
+
_reindex_convention_file(path, target_path)
|
|
1504
|
+
|
|
1505
|
+
|
|
1506
|
+
def _reindex_convention_file(path: Path, target_path: Path):
|
|
1507
|
+
"""Reindex a convention file after modification."""
|
|
1508
|
+
try:
|
|
1509
|
+
db = get_db(path)
|
|
1510
|
+
from .indexers.docs import index_file
|
|
1511
|
+
entries = index_file(target_path)
|
|
1512
|
+
for entry in entries:
|
|
1513
|
+
db.upsert(
|
|
1514
|
+
ids=[f"{entry.file_path}:{entry.chunk_index}"],
|
|
1515
|
+
documents=[entry.content],
|
|
1516
|
+
metadatas=[entry.to_metadata()],
|
|
1517
|
+
)
|
|
1518
|
+
click.echo(f" Indexed {len(entries)} section(s)")
|
|
1519
|
+
except Exception as e:
|
|
1520
|
+
click.echo(f" ⚠ Could not reindex: {e}")
|
|
1521
|
+
|
|
1522
|
+
|
|
896
1523
|
@main.command()
|
|
897
1524
|
@click.option("--path", type=click.Path(exists=True, path_type=Path), default=".")
|
|
898
1525
|
@click.option("--dry-run", is_flag=True, help="Show what would be removed")
|
|
@@ -1020,9 +1647,9 @@ def new_branch(issue: int, path: Path, content: str, issue_json: str, branch: st
|
|
|
1020
1647
|
click.echo(f"✗ Could not fetch issue #{issue}", err=True)
|
|
1021
1648
|
return
|
|
1022
1649
|
|
|
1023
|
-
title = issue_data.get("title"
|
|
1024
|
-
body = issue_data.get("body"
|
|
1025
|
-
labels = issue_data.get("labels"
|
|
1650
|
+
title = issue_data.get("title") or f"Issue #{issue}"
|
|
1651
|
+
body = issue_data.get("body") or "" # Handle null and empty
|
|
1652
|
+
labels = issue_data.get("labels") or []
|
|
1026
1653
|
|
|
1027
1654
|
if labels:
|
|
1028
1655
|
if isinstance(labels[0], dict):
|
|
@@ -1076,115 +1703,244 @@ author: {get_author()}
|
|
|
1076
1703
|
|
|
1077
1704
|
|
|
1078
1705
|
# ============================================================================
|
|
1079
|
-
#
|
|
1706
|
+
# Usage Documentation
|
|
1080
1707
|
# ============================================================================
|
|
1081
1708
|
|
|
1082
1709
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1710
|
+
@main.command("usage")
|
|
1711
|
+
@click.option("--section", "-s", help="Show specific section (mcp, cli, workflows, conventions)")
|
|
1712
|
+
def usage(section: str | None):
|
|
1713
|
+
"""Show how to use ragtime effectively with AI agents.
|
|
1086
1714
|
|
|
1715
|
+
Prints documentation on integrating ragtime into your AI workflow,
|
|
1716
|
+
whether using the MCP server or CLI directly.
|
|
1717
|
+
"""
|
|
1718
|
+
sections = {
|
|
1719
|
+
"mcp": USAGE_MCP,
|
|
1720
|
+
"cli": USAGE_CLI,
|
|
1721
|
+
"workflows": USAGE_WORKFLOWS,
|
|
1722
|
+
"conventions": USAGE_CONVENTIONS,
|
|
1723
|
+
}
|
|
1087
1724
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
@main.command("install")
|
|
1097
|
-
@click.option("--global", "global_install", is_flag=True, help="Install to ~/.claude/commands/")
|
|
1098
|
-
@click.option("--workspace", "workspace_install", is_flag=True, help="Install to .claude/commands/")
|
|
1099
|
-
@click.option("--list", "list_commands", is_flag=True, help="List available commands")
|
|
1100
|
-
@click.option("--force", is_flag=True, help="Overwrite existing commands without asking")
|
|
1101
|
-
@click.argument("commands", nargs=-1)
|
|
1102
|
-
def install_commands(global_install: bool, workspace_install: bool, list_commands: bool,
|
|
1103
|
-
force: bool, commands: tuple):
|
|
1104
|
-
"""Install Claude command templates."""
|
|
1105
|
-
available = get_available_commands()
|
|
1106
|
-
|
|
1107
|
-
if list_commands:
|
|
1108
|
-
click.echo("Available commands:")
|
|
1109
|
-
for cmd in available:
|
|
1110
|
-
click.echo(f" - {cmd}")
|
|
1725
|
+
if section:
|
|
1726
|
+
if section.lower() in sections:
|
|
1727
|
+
click.echo(sections[section.lower()])
|
|
1728
|
+
else:
|
|
1729
|
+
click.echo(f"Unknown section: {section}")
|
|
1730
|
+
click.echo(f"Available: {', '.join(sections.keys())}")
|
|
1111
1731
|
return
|
|
1112
1732
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1733
|
+
# Print all sections
|
|
1734
|
+
click.echo(USAGE_HEADER)
|
|
1735
|
+
for content in sections.values():
|
|
1736
|
+
click.echo(content)
|
|
1737
|
+
click.echo()
|
|
1116
1738
|
|
|
1117
|
-
if global_install:
|
|
1118
|
-
target_dir = Path.home() / ".claude" / "commands"
|
|
1119
|
-
elif workspace_install:
|
|
1120
|
-
target_dir = Path.cwd() / ".claude" / "commands"
|
|
1121
|
-
else:
|
|
1122
|
-
target_dir = Path.cwd() / ".claude" / "commands"
|
|
1123
|
-
click.echo("Installing to workspace (.claude/commands/)")
|
|
1124
|
-
|
|
1125
|
-
if commands:
|
|
1126
|
-
to_install = [c for c in commands if c in available]
|
|
1127
|
-
not_found = [c for c in commands if c not in available]
|
|
1128
|
-
if not_found:
|
|
1129
|
-
click.echo(f"Warning: Commands not found: {', '.join(not_found)}", err=True)
|
|
1130
|
-
else:
|
|
1131
|
-
to_install = available
|
|
1132
1739
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
return
|
|
1740
|
+
USAGE_HEADER = """
|
|
1741
|
+
# Ragtime Usage Guide
|
|
1136
1742
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1743
|
+
Ragtime provides persistent memory for AI coding sessions. Use it via:
|
|
1744
|
+
- **MCP Server** (recommended for Claude Code / AI tools)
|
|
1745
|
+
- **CLI** (for scripts, hooks, and manual use)
|
|
1746
|
+
"""
|
|
1747
|
+
|
|
1748
|
+
USAGE_MCP = """
|
|
1749
|
+
## MCP Server Tools
|
|
1750
|
+
|
|
1751
|
+
After `ragtime init`, add to your `.mcp.json`:
|
|
1752
|
+
|
|
1753
|
+
```json
|
|
1754
|
+
{
|
|
1755
|
+
"mcpServers": {
|
|
1756
|
+
"ragtime": {
|
|
1757
|
+
"command": "ragtime",
|
|
1758
|
+
"args": ["serve"]
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
```
|
|
1763
|
+
|
|
1764
|
+
### Core Tools
|
|
1765
|
+
|
|
1766
|
+
| Tool | Purpose |
|
|
1767
|
+
|------|---------|
|
|
1768
|
+
| `mcp__ragtime__search` | Find relevant memories, docs, and code |
|
|
1769
|
+
| `mcp__ragtime__remember` | Store new knowledge |
|
|
1770
|
+
| `mcp__ragtime__list_memories` | Browse stored memories |
|
|
1771
|
+
| `mcp__ragtime__forget` | Delete a memory |
|
|
1772
|
+
| `mcp__ragtime__graduate` | Promote branch memory to app namespace |
|
|
1773
|
+
|
|
1774
|
+
### Search Examples
|
|
1775
|
+
|
|
1776
|
+
```
|
|
1777
|
+
# Find architecture knowledge
|
|
1778
|
+
mcp__ragtime__search(query="authentication flow", namespace="app")
|
|
1779
|
+
|
|
1780
|
+
# Find team conventions
|
|
1781
|
+
mcp__ragtime__search(query="error handling", namespace="team", type="convention")
|
|
1782
|
+
|
|
1783
|
+
# Search with auto-qualifier detection
|
|
1784
|
+
mcp__ragtime__search(query="how does auth work in mobile", tiered=true)
|
|
1785
|
+
```
|
|
1786
|
+
|
|
1787
|
+
### Remember Examples
|
|
1788
|
+
|
|
1789
|
+
```
|
|
1790
|
+
# Store architecture insight
|
|
1791
|
+
mcp__ragtime__remember(
|
|
1792
|
+
content="JWT tokens expire after 15 minutes, refresh tokens after 7 days",
|
|
1793
|
+
namespace="app",
|
|
1794
|
+
type="architecture",
|
|
1795
|
+
component="auth"
|
|
1796
|
+
)
|
|
1797
|
+
|
|
1798
|
+
# Store team convention
|
|
1799
|
+
mcp__ragtime__remember(
|
|
1800
|
+
content="Always use async/await, never .then() chains",
|
|
1801
|
+
namespace="team",
|
|
1802
|
+
type="convention"
|
|
1803
|
+
)
|
|
1804
|
+
|
|
1805
|
+
# Store branch-specific decision
|
|
1806
|
+
mcp__ragtime__remember(
|
|
1807
|
+
content="Using Redis for session storage because of horizontal scaling needs",
|
|
1808
|
+
namespace="branch-feature/auth",
|
|
1809
|
+
type="decision"
|
|
1810
|
+
)
|
|
1811
|
+
```
|
|
1812
|
+
"""
|
|
1813
|
+
|
|
1814
|
+
USAGE_CLI = """
|
|
1815
|
+
## CLI Commands
|
|
1816
|
+
|
|
1817
|
+
### Memory Management
|
|
1818
|
+
|
|
1819
|
+
```bash
|
|
1820
|
+
# Search memories
|
|
1821
|
+
ragtime search "authentication patterns"
|
|
1822
|
+
ragtime search "conventions" --namespace team
|
|
1823
|
+
|
|
1824
|
+
# Add a convention
|
|
1825
|
+
ragtime add-convention "Always validate user input"
|
|
1826
|
+
ragtime add-convention -c auth "JWT must be validated on every request"
|
|
1827
|
+
|
|
1828
|
+
# List memories
|
|
1829
|
+
ragtime memories --namespace app
|
|
1830
|
+
ragtime memories --type convention
|
|
1831
|
+
|
|
1832
|
+
# Check conventions for changed files
|
|
1833
|
+
ragtime check-conventions
|
|
1834
|
+
ragtime check-conventions --all # For AI (comprehensive)
|
|
1835
|
+
```
|
|
1836
|
+
|
|
1837
|
+
### Branch Context
|
|
1838
|
+
|
|
1839
|
+
```bash
|
|
1840
|
+
# Create branch context from GitHub issue
|
|
1841
|
+
ragtime new-branch 123 --branch feature/auth
|
|
1842
|
+
|
|
1843
|
+
# Graduate branch memories after PR merge
|
|
1844
|
+
ragtime graduate --branch feature/auth
|
|
1845
|
+
```
|
|
1846
|
+
|
|
1847
|
+
### Indexing
|
|
1848
|
+
|
|
1849
|
+
```bash
|
|
1850
|
+
# Index docs and code
|
|
1851
|
+
ragtime index
|
|
1852
|
+
ragtime reindex # Full reindex
|
|
1853
|
+
```
|
|
1854
|
+
"""
|
|
1182
1855
|
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1856
|
+
USAGE_WORKFLOWS = """
|
|
1857
|
+
## Recommended Workflows
|
|
1858
|
+
|
|
1859
|
+
### Starting Work on an Issue
|
|
1860
|
+
|
|
1861
|
+
1. AI reads the issue and existing context:
|
|
1862
|
+
```
|
|
1863
|
+
mcp__ragtime__search(query="auth implementation", namespace="app")
|
|
1864
|
+
```
|
|
1865
|
+
|
|
1866
|
+
2. Check for branch context:
|
|
1867
|
+
```bash
|
|
1868
|
+
# Look for .ragtime/branches/{branch}/context.md
|
|
1869
|
+
```
|
|
1870
|
+
|
|
1871
|
+
3. As you make decisions, store them:
|
|
1872
|
+
```
|
|
1873
|
+
mcp__ragtime__remember(
|
|
1874
|
+
content="Chose PKCE flow for mobile OAuth",
|
|
1875
|
+
namespace="branch-feature/oauth",
|
|
1876
|
+
type="decision"
|
|
1877
|
+
)
|
|
1878
|
+
```
|
|
1879
|
+
|
|
1880
|
+
### Before Creating a PR
|
|
1881
|
+
|
|
1882
|
+
1. Check conventions:
|
|
1883
|
+
```bash
|
|
1884
|
+
ragtime check-conventions
|
|
1885
|
+
```
|
|
1886
|
+
|
|
1887
|
+
2. Review branch memories for graduation candidates
|
|
1888
|
+
|
|
1889
|
+
### After PR Merge
|
|
1890
|
+
|
|
1891
|
+
1. Graduate valuable branch memories to app namespace:
|
|
1892
|
+
```bash
|
|
1893
|
+
ragtime graduate --branch feature/auth
|
|
1894
|
+
```
|
|
1895
|
+
|
|
1896
|
+
### Session Handoff
|
|
1897
|
+
|
|
1898
|
+
Store context in `.ragtime/branches/{branch}/context.md`:
|
|
1899
|
+
- Current state
|
|
1900
|
+
- What's left to do
|
|
1901
|
+
- Key decisions made
|
|
1902
|
+
- Blockers or notes for next session
|
|
1903
|
+
"""
|
|
1904
|
+
|
|
1905
|
+
USAGE_CONVENTIONS = """
|
|
1906
|
+
## Convention System
|
|
1907
|
+
|
|
1908
|
+
### Storing Conventions
|
|
1909
|
+
|
|
1910
|
+
Conventions can live in:
|
|
1911
|
+
- **Files**: `.ragtime/CONVENTIONS.md` or `docs/conventions/`
|
|
1912
|
+
- **Doc sections**: Any `## Conventions` section in markdown
|
|
1913
|
+
- **Memories**: `team` namespace with type `convention`
|
|
1914
|
+
|
|
1915
|
+
```bash
|
|
1916
|
+
# Add via CLI (routes automatically)
|
|
1917
|
+
ragtime add-convention "Use snake_case for JSON fields"
|
|
1918
|
+
|
|
1919
|
+
# Add to specific file
|
|
1920
|
+
ragtime add-convention --to-file docs/api.md "Return 404 for missing resources"
|
|
1921
|
+
|
|
1922
|
+
# Add as memory only
|
|
1923
|
+
ragtime add-convention --to-memory "Prefer composition over inheritance"
|
|
1924
|
+
```
|
|
1925
|
+
|
|
1926
|
+
### Convention Detection
|
|
1927
|
+
|
|
1928
|
+
The indexer automatically detects conventions from:
|
|
1929
|
+
- Files named `*conventions*`, `*rules*`, `*standards*`
|
|
1930
|
+
- Sections with headers like `## Conventions`, `## Rules`
|
|
1931
|
+
- Content between `<!-- convention -->` markers
|
|
1932
|
+
- Frontmatter: `has_conventions: true` or `convention_sections: [...]`
|
|
1933
|
+
|
|
1934
|
+
### Checking Conventions
|
|
1935
|
+
|
|
1936
|
+
```bash
|
|
1937
|
+
# Human use (filtered to relevant)
|
|
1938
|
+
ragtime check-conventions
|
|
1939
|
+
|
|
1940
|
+
# AI use (comprehensive)
|
|
1941
|
+
ragtime check-conventions --all --json
|
|
1942
|
+
```
|
|
1943
|
+
"""
|
|
1188
1944
|
|
|
1189
1945
|
|
|
1190
1946
|
@main.command("setup-ghp")
|
|
@@ -1219,7 +1975,8 @@ def setup_ghp(remove: bool):
|
|
|
1219
1975
|
return
|
|
1220
1976
|
|
|
1221
1977
|
# Updated path for .ragtime/
|
|
1222
|
-
|
|
1978
|
+
# NOTE: No quotes around template vars - GHP's shellEscape() handles escaping
|
|
1979
|
+
hook_command = "ragtime new-branch ${issue.number} --issue-json ${issue.json} --branch ${branch}"
|
|
1223
1980
|
|
|
1224
1981
|
result = subprocess.run(
|
|
1225
1982
|
[
|