Compare commits

...

11 Commits

10 changed files with 1272 additions and 0 deletions

View File

@@ -0,0 +1,451 @@
#!/usr/bin/env python3
"""
Improvement Proposal Generator for compounding-intelligence.
Analyzes fleet session data to identify waste patterns and generates
concrete improvement proposals with ROI estimates.
Input: Session analytics JSON (from fleet metrics or session database)
Output: Markdown proposal document + JSON proposals
Usage:
python3 scripts/improvement_proposals.py --input analytics.json
python3 scripts/improvement_proposals.py --input analytics.json --format json
python3 scripts/improvement_proposals.py --input analytics.json --output proposals.md
python3 scripts/improvement_proposals.py --input analytics.json --threshold 5.0
"""
import argparse
import json
import os
import sys
from collections import defaultdict
from dataclasses import dataclass, field, asdict
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional
# ── Data types ─────────────────────────────────────────────────
@dataclass
class WastePattern:
"""A detected waste pattern in fleet sessions."""
pattern_type: str # "repeated_error", "manual_process", "slow_tool", "failed_retry"
description: str
occurrences: int
total_time_hours: float
affected_repos: List[str] = field(default_factory=list)
example_sessions: List[str] = field(default_factory=list)
@dataclass
class ImprovementProposal:
"""A concrete improvement proposal with ROI estimate."""
title: str
problem: str
proposed_solution: str
estimated_weekly_hours_saved: float
estimated_monthly_hours_saved: float
implementation_hours: float
roi_weeks: float # weeks to break even
priority: str # "critical", "high", "medium", "low"
affected_area: str
supporting_evidence: List[str] = field(default_factory=list)
# ── Session analysis ──────────────────────────────────────────
def analyze_sessions(sessions: List[dict]) -> List[WastePattern]:
"""
Analyze session data to find waste patterns.
Looks for:
- Repeated errors (same error across sessions)
- Manual processes (long sequences of similar actions)
- Slow tools (tools with high latency)
- Failed retries (multiple attempts at same task)
"""
patterns = []
# Track error frequency across sessions
error_counts: Dict[str, List[dict]] = defaultdict(list)
tool_latencies: Dict[str, List[float]] = defaultdict(list)
retry_counts: Dict[str, int] = defaultdict(int)
manual_sequences: List[dict] = []
for session in sessions:
session_id = session.get("session_id", "unknown")
repo = session.get("repo", "global")
messages = session.get("messages", [])
errors = session.get("errors", [])
tool_calls = session.get("tool_calls", [])
duration_min = session.get("duration_minutes", 0)
# 1. Repeated errors
for err in errors:
err_key = _normalize_error(err.get("message", ""))
if err_key:
error_counts[err_key].append({
"session_id": session_id,
"repo": repo,
})
# 2. Tool latency tracking
for tc in tool_calls:
tool_name = tc.get("tool", "unknown")
latency = tc.get("latency_ms", 0)
if latency > 0:
tool_latencies[tool_name].append(latency)
# 3. Failed retries (same tool called 3+ times in sequence)
prev_tool = None
streak = 0
for tc in tool_calls:
tool_name = tc.get("tool", "unknown")
if tool_name == prev_tool:
streak += 1
else:
if streak >= 3:
retry_counts[prev_tool] += 1
streak = 1
prev_tool = tool_name
if streak >= 3:
retry_counts[prev_tool] += 1
# 4. Manual processes (10+ sequential tool calls with no automation)
if len(tool_calls) > 10:
tool_sequence = [tc.get("tool") for tc in tool_calls]
unique_tools = set(tool_sequence)
if len(unique_tools) <= 3 and len(tool_calls) > 10:
manual_sequences.append({
"session_id": session_id,
"repo": repo,
"tool_count": len(tool_calls),
"unique_tools": list(unique_tools),
"duration_min": duration_min,
})
# Generate patterns from collected data
# Repeated errors (appearing in 3+ sessions)
for err_key, occurrences in error_counts.items():
if len(occurrences) >= 3:
repos = list(set(o["repo"] for o in occurrences))
sessions_list = [o["session_id"] for o in occurrences[:5]]
# Estimate time wasted: 5 min per error occurrence
hours = len(occurrences) * 5 / 60
patterns.append(WastePattern(
pattern_type="repeated_error",
description=f"Error: {err_key[:100]}",
occurrences=len(occurrences),
total_time_hours=round(hours, 1),
affected_repos=repos,
example_sessions=sessions_list,
))
# Slow tools (avg latency > 5000ms across 5+ calls)
for tool, latencies in tool_latencies.items():
if len(latencies) >= 5:
avg_ms = sum(latencies) / len(latencies)
if avg_ms > 5000:
hours = sum(latencies) / 1000 / 3600
patterns.append(WastePattern(
pattern_type="slow_tool",
description=f"Tool '{tool}' averages {avg_ms:.0f}ms latency",
occurrences=len(latencies),
total_time_hours=round(hours, 1),
affected_repos=["global"],
example_sessions=[],
))
# Failed retries
for tool, count in retry_counts.items():
if count >= 2:
hours = count * 10 / 60 # ~10 min per failed retry sequence
patterns.append(WastePattern(
pattern_type="failed_retry",
description=f"Tool '{tool}' had {count} retry sequences (3+ consecutive calls)",
occurrences=count,
total_time_hours=round(hours, 1),
affected_repos=["global"],
example_sessions=[],
))
# Manual processes
for seq in manual_sequences:
hours = seq["duration_min"] / 60
patterns.append(WastePattern(
pattern_type="manual_process",
description=f"Session {seq['session_id'][:12]}: {seq['tool_count']} tool calls with only {len(seq['unique_tools'])} unique tools",
occurrences=1,
total_time_hours=round(hours, 1),
affected_repos=[seq["repo"]],
example_sessions=[seq["session_id"]],
))
return sorted(patterns, key=lambda p: p.total_time_hours, reverse=True)
def _normalize_error(message: str) -> str:
"""Normalize error message to a common key."""
if not message:
return ""
msg = message.lower().strip()
# Remove variable parts (paths, IDs, timestamps)
import re
msg = re.sub(r'/\S+', '/PATH', msg)
msg = re.sub(r'\b[0-9a-f]{8,}\b', 'HASH', msg)
msg = re.sub(r'\d{4}-\d{2}-\d{2}[tT]\d{2}:\d{2}', 'TIME', msg)
return msg[:150]
# ── Proposal generation ───────────────────────────────────────
def generate_proposals(
patterns: List[WastePattern],
hourly_rate: float = 50.0,
implementation_overhead: float = 1.5,
) -> List[ImprovementProposal]:
"""
Generate improvement proposals from waste patterns.
Args:
patterns: Detected waste patterns
hourly_rate: Developer hourly rate for ROI calculation
implementation_overhead: Multiplier for implementation time estimate
"""
proposals = []
# Group patterns by type
by_type: Dict[str, List[WastePattern]] = defaultdict(list)
for p in patterns:
by_type[p.pattern_type].append(p)
# 1. Repeated errors → Create fix/skill
for p in by_type.get("repeated_error", []):
weekly_hours = p.total_time_hours / 4 # monthly → weekly
impl_hours = max(1.0, p.occurrences * 0.25) * implementation_overhead
roi_weeks = impl_hours / weekly_hours if weekly_hours > 0 else float('inf')
proposals.append(ImprovementProposal(
title=f"Fix repeated error: {p.description[:60]}",
problem=f"This error occurred {p.occurrences} times across {len(p.affected_repos)} repos, wasting ~{p.total_time_hours:.1f} hours.",
proposed_solution="Root-cause the error and create a permanent fix or mitigation skill.",
estimated_weekly_hours_saved=round(weekly_hours, 1),
estimated_monthly_hours_saved=round(p.total_time_hours, 1),
implementation_hours=round(impl_hours, 1),
roi_weeks=round(roi_weeks, 1),
priority=_priority_from_roi(roi_weeks),
affected_area="reliability",
supporting_evidence=[f"{p.occurrences} occurrences in sessions: {', '.join(p.example_sessions[:3])}"],
))
# 2. Slow tools → Optimize or replace
for p in by_type.get("slow_tool", []):
weekly_hours = p.total_time_hours / 4
impl_hours = 3.0 * implementation_overhead # optimization task
roi_weeks = impl_hours / weekly_hours if weekly_hours > 0 else float('inf')
proposals.append(ImprovementProposal(
title=f"Optimize slow tool: {p.description[:60]}",
problem=f"Tool has {p.occurrences} calls with high latency, wasting ~{p.total_time_hours:.1f} hours total.",
proposed_solution="Profile the tool, add caching, or replace with a faster alternative.",
estimated_weekly_hours_saved=round(weekly_hours, 1),
estimated_monthly_hours_saved=round(p.total_time_hours, 1),
implementation_hours=round(impl_hours, 1),
roi_weeks=round(roi_weeks, 1),
priority=_priority_from_roi(roi_weeks),
affected_area="performance",
supporting_evidence=[f"{p.occurrences} slow calls detected"],
))
# 3. Failed retries → Add retry logic or validation
for p in by_type.get("failed_retry", []):
weekly_hours = p.total_time_hours / 4
impl_hours = 2.0 * implementation_overhead
roi_weeks = impl_hours / weekly_hours if weekly_hours > 0 else float('inf')
proposals.append(ImprovementProposal(
title=f"Reduce retries for tool '{p.description[:50]}'",
problem=f"Tool had {p.occurrences} retry sequences, wasting ~{p.total_time_hours:.1f} hours.",
proposed_solution="Add input validation, pre-flight checks, or automatic retry with backoff.",
estimated_weekly_hours_saved=round(weekly_hours, 1),
estimated_monthly_hours_saved=round(p.total_time_hours, 1),
implementation_hours=round(impl_hours, 1),
roi_weeks=round(roi_weeks, 1),
priority=_priority_from_roi(roi_weeks),
affected_area="reliability",
supporting_evidence=[f"{p.occurrences} retry sequences detected"],
))
# 4. Manual processes → Automate
total_manual_hours = sum(p.total_time_hours for p in by_type.get("manual_process", []))
manual_patterns = by_type.get("manual_process", [])
if manual_patterns:
weekly_hours = total_manual_hours / 4
impl_hours = len(manual_patterns) * 2.0 * implementation_overhead
roi_weeks = impl_hours / weekly_hours if weekly_hours > 0 else float('inf')
proposals.append(ImprovementProposal(
title=f"Automate {len(manual_patterns)} manual processes",
problem=f"{len(manual_patterns)} sessions had long manual tool sequences, wasting ~{total_manual_hours:.1f} hours.",
proposed_solution="Create composite skills or scripts that combine the repeated tool sequences into single operations.",
estimated_weekly_hours_saved=round(weekly_hours, 1),
estimated_monthly_hours_saved=round(total_manual_hours, 1),
implementation_hours=round(impl_hours, 1),
roi_weeks=round(roi_weeks, 1),
priority=_priority_from_roi(roi_weeks),
affected_area="automation",
supporting_evidence=[f"{len(manual_patterns)} manual sessions detected"],
))
return sorted(proposals, key=lambda p: p.estimated_monthly_hours_saved, reverse=True)
def _priority_from_roi(roi_weeks: float) -> str:
"""Determine priority from ROI break-even time."""
if roi_weeks <= 1:
return "critical"
elif roi_weeks <= 4:
return "high"
elif roi_weeks <= 12:
return "medium"
return "low"
# ── Output formatting ─────────────────────────────────────────
def format_proposals_markdown(
proposals: List[ImprovementProposal],
patterns: List[WastePattern],
generated_at: str,
) -> str:
"""Format proposals as a markdown document."""
lines = [
"# Improvement Proposals",
"",
f"Generated: {generated_at}",
f"Based on analysis of {sum(p.occurrences for p in patterns)} waste events across {len(set(r for p in patterns for r in p.affected_repos))} repos.",
"",
"---",
"",
"## Summary",
"",
f"| Metric | Value |",
f"|--------|-------|",
f"| Total proposals | {len(proposals)} |",
f"| Critical priority | {sum(1 for p in proposals if p.priority == 'critical')} |",
f"| Total monthly hours wasted | {sum(p.estimated_monthly_hours_saved for p in proposals):.1f}h |",
f"| Total weekly hours recoverable | {sum(p.estimated_weekly_hours_saved for p in proposals):.1f}h |",
f"| Implementation investment | {sum(p.implementation_hours for p in proposals):.1f}h |",
"",
"---",
"",
]
for i, prop in enumerate(proposals, 1):
lines.extend([
f"## {i}. {prop.title}",
"",
f"**Priority:** {prop.priority.upper()} ",
f"**Area:** {prop.affected_area} ",
f"**ROI break-even:** {prop.roi_weeks:.1f} weeks",
"",
"### Problem",
"",
prop.problem,
"",
"### Proposed Solution",
"",
prop.proposed_solution,
"",
"### ROI Estimate",
"",
f"- Weekly hours saved: **{prop.estimated_weekly_hours_saved}h**",
f"- Monthly hours saved: **{prop.estimated_monthly_hours_saved}h**",
f"- Implementation time: **{prop.implementation_hours}h**",
f"- Break-even: **{prop.roi_weeks:.1f} weeks**",
"",
])
if prop.supporting_evidence:
lines.extend([
"### Evidence",
"",
])
for ev in prop.supporting_evidence:
lines.append(f"- {ev}")
lines.append("")
lines.extend(["---", ""])
# Waste pattern appendix
lines.extend([
"## Appendix: Detected Waste Patterns",
"",
"| Type | Description | Occurrences | Hours |",
"|------|-------------|-------------|-------|",
])
for p in patterns[:20]:
lines.append(
f"| {p.pattern_type} | {p.description[:60]} | {p.occurrences} | {p.total_time_hours}h |"
)
lines.append("")
return "\n".join(lines)
def format_proposals_json(proposals: List[ImprovementProposal]) -> str:
"""Format proposals as JSON."""
return json.dumps(
[asdict(p) for p in proposals],
indent=2,
default=str,
)
# ── Main ──────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="Generate improvement proposals from session analytics")
parser.add_argument("--input", required=True, help="Path to session analytics JSON file")
parser.add_argument("--output", help="Output file path (default: stdout)")
parser.add_argument("--format", choices=["markdown", "json"], default="markdown", help="Output format")
parser.add_argument("--hourly-rate", type=float, default=50.0, help="Developer hourly rate for ROI calc")
parser.add_argument("--threshold", type=float, default=2.0, help="Min monthly hours to include proposal")
args = parser.parse_args()
with open(args.input) as f:
data = json.load(f)
sessions = data if isinstance(data, list) else data.get("sessions", [])
if not sessions:
print("No sessions found in input file.", file=sys.stderr)
sys.exit(1)
# Analyze
patterns = analyze_sessions(sessions)
proposals = generate_proposals(patterns, hourly_rate=args.hourly_rate)
# Filter by threshold
proposals = [p for p in proposals if p.estimated_monthly_hours_saved >= args.threshold]
generated_at = datetime.now(timezone.utc).isoformat()
if args.format == "markdown":
output = format_proposals_markdown(proposals, patterns, generated_at)
else:
output = format_proposals_json(proposals)
if args.output:
with open(args.output, "w") as f:
f.write(output)
print(f"Wrote {len(proposals)} proposals to {args.output}", file=sys.stderr)
else:
print(output)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""
Knowledge Store Staleness Detector — Detect stale knowledge entries by comparing source file hashes.
Usage:
python3 scripts/knowledge_staleness_check.py --index knowledge/index.json
python3 scripts/knowledge_staleness_check.py --index knowledge/index.json --json
python3 scripts/knowledge_staleness_check.py --index knowledge/index.json --fix
"""
import argparse
import hashlib
import json
import os
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, List, Any, Optional
def compute_file_hash(filepath: str) -> Optional[str]:
"""Compute SHA-256 hash of a file. Returns None if file doesn't exist."""
try:
with open(filepath, "rb") as f:
return "sha256:" + hashlib.sha256(f.read()).hexdigest()
except (FileNotFoundError, IsADirectoryError, PermissionError):
return None
def check_staleness(index_path: str, repo_root: str = ".") -> List[Dict[str, Any]]:
"""Check all entries in knowledge index for staleness.
Returns list of entries with staleness info:
- status: "fresh" | "stale" | "missing_source" | "no_hash"
- current_hash: computed hash (if source exists)
- stored_hash: hash from index
"""
with open(index_path) as f:
data = json.load(f)
facts = data.get("facts", [])
results = []
for entry in facts:
source_file = entry.get("source_file")
stored_hash = entry.get("source_hash")
if not source_file:
results.append({**entry, "status": "no_source", "current_hash": None})
continue
full_path = os.path.join(repo_root, source_file)
current_hash = compute_file_hash(full_path)
if current_hash is None:
results.append({**entry, "status": "missing_source", "current_hash": None})
elif not stored_hash:
results.append({**entry, "status": "no_hash", "current_hash": current_hash})
elif current_hash != stored_hash:
results.append({**entry, "status": "stale", "current_hash": current_hash})
else:
results.append({**entry, "status": "fresh", "current_hash": current_hash})
return results
def fix_hashes(index_path: str, repo_root: str = ".") -> int:
"""Add hashes to entries missing them. Returns count of fixed entries."""
with open(index_path) as f:
data = json.load(f)
fixed = 0
for entry in data.get("facts", []):
if entry.get("source_hash"):
continue
source_file = entry.get("source_file")
if not source_file:
continue
full_path = os.path.join(repo_root, source_file)
h = compute_file_hash(full_path)
if h:
entry["source_hash"] = h
fixed += 1
with open(index_path, "w") as f:
json.dump(data, f, indent=2)
return fixed
def main():
parser = argparse.ArgumentParser(description="Check knowledge store staleness")
parser.add_argument("--index", required=True, help="Path to knowledge/index.json")
parser.add_argument("--repo", default=".", help="Repo root for source file resolution")
parser.add_argument("--json", action="store_true", help="Output as JSON")
parser.add_argument("--fix", action="store_true", help="Add hashes to entries missing them")
args = parser.parse_args()
if args.fix:
fixed = fix_hashes(args.index, args.repo)
print(f"Fixed {fixed} entries with missing hashes.")
return
results = check_staleness(args.index, args.repo)
if args.json:
print(json.dumps(results, indent=2))
else:
stale = [r for r in results if r["status"] != "fresh"]
fresh = [r for r in results if r["status"] == "fresh"]
print(f"Knowledge Store Staleness Check")
print(f" Total entries: {len(results)}")
print(f" Fresh: {len(fresh)}")
print(f" Stale/Issues: {len(stale)}")
print()
if stale:
print("Issues found:")
for r in stale:
status = r["status"]
fact = r.get("fact", "?")[:60]
source = r.get("source_file", "?")
print(f" [{status}] {source}: {fact}")
else:
print("All entries are fresh!")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,212 @@
#!/usr/bin/env python3
"""
Comprehensive test script for knowledge extraction prompt.
Validates prompt structure, requirements, and consistency.
"""
import json
import re
from pathlib import Path
def test_prompt_structure():
"""Test that the prompt has the required structure."""
prompt_path = Path("templates/harvest-prompt.md")
if not prompt_path.exists():
return False, "harvest-prompt.md not found"
content = prompt_path.read_text()
# Check for required sections
required_sections = [
"System Prompt",
"Instructions",
"Categories",
"Output Format",
"Confidence Scoring",
"Constraints",
"Example"
]
for section in required_sections:
if section.lower() not in content.lower():
return False, f"Missing required section: {section}"
# Check for required categories
required_categories = ["fact", "pitfall", "pattern", "tool-quirk", "question"]
for category in required_categories:
if category not in content:
return False, f"Missing required category: {category}"
# Check for required output fields
required_fields = ["fact", "category", "repo", "confidence"]
for field in required_fields:
if field not in content:
return False, f"Missing required output field: {field}"
# Check prompt size (should be ~1k tokens, roughly 4k chars)
if len(content) > 5000:
return False, f"Prompt too large: {len(content)} chars (max ~5000)"
if len(content) < 1000:
return False, f"Prompt too small: {len(content)} chars (min ~1000)"
return True, "Prompt structure is valid"
def test_confidence_scoring():
"""Test that confidence scoring is properly defined."""
prompt_path = Path("templates/harvest-prompt.md")
content = prompt_path.read_text()
# Check for confidence scale definitions
confidence_levels = [
("0.9-1.0", "explicitly stated"),
("0.7-0.8", "clearly implied"),
("0.5-0.6", "suggested"),
("0.3-0.4", "inferred"),
("0.1-0.2", "speculative")
]
for level, description in confidence_levels:
if level not in content:
return False, f"Missing confidence level: {level}"
if description.lower() not in content.lower():
return False, f"Missing confidence description: {description}"
return True, "Confidence scoring is properly defined"
def test_example_quality():
"""Test that examples are clear and complete."""
prompt_path = Path("templates/harvest-prompt.md")
content = prompt_path.read_text()
# Check for example input/output
if "example" not in content.lower():
return False, "No examples provided"
# Check that example includes all categories
example_section = content[content.lower().find("example"):]
# Look for JSON example
json_match = re.search(r'\{[\s\S]*"knowledge"[\s\S]*\}', example_section)
if not json_match:
return False, "No JSON example found"
example_json = json_match.group(0)
# Check for all categories in example
for category in ["fact", "pitfall", "pattern", "tool-quirk", "question"]:
if category not in example_json:
return False, f"Example missing category: {category}"
return True, "Examples are clear and complete"
def test_constraint_coverage():
"""Test that constraints cover all requirements."""
prompt_path = Path("templates/harvest-prompt.md")
content = prompt_path.read_text()
required_constraints = [
"No hallucination",
"only extract",
"explicitly",
"partial",
"failed sessions",
"1k tokens"
]
for constraint in required_constraints:
if constraint.lower() not in content.lower():
return False, f"Missing constraint: {constraint}"
return True, "Constraints cover all requirements"
def test_test_sessions():
"""Test that test sessions exist and are valid."""
test_sessions_dir = Path("test_sessions")
if not test_sessions_dir.exists():
return False, "test_sessions directory not found"
session_files = list(test_sessions_dir.glob("*.jsonl"))
if len(session_files) < 5:
return False, f"Only {len(session_files)} test sessions found, need 5"
# Check each session file
for session_file in session_files:
content = session_file.read_text()
lines = content.strip().split("\n")
# Check that each line is valid JSON
for i, line in enumerate(lines, 1):
try:
json.loads(line)
except json.JSONDecodeError as e:
return False, f"Invalid JSON in {session_file.name}, line {i}: {e}"
return True, f"Found {len(session_files)} valid test sessions"
def run_all_tests():
"""Run all tests and return results."""
tests = [
("Prompt Structure", test_prompt_structure),
("Confidence Scoring", test_confidence_scoring),
("Example Quality", test_example_quality),
("Constraint Coverage", test_constraint_coverage),
("Test Sessions", test_test_sessions)
]
results = []
all_passed = True
for test_name, test_func in tests:
try:
passed, message = test_func()
results.append({
"test": test_name,
"passed": passed,
"message": message
})
if not passed:
all_passed = False
except Exception as e:
results.append({
"test": test_name,
"passed": False,
"message": f"Error: {str(e)}"
})
all_passed = False
# Print results
print("=" * 60)
print("HARVEST PROMPT TEST RESULTS")
print("=" * 60)
for result in results:
status = "✓ PASS" if result["passed"] else "✗ FAIL"
print(f"{status}: {result['test']}")
print(f" {result['message']}")
print()
print("=" * 60)
if all_passed:
print("ALL TESTS PASSED!")
else:
print("SOME TESTS FAILED!")
print("=" * 60)
return all_passed, results
if __name__ == "__main__":
all_passed, results = run_all_tests()
# Save results to file
with open("test_results.json", "w") as f:
json.dump({
"all_passed": all_passed,
"results": results,
"timestamp": "2026-04-14T19:05:00Z"
}, f, indent=2)
print(f"Results saved to test_results.json")
# Exit with appropriate code
exit(0 if all_passed else 1)

