devcoach 0.3.8__tar.gz → 0.3.9__tar.gz
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.
- devcoach-0.3.9/.github/scripts/export_backup.py +138 -0
- devcoach-0.3.9/.github/scripts/fixtures/devcoach-backup.zip +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/.github/scripts/take_screenshots.py +18 -19
- {devcoach-0.3.8 → devcoach-0.3.9}/.github/workflows/ci.yml +1 -1
- {devcoach-0.3.8 → devcoach-0.3.9}/.github/workflows/ruff-autofix.yml +9 -14
- {devcoach-0.3.8 → devcoach-0.3.9}/PKG-INFO +1 -1
- devcoach-0.3.9/docs/how-it-works.md +112 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/docs/index.md +32 -6
- devcoach-0.3.9/docs/screenshots/knowledge-map-dark.png +0 -0
- devcoach-0.3.9/docs/screenshots/knowledge-map-light.png +0 -0
- devcoach-0.3.9/docs/screenshots/lesson-ci-cd-pipeline-stages-dark.png +0 -0
- devcoach-0.3.9/docs/screenshots/lesson-ci-cd-pipeline-stages-light.png +0 -0
- devcoach-0.3.9/docs/screenshots/lesson-docker-layer-cache-dark.png +0 -0
- devcoach-0.3.9/docs/screenshots/lesson-docker-layer-cache-light.png +0 -0
- devcoach-0.3.9/docs/screenshots/lesson-git-interactive-rebase-dark.png +0 -0
- devcoach-0.3.9/docs/screenshots/lesson-git-interactive-rebase-light.png +0 -0
- devcoach-0.3.9/docs/screenshots/lesson-postgresql-explain-analyze-dark.png +0 -0
- devcoach-0.3.9/docs/screenshots/lesson-postgresql-explain-analyze-light.png +0 -0
- devcoach-0.3.9/docs/screenshots/lesson-redis-cache-stampede-dark.png +0 -0
- devcoach-0.3.9/docs/screenshots/lesson-redis-cache-stampede-light.png +0 -0
- devcoach-0.3.9/docs/screenshots/lessons-dark.png +0 -0
- devcoach-0.3.9/docs/screenshots/lessons-light.png +0 -0
- devcoach-0.3.9/docs/screenshots/settings-dark.png +0 -0
- devcoach-0.3.9/docs/screenshots/settings-light.png +0 -0
- devcoach-0.3.9/docs/web-ui.md +144 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/mkdocs.yml +7 -1
- {devcoach-0.3.8 → devcoach-0.3.9}/pyproject.toml +1 -1
- {devcoach-0.3.8 → devcoach-0.3.9}/sonar-project.properties +1 -1
- devcoach-0.3.9/src/devcoach/SKILL.md +490 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/cli/commands.py +15 -4
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/core/coach.py +9 -10
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/core/db.py +49 -20
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/core/git.py +9 -11
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/core/models.py +3 -43
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/mcp/server.py +178 -99
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/web/static/style.css +1 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/web/templates/lesson_detail.html +11 -5
- {devcoach-0.3.8 → devcoach-0.3.9}/tests/test_cli.py +1 -1
- {devcoach-0.3.8 → devcoach-0.3.9}/tests/test_mcp_server.py +125 -80
- {devcoach-0.3.8 → devcoach-0.3.9}/uv.lock +77 -77
- devcoach-0.3.8/.github/scripts/fixtures/devcoach-backup.zip +0 -0
- devcoach-0.3.8/docs/screenshots/knowledge-map-dark.png +0 -0
- devcoach-0.3.8/docs/screenshots/knowledge-map-light.png +0 -0
- devcoach-0.3.8/docs/screenshots/lessons-dark.png +0 -0
- devcoach-0.3.8/docs/screenshots/lessons-light.png +0 -0
- devcoach-0.3.8/docs/screenshots/settings-dark.png +0 -0
- devcoach-0.3.8/docs/screenshots/settings-light.png +0 -0
- devcoach-0.3.8/docs/web-ui.md +0 -88
- devcoach-0.3.8/src/devcoach/SKILL.md +0 -285
- {devcoach-0.3.8 → devcoach-0.3.9}/.github/dependabot.yml +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/.github/workflows/update-screenshots.yml +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/.gitignore +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/CLAUDE.md +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/LICENSE +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/NOTICE +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/README.md +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/SKILL.md +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/docs/PLAN.md +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/docs/cli.md +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/docs/configuration.md +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/docs/favicon.svg +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/docs/getting-started.md +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/docs/mcp-server.md +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/__init__.py +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/cli/__init__.py +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/core/__init__.py +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/core/detect.py +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/core/prompts.py +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/mcp/__init__.py +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/web/__init__.py +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/web/app.py +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/web/static/favicon.svg +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/web/static/relative-time.js +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/web/static/vendor/alpinejs.min.js +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/web/static/vendor/flatpickr-dark.min.css +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/web/static/vendor/flatpickr.min.css +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/web/static/vendor/flatpickr.min.js +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/web/static/vendor/highlight.min.js +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/web/static/vendor/hljs-dark.min.css +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/web/static/vendor/hljs-light.min.css +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/web/static/vendor/htmx.min.js +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/web/static/vendor/icons/bitbucket.svg +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/web/static/vendor/icons/github.svg +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/web/static/vendor/icons/gitlab.svg +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/web/static/vendor/icons/vscode.svg +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/web/static/vendor/marked.min.js +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/web/static/vendor/tailwind.js +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/web/templates/base.html +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/web/templates/lessons.html +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/web/templates/profile.html +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/src/devcoach/web/templates/settings.html +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/tests/__init__.py +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/tests/conftest.py +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/tests/test_cli_commands.py +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/tests/test_coach.py +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/tests/test_db_extra.py +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/tests/test_detect.py +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/tests/test_git.py +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/tests/test_prompts.py +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/tests/test_web.py +0 -0
- {devcoach-0.3.8 → devcoach-0.3.9}/tests/test_web_extra.py +0 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Anonymize a devcoach backup zip for safe public sharing.
|
|
2
|
+
|
|
3
|
+
Replacements applied to lessons.json:
|
|
4
|
+
- Absolute paths (/root/username/...) → /home/user/...
|
|
5
|
+
- Repository owner prefix (owner/repo) → developer/repo
|
|
6
|
+
- Commit hashes (full 40-char SHA-1) → random 40-char hex
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python .github/scripts/anonymize_backup.py <input.zip> [output.zip]
|
|
10
|
+
|
|
11
|
+
If output.zip is omitted the anonymized file is written next to the input
|
|
12
|
+
with "-anon" appended to the stem (e.g. devcoach-backup-anon.zip).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
import secrets
|
|
18
|
+
import sys
|
|
19
|
+
import zipfile
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
SHA1_RE = re.compile(r"\b[0-9a-f]{40}\b", re.IGNORECASE)
|
|
24
|
+
REPO_USER_RE = re.compile(r"^[^/\s]+/")
|
|
25
|
+
|
|
26
|
+
ANON_REPO_PREFIX = "developer/"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _random_sha1() -> str:
|
|
30
|
+
return secrets.token_hex(20)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _anonymize_path(path: str) -> str:
|
|
34
|
+
"""Replace username in any absolute path /root/username/... → /home/user/..."""
|
|
35
|
+
parts = path.split("/")
|
|
36
|
+
# Absolute path: ['', '<root>', '<username>', ...]
|
|
37
|
+
if len(parts) >= 3 and parts[0] == "" and parts[1] and parts[2]:
|
|
38
|
+
parts[1] = "home"
|
|
39
|
+
parts[2] = "user"
|
|
40
|
+
return "/".join(parts)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _has_personal_path(value: str) -> bool:
|
|
44
|
+
"""True if value contains an absolute path whose username is not 'user'."""
|
|
45
|
+
parts = value.split("/")
|
|
46
|
+
return (
|
|
47
|
+
len(parts) >= 3
|
|
48
|
+
and parts[0] == ""
|
|
49
|
+
and parts[1] != ""
|
|
50
|
+
and parts[2] not in ("", "user")
|
|
51
|
+
and "/" in value[1:] # at least one slash after the leading one
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def anonymize_lesson(lesson: dict, original_hashes: set[str]) -> dict:
|
|
56
|
+
out = dict(lesson)
|
|
57
|
+
|
|
58
|
+
if out.get("folder"):
|
|
59
|
+
out["folder"] = _anonymize_path(out["folder"])
|
|
60
|
+
|
|
61
|
+
if out.get("repository"):
|
|
62
|
+
out["repository"] = REPO_USER_RE.sub(ANON_REPO_PREFIX, out["repository"])
|
|
63
|
+
|
|
64
|
+
if out.get("commit_hash"):
|
|
65
|
+
original_hashes.add(out["commit_hash"].lower())
|
|
66
|
+
out["commit_hash"] = _random_sha1()
|
|
67
|
+
|
|
68
|
+
if out.get("task_context"):
|
|
69
|
+
ctx = out["task_context"]
|
|
70
|
+
|
|
71
|
+
def _replace_hash(m: re.Match) -> str:
|
|
72
|
+
original_hashes.add(m.group().lower())
|
|
73
|
+
return _random_sha1()
|
|
74
|
+
|
|
75
|
+
ctx = SHA1_RE.sub(_replace_hash, ctx)
|
|
76
|
+
tokens = ctx.split()
|
|
77
|
+
ctx = " ".join(_anonymize_path(t) if _has_personal_path(t) else t for t in tokens)
|
|
78
|
+
out["task_context"] = ctx
|
|
79
|
+
|
|
80
|
+
return out
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def anonymize_zip(src: Path, dst: Path) -> None:
|
|
84
|
+
original_hashes: set[str] = set()
|
|
85
|
+
|
|
86
|
+
with zipfile.ZipFile(src) as zin, zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as zout:
|
|
87
|
+
for name in zin.namelist():
|
|
88
|
+
if name == "learning-state.md":
|
|
89
|
+
continue # personal coaching narrative — excluded from anon export
|
|
90
|
+
|
|
91
|
+
raw = zin.read(name)
|
|
92
|
+
|
|
93
|
+
if name == "lessons.json":
|
|
94
|
+
lessons = json.loads(raw.decode())
|
|
95
|
+
lessons = [anonymize_lesson(l, original_hashes) for l in lessons]
|
|
96
|
+
raw = json.dumps(lessons, indent=2, ensure_ascii=False).encode()
|
|
97
|
+
|
|
98
|
+
zout.writestr(name, raw)
|
|
99
|
+
|
|
100
|
+
print(f"Anonymized backup written to: {dst}")
|
|
101
|
+
_verify(dst, original_hashes)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _verify(path: Path, original_hashes: set[str]) -> None:
|
|
105
|
+
with zipfile.ZipFile(path) as z:
|
|
106
|
+
lessons = json.loads(z.read("lessons.json").decode())
|
|
107
|
+
issues: list[str] = []
|
|
108
|
+
for lesson in lessons:
|
|
109
|
+
for field in ("folder", "repository", "commit_hash", "task_context"):
|
|
110
|
+
val = lesson.get(field) or ""
|
|
111
|
+
if _has_personal_path(val):
|
|
112
|
+
issues.append(f" {lesson['id']}.{field}: personal path still present")
|
|
113
|
+
for match in SHA1_RE.finditer(val):
|
|
114
|
+
if match.group().lower() in original_hashes:
|
|
115
|
+
issues.append(f" {lesson['id']}.{field}: original SHA-1 hash still present")
|
|
116
|
+
if issues:
|
|
117
|
+
print("Verification warnings:")
|
|
118
|
+
for w in issues:
|
|
119
|
+
print(w)
|
|
120
|
+
else:
|
|
121
|
+
print(f"Verification passed ({len(lessons)} lessons, no personal data detected).")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main() -> None:
|
|
125
|
+
if len(sys.argv) < 2:
|
|
126
|
+
sys.exit(f"Usage: python {sys.argv[0]} <input.zip> [output.zip]")
|
|
127
|
+
|
|
128
|
+
src = Path(sys.argv[1])
|
|
129
|
+
if not src.exists():
|
|
130
|
+
sys.exit(f"File not found: {src}")
|
|
131
|
+
|
|
132
|
+
dst = Path(sys.argv[2]) if len(sys.argv) > 2 else src.with_stem(src.stem + "-anon")
|
|
133
|
+
|
|
134
|
+
anonymize_zip(src, dst)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
main()
|
|
Binary file
|
|
@@ -27,6 +27,14 @@ PAGES = [
|
|
|
27
27
|
("settings", "/settings"),
|
|
28
28
|
]
|
|
29
29
|
|
|
30
|
+
LESSON_PAGES = [
|
|
31
|
+
("lesson-docker-layer-cache", "/lessons/lesson-docker-layer-cache-001"),
|
|
32
|
+
("lesson-postgresql-explain-analyze", "/lessons/lesson-postgresql-explain-analyze-001"),
|
|
33
|
+
("lesson-git-interactive-rebase", "/lessons/lesson-git-interactive-rebase-001"),
|
|
34
|
+
("lesson-ci-cd-pipeline-stages", "/lessons/lesson-ci-cd-pipeline-stages-001"),
|
|
35
|
+
("lesson-redis-cache-stampede", "/lessons/lesson-redis-cache-stampede-001"),
|
|
36
|
+
]
|
|
37
|
+
|
|
30
38
|
def restore_db() -> None:
|
|
31
39
|
result = subprocess.run(
|
|
32
40
|
["devcoach", "restore", str(BACKUP_ZIP)],
|
|
@@ -55,25 +63,16 @@ def take_screenshots(server_proc: subprocess.Popen) -> None:
|
|
|
55
63
|
with sync_playwright() as pw:
|
|
56
64
|
browser = pw.chromium.launch()
|
|
57
65
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
ctx = browser.new_context(viewport=VIEWPORT, color_scheme="dark")
|
|
69
|
-
page = ctx.new_page()
|
|
70
|
-
for name, path in PAGES:
|
|
71
|
-
page.goto(f"{BASE_URL}{path}")
|
|
72
|
-
page.wait_for_load_state("networkidle")
|
|
73
|
-
out = SCREENSHOTS_DIR / f"{name}-dark.png"
|
|
74
|
-
page.screenshot(path=str(out))
|
|
75
|
-
print(f" saved {out}")
|
|
76
|
-
ctx.close()
|
|
66
|
+
for scheme in ("light", "dark"):
|
|
67
|
+
ctx = browser.new_context(viewport=VIEWPORT, color_scheme=scheme)
|
|
68
|
+
page = ctx.new_page()
|
|
69
|
+
for name, path in PAGES + LESSON_PAGES:
|
|
70
|
+
page.goto(f"{BASE_URL}{path}")
|
|
71
|
+
page.wait_for_load_state("networkidle")
|
|
72
|
+
out = SCREENSHOTS_DIR / f"{name}-{scheme}.png"
|
|
73
|
+
page.screenshot(path=str(out))
|
|
74
|
+
print(f" saved {out}")
|
|
75
|
+
ctx.close()
|
|
77
76
|
|
|
78
77
|
browser.close()
|
|
79
78
|
|
|
@@ -202,7 +202,7 @@ jobs:
|
|
|
202
202
|
- run: uv sync --group dev
|
|
203
203
|
- name: Run tests with coverage
|
|
204
204
|
run: uv run pytest tests/ -v --tb=short --cov=src/devcoach --cov-report=xml
|
|
205
|
-
- uses: sonarsource/sonarqube-scan-action@
|
|
205
|
+
- uses: sonarsource/sonarqube-scan-action@v8
|
|
206
206
|
env:
|
|
207
207
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
208
208
|
|
|
@@ -7,7 +7,6 @@ on:
|
|
|
7
7
|
|
|
8
8
|
permissions:
|
|
9
9
|
contents: write
|
|
10
|
-
pull-requests: write
|
|
11
10
|
|
|
12
11
|
jobs:
|
|
13
12
|
autofix:
|
|
@@ -29,16 +28,12 @@ jobs:
|
|
|
29
28
|
- name: Run ruff format
|
|
30
29
|
run: uv run ruff format src/ tests/
|
|
31
30
|
|
|
32
|
-
- name:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
Review the diff, then merge or close.
|
|
42
|
-
branch: ruff/autofix
|
|
43
|
-
delete-branch: true
|
|
44
|
-
labels: style
|
|
31
|
+
- name: Commit and push if there are changes
|
|
32
|
+
run: |
|
|
33
|
+
git config user.name "github-actions[bot]"
|
|
34
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
35
|
+
git add -A
|
|
36
|
+
git diff --staged --quiet && echo "No changes." || (
|
|
37
|
+
git commit -m "style: apply ruff auto-fixes [skip ci]" &&
|
|
38
|
+
git push
|
|
39
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devcoach
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.9
|
|
4
4
|
Summary: A local MCP server that acts as a progressive technical coach for Claude Code and Claude Desktop
|
|
5
5
|
Project-URL: Homepage, https://github.com/UltimaPhoenix/dev-coach
|
|
6
6
|
Project-URL: Repository, https://github.com/UltimaPhoenix/dev-coach
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# How it works
|
|
2
|
+
|
|
3
|
+
devcoach is a silent technical coach that hooks into every Claude response.
|
|
4
|
+
The diagrams below show the three main flows: session startup, the coaching loop,
|
|
5
|
+
and how a lesson topic is selected.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Session startup
|
|
10
|
+
|
|
11
|
+
At the start of each Claude session devcoach checks whether the user is set up,
|
|
12
|
+
loads prior coaching context, and primes lesson selection before any task is done.
|
|
13
|
+
|
|
14
|
+
```mermaid
|
|
15
|
+
flowchart TD
|
|
16
|
+
A([Session starts]) --> B[Read devcoach://onboarding]
|
|
17
|
+
B --> C{needs_onboarding?}
|
|
18
|
+
C -- Yes --> D{Existing backup\nto restore?}
|
|
19
|
+
D -- Yes --> E[Restore backup\n→ mark onboarding done]
|
|
20
|
+
D -- No --> F[Detect stack\nautomatically or manually]
|
|
21
|
+
F --> G[Confirm topics + confidence\nPropose groups]
|
|
22
|
+
G --> H[complete_onboarding]
|
|
23
|
+
E & H --> I
|
|
24
|
+
C -- No --> I[Read ~/.devcoach/learning-state.md]
|
|
25
|
+
I --> J{Notebook\nnon-empty?}
|
|
26
|
+
J -- Yes --> K[Load patterns, hypotheses\nand recommended angles]
|
|
27
|
+
J -- No --> L[No prior context\nstart fresh]
|
|
28
|
+
K & L --> M([Ready to coach])
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Coaching loop
|
|
34
|
+
|
|
35
|
+
After every technical task Claude evaluates whether to deliver a lesson.
|
|
36
|
+
The loop is silent when nothing is worth teaching or when the rate limit is reached.
|
|
37
|
+
|
|
38
|
+
```mermaid
|
|
39
|
+
flowchart TD
|
|
40
|
+
A([Technical task\ncompleted]) --> B[Read devcoach://rate-limit]
|
|
41
|
+
B --> C{Allowed?}
|
|
42
|
+
C -- No\nnormal task --> Z([Silent —\nno lesson])
|
|
43
|
+
C -- No\nexplicit request --> D
|
|
44
|
+
C -- Yes --> D[Read profile\ntaught topics\ncoaching notebook]
|
|
45
|
+
D --> E[Analyse task for\nteachable concepts]
|
|
46
|
+
E --> F[Select topic\nsee Lesson selection]
|
|
47
|
+
F --> G{Topic\nfound?}
|
|
48
|
+
G -- No --> Z
|
|
49
|
+
G -- Yes --> H[Compose lesson\ncalibrate depth per-topic]
|
|
50
|
+
H --> I[log_lesson to MCP]
|
|
51
|
+
I --> J([Lesson appears\nat end of response])
|
|
52
|
+
J --> K{User\nfeedback}
|
|
53
|
+
K -- ✅ know --> L{Confidence\nbelow level band?}
|
|
54
|
+
L -- Yes --> M[submit_feedback\nconfidence +1]
|
|
55
|
+
L -- No --> N[Skip — already calibrated]
|
|
56
|
+
K -- ❌ don't know --> O[submit_feedback\nconfidence −1]
|
|
57
|
+
K -- ⏭ skip --> P[No change]
|
|
58
|
+
M & N & O & P --> Q{New observation\nworth saving?}
|
|
59
|
+
Q -- Yes --> R[Write ~/.devcoach/\nlearning-state.md]
|
|
60
|
+
Q -- No --> S
|
|
61
|
+
R --> S([Loop ends])
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Lesson selection
|
|
67
|
+
|
|
68
|
+
When a teachable concept is found, devcoach picks the highest-priority angle
|
|
69
|
+
and calibrates the lesson level to the **per-topic** confidence score — not an average.
|
|
70
|
+
|
|
71
|
+
```mermaid
|
|
72
|
+
flowchart TD
|
|
73
|
+
A([Concepts identified\nin current task]) --> B{Notebook flags\na follow-up angle\nrelevant to this task?}
|
|
74
|
+
B -- Yes --> P1["① Deliver notebook\nfollow-up"]
|
|
75
|
+
|
|
76
|
+
B -- No --> C{Pitfall on a\nprofile topic?}
|
|
77
|
+
C -- Yes --> P2["② Profile pitfall"]
|
|
78
|
+
|
|
79
|
+
C -- No --> D{Interesting pattern\non a profile topic?}
|
|
80
|
+
D -- Yes --> P3["③ Profile pattern"]
|
|
81
|
+
|
|
82
|
+
D -- No --> E{Off-profile concept\nprominent in task?}
|
|
83
|
+
E -- Yes --> P4["④ Off-profile pitfall"]
|
|
84
|
+
|
|
85
|
+
E -- No --> F{Profile topic with\nconfidence < 5?}
|
|
86
|
+
F -- Yes --> P5["⑤ Knowledge gap"]
|
|
87
|
+
|
|
88
|
+
F -- No --> G{Profile topic at\nconfidence 4–6?}
|
|
89
|
+
G -- Yes --> P6["⑥ Deep-dive"]
|
|
90
|
+
|
|
91
|
+
G -- No --> Z([Nothing to teach\n— stay silent])
|
|
92
|
+
|
|
93
|
+
P1 & P2 & P3 & P4 & P5 & P6 --> L[Check taught-topics\nno repeats]
|
|
94
|
+
L --> M{Already taught\nor confidence ≥ 10?}
|
|
95
|
+
M -- Already taught\nnot confidence 10 --> Z
|
|
96
|
+
M -- OK or\nconfidence = 10 --> N[Calibrate level\nper-topic confidence]
|
|
97
|
+
N --> O([Compose and\ndeliver lesson])
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Depth calibration
|
|
103
|
+
|
|
104
|
+
The lesson level is determined by the confidence score for the **specific topic being taught**,
|
|
105
|
+
adjusted by observations in the coaching notebook.
|
|
106
|
+
|
|
107
|
+
| Confidence | Level | Lesson angle |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| 0 – 3 | Junior | Introduce correct practice, explain from scratch, use analogies |
|
|
110
|
+
| 4 – 6 | Mid | Explain the why, mention trade-offs and alternatives |
|
|
111
|
+
| 7 – 9 | Senior | Edge cases, historical context, architectural implications |
|
|
112
|
+
| 10 | Cutting-edge | Latest developments — ignores level floor and taught-topics filter |
|
|
@@ -8,12 +8,30 @@ Everything runs **locally**. No data leaves your machine. One SQLite file at `~/
|
|
|
8
8
|
|
|
9
9
|
## How it works
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
```mermaid
|
|
12
|
+
sequenceDiagram
|
|
13
|
+
actor User
|
|
14
|
+
participant Claude as Claude (AI)
|
|
15
|
+
participant devcoach as devcoach (MCP)
|
|
16
|
+
|
|
17
|
+
User->>+Claude: Complete a technical task
|
|
18
|
+
Claude-->>User: Work completed normally
|
|
19
|
+
|
|
20
|
+
Claude->>devcoach: check rate-limit + profile + taught topics
|
|
21
|
+
devcoach-->>Claude: knowledge map · lesson history · coaching notebook
|
|
22
|
+
|
|
23
|
+
Claude->>Claude: select topic · calibrate depth · compose lesson
|
|
24
|
+
|
|
25
|
+
Claude->>devcoach: log_lesson(id, topic, level, body, …)
|
|
26
|
+
Claude-->>-User: Response + 🎓 lesson at the bottom
|
|
27
|
+
|
|
28
|
+
User->>+Claude: ✅ know · ❌ don't know · ⏭ skip
|
|
29
|
+
Claude->>devcoach: submit_feedback → confidence ±1
|
|
30
|
+
Claude->>Claude: update coaching notebook if warranted
|
|
31
|
+
Claude-->>-User: acknowledged
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
See [How it works](how-it-works.md) for the full decision flow.
|
|
17
35
|
|
|
18
36
|
---
|
|
19
37
|
|
|
@@ -43,6 +61,14 @@ Everything runs **locally**. No data leaves your machine. One SQLite file at `~/
|
|
|
43
61
|
=== "Light"
|
|
44
62
|

|
|
45
63
|
|
|
64
|
+
### Lesson detail
|
|
65
|
+
|
|
66
|
+
=== "Dark"
|
|
67
|
+

|
|
68
|
+
|
|
69
|
+
=== "Light"
|
|
70
|
+

|
|
71
|
+
|
|
46
72
|
---
|
|
47
73
|
|
|
48
74
|
## Quick install
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Web UI
|
|
2
|
+
|
|
3
|
+
The devcoach web dashboard provides a visual interface for everything the CLI does.
|
|
4
|
+
|
|
5
|
+
## Launch
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
devcoach ui # http://localhost:7860
|
|
9
|
+
devcoach ui --port 8080 # custom port
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Or via the MCP tool (Claude can launch it for you):
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
open_ui({ port: 7860 })
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Pages
|
|
21
|
+
|
|
22
|
+
### Knowledge map (`/`)
|
|
23
|
+
|
|
24
|
+
Displays your full knowledge map with colour-coded confidence bars:
|
|
25
|
+
|
|
26
|
+
- **Green** — confidence ≥ 7 (strong)
|
|
27
|
+
- **Yellow** — confidence 4–6 (intermediate)
|
|
28
|
+
- **Red** — confidence ≤ 3 (learning)
|
|
29
|
+
|
|
30
|
+
**Edit mode** (click `✎ Edit`):
|
|
31
|
+
- `+` / `−` buttons beside each bar to adjust confidence by 1
|
|
32
|
+
- `⇄` button to move a topic to a different group
|
|
33
|
+
- `×` button to delete a topic
|
|
34
|
+
- `+ topic` button in each group header to add a topic directly to that group
|
|
35
|
+
- `+ Add group` / `+ Add topic` buttons in the page header
|
|
36
|
+
- `×` beside group names to delete a group (topics move to Other)
|
|
37
|
+
|
|
38
|
+
**View mode**: topic names are clickable links that filter the lessons page to that topic.
|
|
39
|
+
|
|
40
|
+
**Stats bar** (top of page): total lessons, today's count vs. daily limit, this week's count, and current rate-limit status.
|
|
41
|
+
|
|
42
|
+
=== "Dark"
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
=== "Light"
|
|
46
|
+

|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
### Lessons (`/lessons`)
|
|
51
|
+
|
|
52
|
+
Filterable, sortable table of all delivered lessons.
|
|
53
|
+
|
|
54
|
+
**Filters:**
|
|
55
|
+
- Period (today / week / month / year / all)
|
|
56
|
+
- Category tag
|
|
57
|
+
- Difficulty level
|
|
58
|
+
- Project / repository / branch / commit hash
|
|
59
|
+
- Starred only
|
|
60
|
+
- Feedback (know / dont_know / none)
|
|
61
|
+
- Free-text search
|
|
62
|
+
- Date range (supports optional time: `2026-04-25T14:30`)
|
|
63
|
+
|
|
64
|
+
**Sort:** by timestamp, level, topic, title, or feedback. Ascending or descending.
|
|
65
|
+
|
|
66
|
+
**Pagination:** 25 per page.
|
|
67
|
+
|
|
68
|
+
**Actions per lesson:**
|
|
69
|
+
- `★` — toggle starred
|
|
70
|
+
- Feedback buttons (✓ / ✗ / clear) — record comprehension, adjusts knowledge confidence
|
|
71
|
+
- Lesson ID link — opens the detail page
|
|
72
|
+
|
|
73
|
+
=== "Dark"
|
|
74
|
+

|
|
75
|
+
|
|
76
|
+
=== "Light"
|
|
77
|
+

|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
### Lesson detail (`/lessons/<id>`)
|
|
82
|
+
|
|
83
|
+
Full lesson content laid out in reading order:
|
|
84
|
+
|
|
85
|
+
- **Title row** — star toggle, title, level badge (junior / mid / senior)
|
|
86
|
+
- **Metadata row** — relative date with tooltip, topic ID, category tags, feedback badge + clear button
|
|
87
|
+
- **TL;DR callout** — one-sentence summary in a highlighted indigo box, always visible above the body
|
|
88
|
+
- **Lesson body** — full markdown content with syntax-highlighted code blocks
|
|
89
|
+
- **Task context** — the coding task that triggered the lesson (when available)
|
|
90
|
+
- **Git metadata** — project, repository (with platform icon + link), branch, commit hash, folder (VSCode deep-link)
|
|
91
|
+
- **Feedback buttons** — ✓ I know this / ✗ I don't know this (hidden once feedback is recorded)
|
|
92
|
+
|
|
93
|
+
=== "Docker layer caching (Junior)"
|
|
94
|
+
=== "Dark"
|
|
95
|
+

|
|
96
|
+
=== "Light"
|
|
97
|
+

|
|
98
|
+
|
|
99
|
+
=== "PostgreSQL EXPLAIN ANALYZE (Mid)"
|
|
100
|
+
=== "Dark"
|
|
101
|
+

|
|
102
|
+
=== "Light"
|
|
103
|
+

|
|
104
|
+
|
|
105
|
+
=== "Git interactive rebase (Mid)"
|
|
106
|
+
=== "Dark"
|
|
107
|
+

|
|
108
|
+
=== "Light"
|
|
109
|
+

|
|
110
|
+
|
|
111
|
+
=== "CI/CD pipeline stages (Senior)"
|
|
112
|
+
=== "Dark"
|
|
113
|
+

|
|
114
|
+
=== "Light"
|
|
115
|
+

|
|
116
|
+
|
|
117
|
+
=== "Cache stampede (Senior)"
|
|
118
|
+
=== "Dark"
|
|
119
|
+

|
|
120
|
+
=== "Light"
|
|
121
|
+

|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
### Settings (`/settings`)
|
|
126
|
+
|
|
127
|
+
- **Max per day** — maximum lessons in a 24-hour window (1–20)
|
|
128
|
+
- **Min gap** — minimum minutes between lessons (0–1440), input as hours + minutes
|
|
129
|
+
- **Export lessons** — download all lessons as JSON
|
|
130
|
+
- **Import lessons** — upload a previously exported JSON file
|
|
131
|
+
- **Export backup** — full zip (settings + knowledge + lessons)
|
|
132
|
+
- **Import backup** — restore from a backup zip
|
|
133
|
+
|
|
134
|
+
=== "Dark"
|
|
135
|
+

|
|
136
|
+
|
|
137
|
+
=== "Light"
|
|
138
|
+

|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Keyboard shortcuts
|
|
143
|
+
|
|
144
|
+
The web UI has no keyboard shortcuts. Use the CLI for faster access to individual commands.
|
|
@@ -27,12 +27,14 @@ theme:
|
|
|
27
27
|
- navigation.indexes
|
|
28
28
|
- search.highlight
|
|
29
29
|
- content.code.copy
|
|
30
|
+
- content.tabs.link
|
|
30
31
|
- toc.integrate
|
|
31
32
|
icon:
|
|
32
33
|
repo: fontawesome/brands/github
|
|
33
34
|
|
|
34
35
|
nav:
|
|
35
36
|
- Home: index.md
|
|
37
|
+
- How it works: how-it-works.md
|
|
36
38
|
- Getting started: getting-started.md
|
|
37
39
|
- CLI reference: cli.md
|
|
38
40
|
- MCP server: mcp-server.md
|
|
@@ -46,7 +48,11 @@ markdown_extensions:
|
|
|
46
48
|
- admonition
|
|
47
49
|
- pymdownx.highlight:
|
|
48
50
|
anchor_linenums: true
|
|
49
|
-
- pymdownx.superfences
|
|
51
|
+
- pymdownx.superfences:
|
|
52
|
+
custom_fences:
|
|
53
|
+
- name: mermaid
|
|
54
|
+
class: mermaid
|
|
55
|
+
format: !!python/name:pymdownx.superfences.fence_code_format
|
|
50
56
|
- pymdownx.tabbed:
|
|
51
57
|
alternate_style: true
|
|
52
58
|
- tables
|