jean-agent 0.1.0__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.
- jean_agent-0.1.0/.github/workflows/release.yml +69 -0
- jean_agent-0.1.0/.gitignore +168 -0
- jean_agent-0.1.0/AGENTS.md +333 -0
- jean_agent-0.1.0/LICENSE +21 -0
- jean_agent-0.1.0/PKG-INFO +279 -0
- jean_agent-0.1.0/README.md +212 -0
- jean_agent-0.1.0/jean/__init__.py +3 -0
- jean_agent-0.1.0/jean/__main__.py +4 -0
- jean_agent-0.1.0/jean/app.py +228 -0
- jean_agent-0.1.0/jean/auth/__init__.py +0 -0
- jean_agent-0.1.0/jean/auth/deps.py +45 -0
- jean_agent-0.1.0/jean/auth/siws.py +119 -0
- jean_agent-0.1.0/jean/builtin_skills/__init__.py +1 -0
- jean_agent-0.1.0/jean/builtin_skills/skill-builder/SKILL.md +97 -0
- jean_agent-0.1.0/jean/cli.py +853 -0
- jean_agent-0.1.0/jean/config.py +266 -0
- jean_agent-0.1.0/jean/credentials.py +176 -0
- jean_agent-0.1.0/jean/db.py +111 -0
- jean_agent-0.1.0/jean/diagnostics.py +330 -0
- jean_agent-0.1.0/jean/harness/__init__.py +0 -0
- jean_agent-0.1.0/jean/harness/io_tools.py +104 -0
- jean_agent-0.1.0/jean/harness/options.py +204 -0
- jean_agent-0.1.0/jean/harness/sessions.py +480 -0
- jean_agent-0.1.0/jean/harness/skill_tools.py +270 -0
- jean_agent-0.1.0/jean/harness/skills.py +168 -0
- jean_agent-0.1.0/jean/harness/user_skills.py +225 -0
- jean_agent-0.1.0/jean/migrations/001_initial.sql +61 -0
- jean_agent-0.1.0/jean/migrations/002_channel_approvals.sql +11 -0
- jean_agent-0.1.0/jean/migrations/003_pending_channel_messages.sql +8 -0
- jean_agent-0.1.0/jean/migrations/004_user_skills.sql +8 -0
- jean_agent-0.1.0/jean/migrations/005_owner_cc.sql +1 -0
- jean_agent-0.1.0/jean/migrations/__init__.py +1 -0
- jean_agent-0.1.0/jean/models.py +616 -0
- jean_agent-0.1.0/jean/setup_guide.py +183 -0
- jean_agent-0.1.0/jean/slack/__init__.py +0 -0
- jean_agent-0.1.0/jean/slack/approvals.py +269 -0
- jean_agent-0.1.0/jean/slack/attachments.py +144 -0
- jean_agent-0.1.0/jean/slack/bolt.py +52 -0
- jean_agent-0.1.0/jean/slack/formatting.py +107 -0
- jean_agent-0.1.0/jean/slack/handlers.py +794 -0
- jean_agent-0.1.0/jean/slack/identity.py +251 -0
- jean_agent-0.1.0/jean/slack/thread_context.py +91 -0
- jean_agent-0.1.0/jean/templates/Dockerfile.tmpl +55 -0
- jean_agent-0.1.0/jean/templates/__init__.py +1 -0
- jean_agent-0.1.0/jean/templates/dockerignore.tmpl +15 -0
- jean_agent-0.1.0/jean/templates/fly.toml.tmpl +27 -0
- jean_agent-0.1.0/jean/templates/jean.toml.tmpl +86 -0
- jean_agent-0.1.0/jean/templates/jean_system_prompt.md.tmpl +10 -0
- jean_agent-0.1.0/jean/web/__init__.py +0 -0
- jean_agent-0.1.0/jean/web/display.py +67 -0
- jean_agent-0.1.0/jean/web/routes.py +470 -0
- jean_agent-0.1.0/jean/web/static/jean.css +5 -0
- jean_agent-0.1.0/jean/web/templates/admin_channels.html.j2 +57 -0
- jean_agent-0.1.0/jean/web/templates/admin_skill_detail.html.j2 +38 -0
- jean_agent-0.1.0/jean/web/templates/admin_skills.html.j2 +59 -0
- jean_agent-0.1.0/jean/web/templates/admin_users.html.j2 +45 -0
- jean_agent-0.1.0/jean/web/templates/base.html.j2 +40 -0
- jean_agent-0.1.0/jean/web/templates/conversation_detail.html.j2 +41 -0
- jean_agent-0.1.0/jean/web/templates/conversations_list.html.j2 +30 -0
- jean_agent-0.1.0/jean/web/templates/diagnostics.html.j2 +32 -0
- jean_agent-0.1.0/jean/web/templates/index.html.j2 +17 -0
- jean_agent-0.1.0/jean/web/templates/misconfig.html.j2 +27 -0
- jean_agent-0.1.0/jean/wizard.py +423 -0
- jean_agent-0.1.0/pyproject.toml +87 -0
- jean_agent-0.1.0/skills/haiku/SKILL.md +23 -0
- jean_agent-0.1.0/tests/__init__.py +0 -0
- jean_agent-0.1.0/tests/conftest.py +85 -0
- jean_agent-0.1.0/tests/test_attachments.py +48 -0
- jean_agent-0.1.0/tests/test_config.py +61 -0
- jean_agent-0.1.0/tests/test_db.py +94 -0
- jean_agent-0.1.0/tests/test_diagnostics.py +37 -0
- jean_agent-0.1.0/tests/test_formatting.py +85 -0
- jean_agent-0.1.0/tests/test_imports.py +32 -0
- jean_agent-0.1.0/tests/test_skills.py +119 -0
- jean_agent-0.1.0/tests/test_user_skills.py +326 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
name: Release to PyPI
|
|
2
|
+
|
|
3
|
+
# Triggers on either a `v*` tag push (typical release) or a manual
|
|
4
|
+
# workflow_dispatch (re-publish without a new tag, e.g. recovering from
|
|
5
|
+
# a build flake). The workflow validates that pyproject.toml's version
|
|
6
|
+
# matches the tag name before publishing.
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
tags:
|
|
10
|
+
- "v*"
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
build:
|
|
18
|
+
name: Build wheel + sdist
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
- name: Set up Python
|
|
23
|
+
uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: "3.12"
|
|
26
|
+
- name: Install build tooling
|
|
27
|
+
run: pip install --upgrade build hatchling
|
|
28
|
+
- name: Verify version matches tag
|
|
29
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
30
|
+
run: |
|
|
31
|
+
TAG_VERSION="${GITHUB_REF_NAME#v}"
|
|
32
|
+
PROJ_VERSION=$(python -c "import tomllib, pathlib; print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'])")
|
|
33
|
+
if [ "$TAG_VERSION" != "$PROJ_VERSION" ]; then
|
|
34
|
+
echo "::error::Tag is v$TAG_VERSION but pyproject.toml says version=$PROJ_VERSION"
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
echo "Version $PROJ_VERSION matches tag $GITHUB_REF_NAME ✓"
|
|
38
|
+
- name: Build distributions
|
|
39
|
+
run: python -m build
|
|
40
|
+
- name: Upload artifacts
|
|
41
|
+
uses: actions/upload-artifact@v4
|
|
42
|
+
with:
|
|
43
|
+
name: dist
|
|
44
|
+
path: dist/
|
|
45
|
+
|
|
46
|
+
publish:
|
|
47
|
+
name: Publish to PyPI
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
needs: build
|
|
50
|
+
environment:
|
|
51
|
+
# The `pypi` environment must exist in the GitHub repo's Settings →
|
|
52
|
+
# Environments. PyPI's Trusted Publisher config is bound to it.
|
|
53
|
+
name: pypi
|
|
54
|
+
url: https://pypi.org/project/jean-agent/
|
|
55
|
+
permissions:
|
|
56
|
+
# Required for PyPI Trusted Publishing (OIDC token issuance).
|
|
57
|
+
id-token: write
|
|
58
|
+
steps:
|
|
59
|
+
- name: Download artifacts
|
|
60
|
+
uses: actions/download-artifact@v4
|
|
61
|
+
with:
|
|
62
|
+
name: dist
|
|
63
|
+
path: dist/
|
|
64
|
+
- name: Publish to PyPI via Trusted Publishing
|
|
65
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
66
|
+
# No api-token configured — Trusted Publishing uses the OIDC token
|
|
67
|
+
# signed by GitHub Actions and verified by PyPI against the
|
|
68
|
+
# pending-publisher config (project + owner + repo + workflow +
|
|
69
|
+
# environment).
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
*.manifest
|
|
31
|
+
*.spec
|
|
32
|
+
|
|
33
|
+
# Installer logs
|
|
34
|
+
pip-log.txt
|
|
35
|
+
pip-delete-this-directory.txt
|
|
36
|
+
|
|
37
|
+
# Unit test / coverage reports
|
|
38
|
+
htmlcov/
|
|
39
|
+
.tox/
|
|
40
|
+
.nox/
|
|
41
|
+
.coverage
|
|
42
|
+
.coverage.*
|
|
43
|
+
.cache
|
|
44
|
+
nosetests.xml
|
|
45
|
+
coverage.xml
|
|
46
|
+
*.cover
|
|
47
|
+
*.py,cover
|
|
48
|
+
.hypothesis/
|
|
49
|
+
.pytest_cache/
|
|
50
|
+
cover/
|
|
51
|
+
|
|
52
|
+
# Translations
|
|
53
|
+
*.mo
|
|
54
|
+
*.pot
|
|
55
|
+
|
|
56
|
+
# Django stuff:
|
|
57
|
+
*.log
|
|
58
|
+
local_settings.py
|
|
59
|
+
db.sqlite3
|
|
60
|
+
db.sqlite3-journal
|
|
61
|
+
|
|
62
|
+
# Flask stuff:
|
|
63
|
+
instance/
|
|
64
|
+
.webassets-cache
|
|
65
|
+
|
|
66
|
+
# Scrapy stuff:
|
|
67
|
+
.scrapy
|
|
68
|
+
|
|
69
|
+
# Sphinx documentation
|
|
70
|
+
docs/_build/
|
|
71
|
+
|
|
72
|
+
# PyBuilder
|
|
73
|
+
.pybuilder/
|
|
74
|
+
target/
|
|
75
|
+
|
|
76
|
+
# Jupyter Notebook
|
|
77
|
+
.ipynb_checkpoints
|
|
78
|
+
|
|
79
|
+
# IPython
|
|
80
|
+
profile_default/
|
|
81
|
+
ipython_config.py
|
|
82
|
+
|
|
83
|
+
# pyenv
|
|
84
|
+
.python-version
|
|
85
|
+
|
|
86
|
+
# pipenv
|
|
87
|
+
Pipfile.lock
|
|
88
|
+
|
|
89
|
+
# poetry
|
|
90
|
+
poetry.lock
|
|
91
|
+
|
|
92
|
+
# pdm
|
|
93
|
+
.pdm.toml
|
|
94
|
+
.pdm-python
|
|
95
|
+
.pdm-build/
|
|
96
|
+
|
|
97
|
+
# PEP 582
|
|
98
|
+
__pypackages__/
|
|
99
|
+
|
|
100
|
+
# Celery
|
|
101
|
+
celerybeat-schedule
|
|
102
|
+
celerybeat.pid
|
|
103
|
+
|
|
104
|
+
# SageMath parsed files
|
|
105
|
+
*.sage.py
|
|
106
|
+
|
|
107
|
+
# Environments
|
|
108
|
+
.env
|
|
109
|
+
.env.*
|
|
110
|
+
!.env.example
|
|
111
|
+
.venv
|
|
112
|
+
env/
|
|
113
|
+
venv/
|
|
114
|
+
ENV/
|
|
115
|
+
env.bak/
|
|
116
|
+
venv.bak/
|
|
117
|
+
|
|
118
|
+
# Spyder project settings
|
|
119
|
+
.spyderproject
|
|
120
|
+
.spyproject
|
|
121
|
+
|
|
122
|
+
# Rope project settings
|
|
123
|
+
.ropeproject
|
|
124
|
+
|
|
125
|
+
# mkdocs documentation
|
|
126
|
+
/site
|
|
127
|
+
|
|
128
|
+
# mypy
|
|
129
|
+
.mypy_cache/
|
|
130
|
+
.dmypy.json
|
|
131
|
+
dmypy.json
|
|
132
|
+
|
|
133
|
+
# Pyre type checker
|
|
134
|
+
.pyre/
|
|
135
|
+
|
|
136
|
+
# pytype static type analyzer
|
|
137
|
+
.pytype/
|
|
138
|
+
|
|
139
|
+
# Cython debug symbols
|
|
140
|
+
cython_debug/
|
|
141
|
+
|
|
142
|
+
# Ruff
|
|
143
|
+
.ruff_cache/
|
|
144
|
+
|
|
145
|
+
# IDEs
|
|
146
|
+
.idea/
|
|
147
|
+
.vscode/
|
|
148
|
+
*.swp
|
|
149
|
+
*.swo
|
|
150
|
+
*~
|
|
151
|
+
|
|
152
|
+
# OS
|
|
153
|
+
.DS_Store
|
|
154
|
+
Thumbs.db
|
|
155
|
+
|
|
156
|
+
# Jean runtime artifacts (when self-hosting jean against this repo).
|
|
157
|
+
# The canonical templates live in jean/templates/; these are local copies
|
|
158
|
+
# from `jean init` and the local data the running app produces.
|
|
159
|
+
/jean.toml
|
|
160
|
+
/jean.local.toml
|
|
161
|
+
/fly.toml
|
|
162
|
+
/Dockerfile
|
|
163
|
+
/.dockerignore
|
|
164
|
+
/jean_system_prompt.md
|
|
165
|
+
/.jean/
|
|
166
|
+
/.jean-vendor/
|
|
167
|
+
/credentials.enc
|
|
168
|
+
/.claude/skills
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Read this before working on jean. It captures the architectural invariants
|
|
4
|
+
and the non-obvious gotchas we discovered the hard way.
|
|
5
|
+
|
|
6
|
+
## What jean is
|
|
7
|
+
|
|
8
|
+
A drop-in Slack-fronted Claude Code agent for any host repo. The host adds a
|
|
9
|
+
`skills/` directory plus `jean init`-generated artifacts; jean handles the
|
|
10
|
+
Slack bot, web UI for conversation history, Sign in with Slack auth,
|
|
11
|
+
per-thread Claude sessions, owner-gated rollout (private mode + per-channel
|
|
12
|
+
approvals + non-owner CC), user-created skills via DM, two-way file
|
|
13
|
+
attachments, and Fly.io deploy.
|
|
14
|
+
|
|
15
|
+
The same `jean` package self-tests against itself — `skills/haiku/SKILL.md`
|
|
16
|
+
in this repo is the bundled smoke skill so `jean run` here gives you a
|
|
17
|
+
working bot.
|
|
18
|
+
|
|
19
|
+
## Project layout
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
jean/
|
|
23
|
+
cli.py typer CLI (init, doctor, run, configure-secrets,
|
|
24
|
+
deploy, print-manifest, migrate, grant/revoke-admin)
|
|
25
|
+
config.py Pydantic config model; jean.toml + env + encrypted-store merge
|
|
26
|
+
diagnostics.py Phase 1 (offline) + Phase 2 (Slack API) checks
|
|
27
|
+
setup_guide.py Walkthrough + Slack app manifest renderer
|
|
28
|
+
wizard.py Interactive secrets wizard (browser-open, clipboard, live auth.test)
|
|
29
|
+
credentials.py AES-256-GCM encrypted store; master key in OS keychain
|
|
30
|
+
app.py FastAPI factory + lifespan (starts SessionManager, Socket Mode)
|
|
31
|
+
db.py aiosqlite + WAL pragmas + migration runner
|
|
32
|
+
models.py Dataclasses + DAO for users, conversations, messages,
|
|
33
|
+
attachments, channel_approvals, pending_channel_messages,
|
|
34
|
+
user_skills
|
|
35
|
+
auth/
|
|
36
|
+
siws.py Sign in with Slack via authlib (OIDC)
|
|
37
|
+
deps.py FastAPI deps: current_user, require_user/admin/owner
|
|
38
|
+
harness/
|
|
39
|
+
options.py Build ClaudeAgentOptions from cfg; stitches MCP servers,
|
|
40
|
+
pipes SDK stderr into jean's logger
|
|
41
|
+
sessions.py SessionManager + LiveSession + idle reaper + debug dump
|
|
42
|
+
+ resume-failure fallback + context_was_lost flag
|
|
43
|
+
skills.py Skill discovery + .claude/skills per-skill symlink materializer
|
|
44
|
+
skill_tools.py MCP server `jean_skills` (DM-only): list/read/save/delete user skills
|
|
45
|
+
io_tools.py MCP server `jean_io` (every session): send_file_to_user
|
|
46
|
+
user_skills.py Filesystem layer + reconcile loop for user-created skills
|
|
47
|
+
slack/
|
|
48
|
+
bolt.py AsyncApp + Socket Mode handler factories (pops CLIENT_ID/SECRET
|
|
49
|
+
from env so bolt doesn't auto-enable OAuth)
|
|
50
|
+
identity.py auth.test + owner resolution + UsersCache + mention resolver
|
|
51
|
+
handlers.py app_mention + message.im → SessionManager; EventDedup;
|
|
52
|
+
ThrottledPoster; ProgressReactionLoop; private mode +
|
|
53
|
+
channel approval gates; owner-CC; rescue context
|
|
54
|
+
approvals.py Block Kit approval UI + action handlers + replay-on-approve
|
|
55
|
+
attachments.py Download Slack files → multimodal content blocks
|
|
56
|
+
thread_context.py Fetch unseen replies for thread catch-up; full-thread mode
|
|
57
|
+
(include_bot=True) for rescue context after session loss
|
|
58
|
+
formatting.py mrkdwn conversion + chunking + skill-preamble fallback regex
|
|
59
|
+
web/
|
|
60
|
+
routes.py /, /conversations, /admin/(users|skills|channels),
|
|
61
|
+
/_diagnostics, /attachments
|
|
62
|
+
display.py Slack ID → human handle resolver (DB-first, cache fallback)
|
|
63
|
+
templates/ Jinja2 (base, index, conversations_list,
|
|
64
|
+
admin_users/skills/channels, etc.)
|
|
65
|
+
static/ Pico CSS + jean.css
|
|
66
|
+
migrations/ 001_initial, 002_channel_approvals, 003_pending_channel_messages,
|
|
67
|
+
004_user_skills, 005_owner_cc
|
|
68
|
+
templates/ Files copied by `jean init` (jean.toml, fly.toml, Dockerfile, …)
|
|
69
|
+
builtin_skills/ Skills shipped inside the jean package (skill-builder)
|
|
70
|
+
skills/haiku/ Bundled self-test skill
|
|
71
|
+
tests/ pytest; 51 tests; uses pytest-asyncio auto mode
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Local development
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
python3 -m venv .venv
|
|
78
|
+
source .venv/bin/activate
|
|
79
|
+
pip install -e .[dev]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Run the package against itself (you need a real Slack app + Anthropic key —
|
|
83
|
+
see `jean doctor --setup-guide` for the manifest + step-by-step):
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
jean doctor # Phase 1, no network
|
|
87
|
+
jean configure-secrets # encrypted store wizard
|
|
88
|
+
jean doctor --deep # Phase 2 (auth.test + scope check + owner resolve)
|
|
89
|
+
jean run # FastAPI :8080 + Socket Mode listener
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Tests
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pytest # 51 tests; ~0.7s
|
|
96
|
+
pytest tests/test_db.py # focused
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Fixtures in `tests/conftest.py` build a tmpdir config + SQLite DB, mock env
|
|
100
|
+
vars where needed. There are NO tests that hit Slack or Anthropic APIs;
|
|
101
|
+
`test_create_app_smoke_does_not_crash_in_force_mode` stubs identity to
|
|
102
|
+
exercise the full `create_app(force=True)` path offline.
|
|
103
|
+
|
|
104
|
+
## Architecture invariants — don't break these
|
|
105
|
+
|
|
106
|
+
1. **Single uvicorn worker, single Fly machine.** SQLite + WAL needs one
|
|
107
|
+
writer; the in-memory `SessionManager` needs single-process state.
|
|
108
|
+
2. **Socket Mode only.** No public webhook URL needed for events; only the
|
|
109
|
+
SIWS callback is inbound HTTP. Don't reintroduce HTTP Events Mode without
|
|
110
|
+
re-evaluating the ACK story (see the `process_before_response` gotcha).
|
|
111
|
+
3. **One LiveSession per (channel, thread_ts)** while warm. Resume from
|
|
112
|
+
disk via `ClaudeAgentOptions(resume=session_id)` after idle reap. The SDK
|
|
113
|
+
writes transcripts to `~/.claude/...` (per `getpwuid()` lookup) — see the
|
|
114
|
+
non-root user gotcha below for the symlink trick that keeps them on the
|
|
115
|
+
Fly volume.
|
|
116
|
+
4. **Env vars override the encrypted credentials file** at runtime. Never
|
|
117
|
+
reverse the precedence — `fly secrets set` must always win.
|
|
118
|
+
5. **Skill discovery is filesystem-based** via `setting_sources=["project"]`
|
|
119
|
+
and `.claude/skills/`. We materialize per-skill symlinks from three
|
|
120
|
+
sources (repo / builtin / user) with precedence repo > builtin > user.
|
|
121
|
+
6. **DM-only invariant for skill mutations.** Three gates: visibility in
|
|
122
|
+
the `skills_only` system prompt, MCP server registration, in-tool check.
|
|
123
|
+
|
|
124
|
+
## Non-obvious gotchas (read before changing related code)
|
|
125
|
+
|
|
126
|
+
These each cost real debugging time. Don't re-introduce them.
|
|
127
|
+
|
|
128
|
+
### Bundled Claude CLI ignores `HOME`; needs `~/.claude` symlinked to the volume
|
|
129
|
+
|
|
130
|
+
The Claude CLI binary that ships inside `claude-agent-sdk` resolves home via
|
|
131
|
+
`getpwuid()` (not `$HOME`). The `jean` user's passwd home is `/home/jean`,
|
|
132
|
+
so transcripts go to `/home/jean/.claude/...` regardless of `HOME=/data/home`.
|
|
133
|
+
That's on the ephemeral container layer — gone on every redeploy → every
|
|
134
|
+
existing thread breaks with "No conversation found with session ID …".
|
|
135
|
+
|
|
136
|
+
The Dockerfile's entrypoint replaces `/home/jean/.claude` with a symlink
|
|
137
|
+
to `/data/home/.claude` on the persistent volume. Don't reintroduce code
|
|
138
|
+
that relies on `HOME=/data/home` working — verify with
|
|
139
|
+
`ls -la /home/jean/.claude` after a deploy that it's a symlink.
|
|
140
|
+
|
|
141
|
+
There's also a runtime fallback: when resume fails (`LiveSession._ensure_client`
|
|
142
|
+
catches the exception), the session retries fresh AND sets `context_was_lost`.
|
|
143
|
+
The handler reads that flag, fetches the full Slack thread (including the
|
|
144
|
+
bot's own past replies via `include_bot=True`), and feeds it to Claude as
|
|
145
|
+
"rescue context" so the user keeps continuity. See
|
|
146
|
+
`jean/slack/thread_context.py:fetch_unseen_thread_messages`.
|
|
147
|
+
|
|
148
|
+
### slack-bolt auto-enables OAuth when `SLACK_CLIENT_ID/SECRET` are in env
|
|
149
|
+
|
|
150
|
+
If both are present in `os.environ` when constructing `AsyncApp`, bolt
|
|
151
|
+
silently wires up a file-based `InstallationStore` + multi-team
|
|
152
|
+
authorization → every event fails with "AuthorizeResult was not found"
|
|
153
|
+
because the install store is empty. We use those env vars for our own
|
|
154
|
+
SIWS code via authlib, not for bolt. `jean/slack/bolt.py:build_app` pops
|
|
155
|
+
them from `os.environ` for the constructor's lifetime, then restores.
|
|
156
|
+
|
|
157
|
+
### SDK skill scaffolding lives in UserMessage TextBlocks
|
|
158
|
+
|
|
159
|
+
When a Claude Code skill activates, the SDK sends the SKILL.md content +
|
|
160
|
+
`ARGUMENTS: <input>` as a TextBlock inside a **UserMessage** (not an
|
|
161
|
+
AssistantMessage). It's input to the model, not the model's reply.
|
|
162
|
+
|
|
163
|
+
`jean/harness/sessions.py:_interpret_message` only yields TextDeltas from
|
|
164
|
+
**AssistantMessage** blocks for this reason. Don't add a fallback that
|
|
165
|
+
yields UserMessage text — you'll get the SKILL.md echoed into Slack.
|
|
166
|
+
|
|
167
|
+
A regex fallback exists in `jean/slack/formatting.py:filter_for_display`
|
|
168
|
+
as defense-in-depth in case a future SDK version moves scaffolding to
|
|
169
|
+
AssistantMessage.
|
|
170
|
+
|
|
171
|
+
### `bot_user_id` must be the Uxxx, not the Bxxx
|
|
172
|
+
|
|
173
|
+
`auth.test` returns both `bot_id` (`Bxxx`) and `user_id` (`Uxxx`). Slack
|
|
174
|
+
mentions use `<@Uxxx>` — bot-mention stripping and "is this the bot?"
|
|
175
|
+
checks need the user_id form. `jean/slack/identity.py:workspace_identity`
|
|
176
|
+
prefers `user_id`. Don't switch the order.
|
|
177
|
+
|
|
178
|
+
### `ThrottledPoster` needs `_post_lock`
|
|
179
|
+
|
|
180
|
+
Two concurrent `_do_flush` calls (loop tick + `finalize()`) can both
|
|
181
|
+
observe `posted_ts is None` and both call `chat.postMessage` → duplicate
|
|
182
|
+
replies. The `_post_lock` serializes the post-or-update decision across
|
|
183
|
+
the actual API call.
|
|
184
|
+
|
|
185
|
+
### `process_before_response` defaults to False — keep it that way
|
|
186
|
+
|
|
187
|
+
If `True`, bolt waits for the listener to finish before ACKing Slack.
|
|
188
|
+
Claude turns take >3s, Slack re-delivers, the listener runs twice →
|
|
189
|
+
duplicate replies. `EventDedup` catches it too, but the ACK timing is
|
|
190
|
+
the right fix.
|
|
191
|
+
|
|
192
|
+
### `client.query()` accepts `str` or `AsyncIterable[dict]`, never `list`
|
|
193
|
+
|
|
194
|
+
`to_content_blocks(text, staged_files)` returns a `list[dict]` for
|
|
195
|
+
multimodal messages. Pass it to `query()` directly and the SDK tries to
|
|
196
|
+
`async for` over the list — crash with `__aiter__` error. The fix wraps
|
|
197
|
+
in `_as_message_stream()` (single-item async generator) inside
|
|
198
|
+
`LiveSession.submit`.
|
|
199
|
+
|
|
200
|
+
### Don't run as root in Docker — Claude CLI refuses `bypassPermissions`
|
|
201
|
+
|
|
202
|
+
`--dangerously-skip-permissions` (which `bypassPermissions` translates to)
|
|
203
|
+
errors out with "cannot be used with root/sudo privileges". The Dockerfile
|
|
204
|
+
creates a non-root `jean` user (uid 1000) and uses `gosu` in the entrypoint
|
|
205
|
+
to drop privileges. Even with the current default `permission_mode = "auto"`,
|
|
206
|
+
keep the non-root setup — it's good practice independent of the SDK
|
|
207
|
+
check, and lets users flip to `bypassPermissions` cleanly.
|
|
208
|
+
|
|
209
|
+
### Honor `X-Forwarded-Proto` in uvicorn behind the Fly edge proxy
|
|
210
|
+
|
|
211
|
+
Fly terminates TLS at the edge and forwards plain HTTP to the container.
|
|
212
|
+
Without `proxy_headers=True` + `forwarded_allow_ips="*"`, `request.url_for()`
|
|
213
|
+
generates `http://...` callbacks → Slack rejects them with "redirect_uri
|
|
214
|
+
did not match". See the uvicorn config in `jean/cli.py:run`.
|
|
215
|
+
|
|
216
|
+
### `pip install jean` resolves to a different PyPI package
|
|
217
|
+
|
|
218
|
+
The `jean` name on PyPI is taken by an unrelated "OOP Learning Example".
|
|
219
|
+
The Dockerfile installs from the local repo (`pip install /app`). When we
|
|
220
|
+
publish to PyPI as `shinkansen-jean`, switch the Dockerfile back to
|
|
221
|
+
installing from PyPI by name.
|
|
222
|
+
|
|
223
|
+
### Hatchling: don't list packages in both `packages` and `force-include`
|
|
224
|
+
|
|
225
|
+
`packages = ["jean"]` already picks up every non-gitignored file under
|
|
226
|
+
`jean/`. Listing the same subdirs in `force-include` makes hatchling crash
|
|
227
|
+
with "second file at same path: …/__init__.py". The current `pyproject.toml`
|
|
228
|
+
uses only `packages`.
|
|
229
|
+
|
|
230
|
+
### Slack app's Messages Tab is off by default
|
|
231
|
+
|
|
232
|
+
Without `features.app_home.messages_tab_enabled: true` in the manifest,
|
|
233
|
+
users see "Se desactivó el envío de mensajes a esta aplicación" when they
|
|
234
|
+
try to DM the bot. `jean/setup_guide.py:render_manifest` includes the
|
|
235
|
+
toggle; existing apps need to flip it in App Home settings.
|
|
236
|
+
|
|
237
|
+
### Session must close after `save_skill_file` / `delete_skill_file`
|
|
238
|
+
|
|
239
|
+
The Claude SDK caches the skill list at client construction. New skills
|
|
240
|
+
written mid-turn aren't visible to the same client. The handler tracks
|
|
241
|
+
`SESSION_INVALIDATING_TOOLS` (in `jean/slack/handlers.py`) and calls
|
|
242
|
+
`SessionManager.close_session` after the turn ends — the next message in
|
|
243
|
+
the thread spins up a fresh client (via `resume=session_id`) that
|
|
244
|
+
re-discovers skills. Context survives via the SDK's on-disk transcript.
|
|
245
|
+
|
|
246
|
+
## How to add a skill (the host's job — but useful to know)
|
|
247
|
+
|
|
248
|
+
In the host repo, create `skills/<name>/SKILL.md`:
|
|
249
|
+
|
|
250
|
+
```markdown
|
|
251
|
+
---
|
|
252
|
+
name: <name>
|
|
253
|
+
description: One-line; surfaces in `skills_only` system prompt + triggers the model.
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
Body — Claude reads this when the skill activates.
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
The `[claude] skills_only = true` default lists every discovered skill's
|
|
260
|
+
name + description in the system prompt and tells Claude to politely
|
|
261
|
+
decline anything that doesn't fit a skill. See
|
|
262
|
+
`jean/harness/options.py:_skills_only_addendum`.
|
|
263
|
+
|
|
264
|
+
Skills can call two MCP tools jean exposes:
|
|
265
|
+
- `send_file_to_user(local_path, title?, comment?)` — upload an artifact to the current Slack thread (every session).
|
|
266
|
+
- Skill-management tools (`save_skill_file`, etc.) — only available in DMs and only to users matching `[claude] user_skill_authors` policy.
|
|
267
|
+
|
|
268
|
+
The bundled `skills/haiku/` is the simplest possible example.
|
|
269
|
+
`jean/builtin_skills/skill-builder/SKILL.md` is the meta-skill that drives
|
|
270
|
+
DM-based skill creation/editing.
|
|
271
|
+
|
|
272
|
+
## Debugging
|
|
273
|
+
|
|
274
|
+
- **Dump every SDK message** to inspect what's actually arriving:
|
|
275
|
+
`JEAN_DEBUG_SDK_MESSAGES=1 jean run` → writes JSON-lines to
|
|
276
|
+
`<db_dir>/sdk_debug.jsonl`. Used to discover the UserMessage gotcha.
|
|
277
|
+
- **SDK subprocess stderr** is piped into `jean.sdk.stderr` logger at
|
|
278
|
+
WARNING level by default — visible in `flyctl logs`. Surfaces "unknown
|
|
279
|
+
flag", "model not found", "No conversation found", etc.
|
|
280
|
+
- **SQLite shell**: `sqlite3 .jean/jean.db` (or `/data/jean.db` on Fly).
|
|
281
|
+
- **Web UI diagnostics page**: `/_diagnostics` (owner-only) shows the
|
|
282
|
+
same Phase 1 + Phase 2 checks as `jean doctor --deep`.
|
|
283
|
+
- **misconfig mode**: if startup checks fail, every route renders the
|
|
284
|
+
diagnostics page so the owner can fix things without dropping to CLI.
|
|
285
|
+
- **`jean run --verbose`** promotes log level to DEBUG and unmutes
|
|
286
|
+
upstream loggers (`httpx`, `slack_sdk`).
|
|
287
|
+
|
|
288
|
+
## When you change X, also do Y
|
|
289
|
+
|
|
290
|
+
- **New `[<section>]` field in config.py** → add to
|
|
291
|
+
`jean/templates/jean.toml.tmpl` with explanatory comment.
|
|
292
|
+
- **New DB column / table** → add a migration as
|
|
293
|
+
`jean/migrations/NNN_<name>.sql`; the runner picks it up automatically.
|
|
294
|
+
Update `tests/test_db.py:test_migrations_apply_to_clean_db` if a new
|
|
295
|
+
table needs explicit assertions.
|
|
296
|
+
- **New required Slack bot scope** → add to
|
|
297
|
+
`slack/identity.py:REQUIRED_BOT_SCOPES` AND to the manifest in
|
|
298
|
+
`jean/setup_guide.py:render_manifest`. Users with existing apps need
|
|
299
|
+
to reinstall the app for new scopes.
|
|
300
|
+
- **New jean CLI command** → register on the `typer` app in
|
|
301
|
+
`jean/cli.py`. Help text becomes visible in `jean --help`.
|
|
302
|
+
- **New MCP tool** → decide DM-only (add to `jean_skills` server in
|
|
303
|
+
`skill_tools.py`) or every-session (add to `jean_io` server in
|
|
304
|
+
`io_tools.py`). Each is registered in `options.py:options_kwargs`.
|
|
305
|
+
- **New OS package needed for a skill** (poppler, libreoffice, etc.) →
|
|
306
|
+
add to the `apt-get install` line in `jean/templates/Dockerfile.tmpl`
|
|
307
|
+
with a comment for why.
|
|
308
|
+
|
|
309
|
+
## What's intentionally NOT built (future work)
|
|
310
|
+
|
|
311
|
+
- Multi-machine / LiteFS — single machine is by design (see invariant 1).
|
|
312
|
+
- HTTP Events Mode for Slack — Socket Mode covers our use cases.
|
|
313
|
+
- Custom session store — the SDK's default disk transcripts + the symlink
|
|
314
|
+
to the Fly volume are enough; rescue-from-Slack covers the edge case.
|
|
315
|
+
- Fast Mode (`speed: "fast"`) — the SDK doesn't expose it yet (`betas`
|
|
316
|
+
field only accepts `context-1m-2025-08-07`). When Anthropic ships it,
|
|
317
|
+
wire a `[claude] speed` config option to it.
|
|
318
|
+
- PyPI publish as `shinkansen-jean` — until then, install from local
|
|
319
|
+
repo (jean-on-jean) or from a git tag.
|
|
320
|
+
- Multi-tenant — one jean install talks to one Slack workspace.
|
|
321
|
+
|
|
322
|
+
## Style notes
|
|
323
|
+
|
|
324
|
+
- Async everywhere; if you need to mix sync/async, prefer async wrappers.
|
|
325
|
+
- Pydantic v2 is the source of truth for config validation.
|
|
326
|
+
- Defensive SDK probing via `getattr`/`type(x).__name__` — the SDK shape
|
|
327
|
+
drifts across versions; don't rely on exact class identity.
|
|
328
|
+
- Tests use `pytest-asyncio` auto mode; no `@pytest.mark.asyncio`
|
|
329
|
+
decorators needed.
|
|
330
|
+
- No `print()` — use `typer.echo`/`typer.secho` for CLI output and
|
|
331
|
+
`logging` for everything else.
|
|
332
|
+
- Templates copied by `jean init` end in `.tmpl` but otherwise carry the
|
|
333
|
+
filename they'll be written as (so `Dockerfile.tmpl` → `Dockerfile`).
|
jean_agent-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shinkansen Finance
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|