View File

@@ -0,0 +1,282 @@
#!/usr/bin/env python3
"""Tests for scripts/improvement_proposals.py — 15 tests."""
import json
import os
import sys
import tempfile
sys.path.insert(0, os.path.dirname(__file__) or ".")
import importlib.util
spec = importlib.util.spec_from_file_location(
"ip", os.path.join(os.path.dirname(__file__) or ".", "improvement_proposals.py")
)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
analyze_sessions = mod.analyze_sessions
generate_proposals = mod.generate_proposals
format_proposals_markdown = mod.format_proposals_markdown
format_proposals_json = mod.format_proposals_json
_normalize_error = mod._normalize_error
# ── Helper to build test sessions ─────────────────────────────
def _make_session(session_id, repo="test-repo", errors=None, tool_calls=None, duration=30):
return {
"session_id": session_id,
"repo": repo,
"errors": [{"message": e} for e in (errors or [])],
"tool_calls": tool_calls or [],
"duration_minutes": duration,
"messages": [],
}
def _make_tool_calls(repeats):
"""Create tool call list with repeated tools."""
calls = []
for tool, count in repeats:
for _ in range(count):
calls.append({"tool": tool, "latency_ms": 100})
return calls
# ── Tests ─────────────────────────────────────────────────────
def test_empty_sessions():
patterns = analyze_sessions([])
assert patterns == []
print("PASS: test_empty_sessions")
def test_no_patterns_on_clean_sessions():
sessions = [
_make_session("s1", tool_calls=[{"tool": "read_file", "latency_ms": 50}]),
_make_session("s2", tool_calls=[{"tool": "write_file", "latency_ms": 80}]),
]
patterns = analyze_sessions(sessions)
# No repeated errors, no slow tools, no retries
assert len(patterns) == 0
print("PASS: test_no_patterns_on_clean_sessions")
def test_repeated_error_detection():
"""Same error across 3+ sessions triggers pattern."""
sessions = [
_make_session(f"s{i}", errors=["ModuleNotFoundError: No module named bannerlord_trace"])
for i in range(4)
]
patterns = analyze_sessions(sessions)
repeated = [p for p in patterns if p.pattern_type == "repeated_error"]
assert len(repeated) == 1
assert repeated[0].occurrences == 4
print("PASS: test_repeated_error_detection")
def test_repeated_error_threshold():
"""2 occurrences should NOT trigger (threshold is 3)."""
sessions = [
_make_session("s1", errors=["TimeoutError: connection timed out"]),
_make_session("s2", errors=["TimeoutError: connection timed out"]),
]
patterns = analyze_sessions(sessions)
repeated = [p for p in patterns if p.pattern_type == "repeated_error"]
assert len(repeated) == 0
print("PASS: test_repeated_error_threshold")
def test_slow_tool_detection():
"""Tool with avg latency > 5000ms across 5+ calls."""
calls = [{"tool": "git_push", "latency_ms": 8000} for _ in range(10)]
sessions = [_make_session("s1", tool_calls=calls)]
patterns = analyze_sessions(sessions)
slow = [p for p in patterns if p.pattern_type == "slow_tool"]
assert len(slow) == 1
assert "git_push" in slow[0].description
print("PASS: test_slow_tool_detection")
def test_fast_tool_not_flagged():
"""Tool under 5000ms avg should not trigger."""
calls = [{"tool": "read_file", "latency_ms": 50} for _ in range(10)]
sessions = [_make_session("s1", tool_calls=calls)]
patterns = analyze_sessions(sessions)
slow = [p for p in patterns if p.pattern_type == "slow_tool"]
assert len(slow) == 0
print("PASS: test_fast_tool_not_flagged")
def test_failed_retry_detection():
"""3+ consecutive calls to same tool triggers retry pattern."""
calls = _make_tool_calls([("execute_code", 5)])
sessions = [_make_session("s1", tool_calls=calls)]
sessions.extend([
_make_session(f"s{i}", tool_calls=_make_tool_calls([("execute_code", 4)]))
for i in range(2, 5)
])
patterns = analyze_sessions(sessions)
retries = [p for p in patterns if p.pattern_type == "failed_retry"]
assert len(retries) >= 1
print("PASS: test_failed_retry_detection")
def test_manual_process_detection():
"""10+ tool calls with <= 3 unique tools."""
calls = _make_tool_calls([("terminal", 8), ("read_file", 5)])
sessions = [_make_session("s1", tool_calls=calls, duration=25)]
patterns = analyze_sessions(sessions)
manual = [p for p in patterns if p.pattern_type == "manual_process"]
assert len(manual) == 1
print("PASS: test_manual_process_detection")
def test_generate_proposals_from_patterns():
"""Proposals generated from waste patterns."""
sessions = [
_make_session(f"s{i}", errors=["Error: push timeout"])
for i in range(5)
]
patterns = analyze_sessions(sessions)
proposals = generate_proposals(patterns)
assert len(proposals) >= 1
assert proposals[0].estimated_monthly_hours_saved > 0
assert proposals[0].priority in ("critical", "high", "medium", "low")
print("PASS: test_generate_proposals_from_patterns")
def test_proposal_roi_positive():
"""ROI weeks should be a positive number for recoverable time."""
patterns = [mod.WastePattern(
pattern_type="repeated_error",
description="Test error",
occurrences=10,
total_time_hours=5.0,
affected_repos=["test"],
)]
proposals = generate_proposals(patterns)
assert len(proposals) == 1
assert proposals[0].roi_weeks > 0
assert proposals[0].roi_weeks < 100
print("PASS: test_proposal_roi_positive")
def test_proposals_sorted_by_impact():
"""Proposals should be sorted by monthly hours saved (descending)."""
sessions = [
_make_session("s1", errors=["Minor warning"] * 3, duration=5),
_make_session("s2", errors=["Critical failure: deploy crashed"] * 5, duration=60),
]
# Add more sessions to cross threshold
for i in range(3, 7):
sessions.append(_make_session(f"s{i}", errors=["Critical failure: deploy crashed"]))
patterns = analyze_sessions(sessions)
proposals = generate_proposals(patterns)
if len(proposals) >= 2:
for i in range(len(proposals) - 1):
assert proposals[i].estimated_monthly_hours_saved >= proposals[i + 1].estimated_monthly_hours_saved
print("PASS: test_proposals_sorted_by_impact")
def test_format_markdown():
"""Markdown output should contain expected sections."""
patterns = [mod.WastePattern(
pattern_type="repeated_error", description="Test", occurrences=5,
total_time_hours=2.5, affected_repos=["repo"],
)]
proposals = generate_proposals(patterns)
md = format_proposals_markdown(proposals, patterns, "2026-04-15T00:00:00Z")
assert "# Improvement Proposals" in md
assert "## Summary" in md
assert "### Problem" in md
assert "### ROI Estimate" in md
assert "## Appendix" in md
print("PASS: test_format_markdown")
def test_format_json():
"""JSON output should be valid and parseable."""
patterns = [mod.WastePattern(
pattern_type="slow_tool", description="Slow", occurrences=10,
total_time_hours=3.0, affected_repos=["global"],
)]
proposals = generate_proposals(patterns)
output = format_proposals_json(proposals)
parsed = json.loads(output)
assert isinstance(parsed, list)
assert len(parsed) == len(proposals)
assert "title" in parsed[0]
assert "roi_weeks" in parsed[0]
print("PASS: test_format_json")
def test_normalize_error():
"""Error normalization should remove paths and hashes."""
err1 = _normalize_error("Failed to clone /Users/apayne/repo with token abc123def456")
assert "/PATH" in err1
assert "HASH" in err1
assert "/Users/apayne" not in err1
err2 = _normalize_error("")
assert err2 == ""
err3 = _normalize_error("Simple error message")
assert "simple error" in err3
print("PASS: test_normalize_error")
def test_cli_integration():
"""End-to-end test: write input JSON, run script, check output."""
import subprocess
sessions = [
_make_session(f"s{i}", errors=["Connection refused: port 8080"])
for i in range(5)
]
with tempfile.TemporaryDirectory() as tmpdir:
input_path = os.path.join(tmpdir, "analytics.json")
output_path = os.path.join(tmpdir, "proposals.md")
with open(input_path, "w") as f:
json.dump({"sessions": sessions}, f)
script = os.path.join(os.path.dirname(__file__) or ".", "improvement_proposals.py")
result = subprocess.run(
[sys.executable, script, "--input", input_path, "--output", output_path],
capture_output=True, text=True, timeout=10,
)
assert result.returncode == 0, f"CLI failed: {result.stderr}"
assert os.path.exists(output_path)
with open(output_path) as f:
content = f.read()
assert "# Improvement Proposals" in content
print("PASS: test_cli_integration")
def run_all():
test_empty_sessions()
test_no_patterns_on_clean_sessions()
test_repeated_error_detection()
test_repeated_error_threshold()
test_slow_tool_detection()
test_fast_tool_not_flagged()
test_failed_retry_detection()
test_manual_process_detection()
test_generate_proposals_from_patterns()
test_proposal_roi_positive()
test_proposals_sorted_by_impact()
test_format_markdown()
test_format_json()
test_normalize_error()
test_cli_integration()
print("\nAll 15 tests passed!")
if __name__ == "__main__":
run_all()

View File

@@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""Tests for scripts/knowledge_staleness_check.py — 8 tests."""
import json
import os
import sys
import tempfile
sys.path.insert(0, os.path.dirname(__file__) or ".")
import importlib.util
spec = importlib.util.spec_from_file_location("ks", os.path.join(os.path.dirname(__file__) or ".", "knowledge_staleness_check.py"))
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
check_staleness = mod.check_staleness
fix_hashes = mod.fix_hashes
compute_file_hash = mod.compute_file_hash
def test_fresh_entry():
with tempfile.TemporaryDirectory() as tmpdir:
src = os.path.join(tmpdir, "source.py")
with open(src, "w") as f:
f.write("print('hello')")
h = compute_file_hash(src)
idx = os.path.join(tmpdir, "index.json")
with open(idx, "w") as f:
json.dump({"facts": [{"fact": "hello", "source_file": "source.py", "source_hash": h}]}, f)
results = check_staleness(idx, tmpdir)
assert results[0]["status"] == "fresh"
print("PASS: test_fresh_entry")
def test_stale_entry():
with tempfile.TemporaryDirectory() as tmpdir:
src = os.path.join(tmpdir, "source.py")
with open(src, "w") as f:
f.write("original content")
idx = os.path.join(tmpdir, "index.json")
with open(idx, "w") as f:
json.dump({"facts": [{"fact": "old", "source_file": "source.py", "source_hash": "sha256:wrong"}]}, f)
# Now change the source
with open(src, "w") as f:
f.write("modified content")
results = check_staleness(idx, tmpdir)
assert results[0]["status"] == "stale"
print("PASS: test_stale_entry")
def test_missing_source():
with tempfile.TemporaryDirectory() as tmpdir:
idx = os.path.join(tmpdir, "index.json")
with open(idx, "w") as f:
json.dump({"facts": [{"fact": "gone", "source_file": "nonexistent.py", "source_hash": "sha256:abc"}]}, f)
results = check_staleness(idx, tmpdir)
assert results[0]["status"] == "missing_source"
print("PASS: test_missing_source")
def test_no_hash():
with tempfile.TemporaryDirectory() as tmpdir:
src = os.path.join(tmpdir, "source.py")
with open(src, "w") as f:
f.write("content")
idx = os.path.join(tmpdir, "index.json")
with open(idx, "w") as f:
json.dump({"facts": [{"fact": "no hash", "source_file": "source.py"}]}, f)
results = check_staleness(idx, tmpdir)
assert results[0]["status"] == "no_hash"
assert results[0]["current_hash"].startswith("sha256:")
print("PASS: test_no_hash")
def test_no_source_field():
with tempfile.TemporaryDirectory() as tmpdir:
idx = os.path.join(tmpdir, "index.json")
with open(idx, "w") as f:
json.dump({"facts": [{"fact": "orphan"}]}, f)
results = check_staleness(idx, tmpdir)
assert results[0]["status"] == "no_source"
print("PASS: test_no_source_field")
def test_fix_hashes():
with tempfile.TemporaryDirectory() as tmpdir:
src = os.path.join(tmpdir, "source.py")
with open(src, "w") as f:
f.write("content for hashing")
idx = os.path.join(tmpdir, "index.json")
with open(idx, "w") as f:
json.dump({"facts": [{"fact": "needs hash", "source_file": "source.py"}]}, f)
fixed = fix_hashes(idx, tmpdir)
assert fixed == 1
# Verify hash was added
with open(idx) as f:
data = json.load(f)
assert data["facts"][0]["source_hash"].startswith("sha256:")
print("PASS: test_fix_hashes")
def test_empty_index():
with tempfile.TemporaryDirectory() as tmpdir:
idx = os.path.join(tmpdir, "index.json")
with open(idx, "w") as f:
json.dump({"facts": []}, f)
results = check_staleness(idx, tmpdir)
assert results == []
print("PASS: test_empty_index")
def test_compute_hash_nonexistent():
h = compute_file_hash("/nonexistent/path/file.py")
assert h is None
print("PASS: test_compute_hash_nonexistent")
def run_all():
test_fresh_entry()
test_stale_entry()
test_missing_source()
test_no_hash()
test_no_source_field()
test_fix_hashes()
test_empty_index()
test_compute_hash_nonexistent()
print("\nAll 8 tests passed!")
if __name__ == "__main__":
run_all()

View File

@@ -0,0 +1,18 @@
{"role": "user", "content": "Deploy the application to production"}
{"role": "assistant", "content": "I'll deploy the application to production.", "tool_calls": [{"function": "terminal", "arguments": {"command": "ssh deploy@prod-server 'cd /app && git pull && docker-compose up -d'"}}]}
{"role": "tool", "content": "ssh: connect to host prod-server port 22: Connection refused"}
{"role": "assistant", "content": "Connection refused. Let me check the server status.", "tool_calls": [{"function": "terminal", "arguments": {"command": "ping -c 3 prod-server"}}]}
{"role": "tool", "content": "PING prod-server (192.168.1.100): 56 data bytes\nRequest timeout for icmp_seq 0\nRequest timeout for icmp_seq 1\nRequest timeout for icmp_seq 2\n--- prod-server ping statistics ---\n3 packets transmitted, 0 packets received, 100.0% packet loss"}
{"role": "assistant", "content": "The server appears to be down. Let me check if there's an alternative deployment method."}
{"role": "user", "content": "Try using the VPS instead"}
{"role": "assistant", "content": "I'll try deploying to the VPS.", "tool_calls": [{"function": "terminal", "arguments": {"command": "ssh deploy@vps-server 'cd /app && git pull && docker-compose up -d'"}}]}
{"role": "tool", "content": "Permission denied (publickey)."}
{"role": "assistant", "content": "Permission denied. The SSH key might not be set up correctly. Let me check the SSH configuration.", "tool_calls": [{"function": "terminal", "arguments": {"command": "cat ~/.ssh/config"}}]}
{"role": "tool", "content": "Host vps-server\n HostName 167.99.126.228\n User deploy\n IdentityFile ~/.ssh/id_rsa_vps"}
{"role": "assistant", "content": "The SSH config looks correct. Let me check if the key exists.", "tool_calls": [{"function": "terminal", "arguments": {"command": "ls -la ~/.ssh/id_rsa_vps"}}]}
{"role": "tool", "content": "ls: /Users/user/.ssh/id_rsa_vps: No such file or directory"}
{"role": "assistant", "content": "The SSH key doesn't exist. This is a pitfall - we need to generate the SSH key first."}
{"role": "user", "content": "Generate a new SSH key for the VPS"}
{"role": "assistant", "content": "I'll generate a new SSH key.", "tool_calls": [{"function": "terminal", "arguments": {"command": "ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_vps -N ''"}}]}
{"role": "tool", "content": "Generating public/private rsa key pair.\nYour identification has been saved in /Users/user/.ssh/id_rsa_vps\nYour public key has been saved in /Users/user/.ssh/id_rsa_vps.pub\nThe key fingerprint is:\nSHA256:abc123... user@machine"}
{"role": "assistant", "content": "SSH key generated. Now I need to add it to the VPS."}

View File

@@ -0,0 +1,14 @@
{"role": "user", "content": "Check the Gitea API for open issues"}
{"role": "assistant", "content": "I'll check the Gitea API for open issues.", "tool_calls": [{"function": "terminal", "arguments": {"command": "curl -H 'Authorization: token $GITEA_TOKEN' https://forge.alexanderwhitestone.com/api/v1/repos/Timmy_Foundation/compounding-intelligence/issues?state=open"}}]}
{"role": "tool", "content": "curl: (6) Could not resolve host: forge.alexanderwhitestone.com"}
{"role": "assistant", "content": "DNS resolution failed. Let me check the network connectivity.", "tool_calls": [{"function": "terminal", "arguments": {"command": "nslookup forge.alexanderwhitestone.com"}}]}
{"role": "tool", "content": "Server: 8.8.8.8\nAddress: 8.8.8.8#53\n\n** server can't find forge.alexanderwhitestone.com: NXDOMAIN"}
{"role": "assistant", "content": "The domain doesn't resolve. This might be a DNS issue. Let me try using the IP address directly.", "tool_calls": [{"function": "terminal", "arguments": {"command": "curl -H 'Authorization: token $GITEA_TOKEN' https://167.99.126.228/api/v1/repos/Timmy_Foundation/compounding-intelligence/issues?state=open"}}]}
{"role": "tool", "content": "curl: (60) SSL certificate problem: self signed certificate"}
{"role": "assistant", "content": "SSL certificate issue. The server is using a self-signed certificate. Let me try with -k flag to skip SSL verification.", "tool_calls": [{"function": "terminal", "arguments": {"command": "curl -k -H 'Authorization: token $GITEA_TOKEN' https://167.99.126.228/api/v1/repos/Timmy_Foundation/compounding-intelligence/issues?state=open"}}]}
{"role": "tool", "content": "[]"}
{"role": "assistant", "content": "No open issues found. The API returned an empty array."}
{"role": "user", "content": "Check if there are any pull requests"}
{"role": "assistant", "content": "Let me check for pull requests.", "tool_calls": [{"function": "terminal", "arguments": {"command": "curl -k -H 'Authorization: token $GITEA_TOKEN' https://167.99.126.228/api/v1/repos/Timmy_Foundation/compounding-intelligence/pulls?state=open"}}]}
{"role": "tool", "content": "[{"id": 20, "title": "feat: add harvester.py + session_reader.py — session knowledge extractor (closes #8)", "state": "open", "user": {"login": "Rockachopa"}, "created_at": "2026-04-14T17:28:20Z"}]"}
{"role": "assistant", "content": "Found 1 open pull request: PR #20 - "feat: add harvester.py + session_reader.py — session knowledge extractor (closes #8)" by Rockachopa."}

View File

@@ -0,0 +1,10 @@
{"role": "user", "content": "How do I deploy a new version of the application?"}
{"role": "assistant", "content": "Here's the deployment process:\n1. Run tests: pytest\n2. Build Docker image: docker build -t app:latest .\n3. Push to registry: docker push registry.example.com/app:latest\n4. Update production: ssh prod 'cd /app && docker-compose pull && docker-compose up -d'\n5. Verify: curl https://app.example.com/health"}
{"role": "user", "content": "What if tests fail?"}
{"role": "assistant", "content": "If tests fail:\n1. Check the error logs: pytest -v\n2. Fix the failing tests\n3. Re-run tests: pytest\n4. If still failing, check recent changes: git log --oneline -10\n5. Consider reverting: git revert HEAD"}
{"role": "user", "content": "What if the Docker build fails?"}
{"role": "assistant", "content": "If Docker build fails:\n1. Check Dockerfile syntax: docker build --no-cache -t app:latest .\n2. Check for missing dependencies: cat requirements.txt\n3. Check disk space: df -h\n4. Clean up Docker: docker system prune -f\n5. Try building with verbose output: docker build --progress=plain -t app:latest ."}
{"role": "user", "content": "What if the push to registry fails?"}
{"role": "assistant", "content": "If push to registry fails:\n1. Check registry credentials: docker login registry.example.com\n2. Check registry space: curl https://registry.example.com/v2/_catalog\n3. Check image size: docker images app:latest\n4. Try tagging with version: docker tag app:latest registry.example.com/app:v1.0.0\n5. Push with version tag: docker push registry.example.com/app:v1.0.0"}
{"role": "user", "content": "What if the production update fails?"}
{"role": "assistant", "content": "If production update fails:\n1. Check SSH connection: ssh prod 'echo connected'\n2. Check disk space on prod: ssh prod 'df -h'\n3. Check Docker status: ssh prod 'docker ps -a'\n4. Check logs: ssh prod 'docker-compose logs --tail=100'\n5. Rollback: ssh prod 'cd /app && git checkout HEAD~1 && docker-compose up -d'"}

View File

@@ -0,0 +1,12 @@
{"role": "user", "content": "I need to implement a new feature for the application"}
{"role": "assistant", "content": "I can help you implement a new feature. What feature would you like to add?"}
{"role": "user", "content": "I want to add user authentication"}
{"role": "assistant", "content": "User authentication is a good feature. I have a few questions:\n1. What authentication method do you prefer? (JWT, OAuth, Session-based)\n2. Do you need social login? (Google, GitHub, etc.)\n3. What user data do you need to store?\n4. Do you need role-based access control?\n5. What's your database? (PostgreSQL, MySQL, SQLite)"}
{"role": "user", "content": "I prefer JWT, no social login, just email and password, no roles, PostgreSQL"}
{"role": "assistant", "content": "Great choices. Here's the implementation plan:\n1. Create user model with email and password hash\n2. Create JWT token generation and validation\n3. Create login endpoint: POST /api/auth/login\n4. Create register endpoint: POST /api/auth/register\n5. Create middleware for protected routes\n6. Add password hashing with bcrypt\n7. Add rate limiting for login attempts\n8. Add password reset functionality\n9. Add email verification\n10. Test all endpoints"}
{"role": "user", "content": "What about password reset?"}
{"role": "assistant", "content": "For password reset:\n1. Create reset token table\n2. Create forgot password endpoint: POST /api/auth/forgot-password\n3. Send reset email with token\n4. Create reset password endpoint: POST /api/auth/reset-password\n5. Validate reset token and expiration\n6. Update password and delete token\n7. Send confirmation email"}
{"role": "user", "content": "What about email verification?"}
{"role": "assistant", "content": "For email verification:\n1. Add email_verified field to user model\n2. Create verification token table\n3. Send verification email on registration\n4. Create verify email endpoint: GET /api/auth/verify-email?token=...\n5. Validate token and update email_verified\n6. Add middleware to check email_verified for certain routes\n7. Add resend verification email endpoint"}
{"role": "user", "content": "What about rate limiting?"}
{"role": "assistant", "content": "For rate limiting:\n1. Use Redis for rate limit storage\n2. Create rate limit middleware\n3. Set limits: 5 login attempts per minute, 3 password resets per hour\n4. Return 429 Too Many Requests when limit exceeded\n5. Add X-RateLimit headers to responses\n6. Log rate limit violations\n7. Add whitelist for trusted IPs"}

View File

@@ -0,0 +1,13 @@
{"role": "user", "content": "Clone the repo at forge.alexanderwhitestone.com/Timmy_Foundation/compounding-intelligence"}
{"role": "assistant", "content": "I'll clone the repository for you.", "tool_calls": [{"function": "terminal", "arguments": {"command": "git clone https://forge.alexanderwhitestone.com/Timmy_Foundation/compounding-intelligence.git"}}]}
{"role": "tool", "content": "Cloning into 'compounding-intelligence'...\nremote: Enumerating objects: 15, done.\nremote: Counting objects: 100% (15/15), done.\nremote: Compressing objects: 100% (12/12), done.\nremote: Total 15 (delta 2), reused 0 (delta 0), pack-reused 0\nReceiving objects: 100% (15/15), done.\nResolving deltas: 100% (2/2), done."}
{"role": "assistant", "content": "Repository cloned successfully. Let me check the structure.", "tool_calls": [{"function": "terminal", "arguments": {"command": "ls -la compounding-intelligence/"}}]}
{"role": "tool", "content": "total 8\ndrwxr-xr-x 5 user staff 160 Apr 14 17:21 .\ndrwxr-xr-x 12 user staff 384 Apr 14 17:21 ..\ndrwxr-xr-x 13 user staff 416 Apr 14 17:21 .git\n-rw-r--r-- 1 user staff 1234 Apr 14 17:21 README.md\ndrwxr-xr-x 3 user staff 96 Apr 14 17:21 knowledge\n-rw-r--r-- 1 user staff 456 Apr 14 17:21 metrics\ndrwxr-xr-x 4 user staff 128 Apr 14 17:21 scripts\ndrwxr-xr-x 3 user staff 96 Apr 14 17:21 templates"}
{"role": "user", "content": "What files are in the templates directory?"}
{"role": "assistant", "content": "Let me check the templates directory.", "tool_calls": [{"function": "terminal", "arguments": {"command": "ls -la compounding-intelligence/templates/"}}]}
{"role": "tool", "content": "total 0\ndrwxr-xr-x 3 user staff 96 Apr 14 17:21 .\ndrwxr-xr-x 5 user staff 160 Apr 14 17:21 ..\n-rw-r--r-- 1 user staff 0 Apr 14 17:21 .gitkeep"}
{"role": "assistant", "content": "The templates directory currently only contains a .gitkeep file. No templates have been added yet."}
{"role": "user", "content": "Create a harvest-prompt.md template in the templates directory."}
{"role": "assistant", "content": "I'll create the harvest-prompt.md template for you.", "tool_calls": [{"function": "terminal", "arguments": {"command": "cat > compounding-intelligence/templates/harvest-prompt.md << 'EOF'\n# Knowledge Extraction Prompt\n\nExtract knowledge from session transcripts.\nEOF"}}]}
{"role": "tool", "content": ""}
{"role": "assistant", "content": "Template created successfully. The harvest-prompt.md file is now in the templates directory."}