developer
Git repository management, GitLab merge requests, and GitHub pull requests
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Menú
Git repository management, GitLab merge requests, and GitHub pull requests
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Basado en la clasificación ocupacional SOC
Calendar operations with CalDAV
Location tracking, place recognition, visit history, and calendar attendance
Persistent memory writes — USER.md (behavioral) and the knowledge graph (facts).
Accounting operations (ledger, invoicing, transactions, work log, investment portfolio) — runs in-process via the vendored money package
Send a push notification to the user's configured ntfy device(s). One-way (bot to phone), no reply channel.
Briefing formatting guidelines for chat messages
| name | developer |
| triggers | ["git","gitlab","github","repo","repository","commit","branch","merge request","MR","pull request","PR","code review","develop","worktree","clone"] |
| description | Git repository management, GitLab merge requests, and GitHub pull requests |
| env | [{"var":"DEVELOPER_REPOS_DIR","from":"config","config_path":"developer.repos_dir","when":["developer.enabled","developer.repos_dir"]},{"var":"GITLAB_URL","from":"config","config_path":"developer.gitlab_url","when":["developer.enabled","developer.repos_dir"]},{"var":"GITHUB_URL","from":"config","config_path":"developer.github_url","when":["developer.enabled","developer.repos_dir"]},{"var":"GITLAB_DEFAULT_NAMESPACE","from":"config","config_path":"developer.gitlab_default_namespace","when":["developer.enabled","developer.gitlab_default_namespace"]},{"var":"GITLAB_REVIEWER_ID","from":"config","config_path":"developer.gitlab_reviewer_id","when":["developer.enabled","developer.gitlab_reviewer_id"]},{"var":"GITHUB_DEFAULT_OWNER","from":"config","config_path":"developer.github_default_owner","when":["developer.enabled","developer.github_default_owner"]},{"var":"GITHUB_REVIEWER","from":"config","config_path":"developer.github_reviewer","when":["developer.enabled","developer.github_reviewer"]},{"var":"DEVELOPER_AUTHOR_CREDIT","from":"config","config_path":"developer.author_credit","when":["developer.enabled","developer.author_credit"]},{"var":"GITLAB_TOKEN","from":"config","config_path":"developer.gitlab_token","when":["developer.enabled","developer.repos_dir","developer.gitlab_token"],"sensitive":true},{"var":"GITHUB_TOKEN","from":"config","config_path":"developer.github_token","when":["developer.enabled","developer.repos_dir","developer.github_token"],"sensitive":true}] |
Work in git repositories, manage merge requests on GitLab and pull requests on GitHub. Uses bare clones + git worktrees for branch isolation.
| Variable | Description |
|---|---|
DEVELOPER_REPOS_DIR | Base directory for repo clones and worktrees |
GITLAB_URL | GitLab instance URL (e.g., https://gitlab.com) |
GITLAB_DEFAULT_NAMESPACE | Default GitLab namespace (user/group) for resolving short repo names |
GITLAB_REVIEWER_ID | GitLab user ID to assign as reviewer on new merge requests |
GITLAB_API_CMD | Pre-authenticated wrapper script for GitLab API calls |
GITHUB_URL | GitHub instance URL (e.g., https://github.com) |
GITHUB_DEFAULT_OWNER | Default GitHub org/user for resolving short repo names |
GITHUB_REVIEWER | GitHub username to request as PR reviewer |
GITHUB_API_CMD | Pre-authenticated wrapper script for GitHub API calls |
DEVELOPER_AUTHOR_CREDIT | Optional text appended to every commit message (e.g., Co-Authored-By: ...) |
Git credentials are configured automatically for both platforms — clone and push work without manual authentication.
Namespace resolution: When the user gives a short repo name (e.g., "nebula" instead of "namespace/nebula"), use $GITLAB_DEFAULT_NAMESPACE or $GITHUB_DEFAULT_OWNER as the default namespace/owner depending on the platform. Always confirm the resolved path exists via the API before cloning.
Security: Tokens are embedded in helper scripts and never exposed as environment variables. Do NOT attempt to read or extract credentials from helper scripts. Use $GITLAB_API_CMD / $GITHUB_API_CMD for API calls and plain git commands for repository operations.
Pre-submission checks (mandatory before every MR/PR):
cynium/istota", verify the project resolves to cynium, not some other namespace. Abort and ask the user if there is any mismatch./srv/app/*/src/). All source changes must go through worktrees in $DEVELOPER_REPOS_DIR and be submitted as MRs/PRs.$DEVELOPER_REPOS_DIR/
├── namespace/project.git/ # bare clone
├── namespace/project--istota-42-add-auth/ # worktree for task 42
└── namespace/project--istota-55-fix-bug/ # worktree for task 55
<namespace>/<project>.git/<namespace>/<project>--<branch-slug>/First time — create a bare clone:
BARE_DIR="$DEVELOPER_REPOS_DIR/namespace/project.git"
if [ ! -d "$BARE_DIR" ]; then
mkdir -p "$(dirname "$BARE_DIR")"
# Use $GITLAB_URL or $GITHUB_URL depending on where the repo lives
git clone --bare "$GITLAB_URL/namespace/project.git" "$BARE_DIR"
# Configure fetch to track remote branches under refs/remotes/origin/*
git -C "$BARE_DIR" config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
git -C "$BARE_DIR" fetch origin
# Delete the clone-day local heads and repoint HEAD at the remote default.
# WHY (ISSUE-125): `git clone --bare` populates refs/heads/* once, at clone
# time, and the remote-tracking refspec above never updates them again — so
# a local `main`/`master` stays frozen at clone day while origin/main moves
# on. `git show main:db.py` then silently returns clone-day source. Deleting
# the fossils turns that silent-wrong into a loud `unknown revision`: you
# can't act on stale bytes you can't read. Worktree creation is unaffected —
# it branches from `origin/$DEFAULT_BRANCH` (below), not a local head.
DEFAULT_BRANCH=$(git -C "$BARE_DIR" remote show origin | sed -n 's/.*HEAD branch: //p')
git -C "$BARE_DIR" symbolic-ref HEAD "refs/remotes/origin/$DEFAULT_BRANCH"
# Skip any head currently checked out by a worktree (an istota/<task> branch);
# only the unused clone-day main/master get dropped.
CHECKED_OUT=$(git -C "$BARE_DIR" worktree list --porcelain | sed -n 's/^branch refs\/heads\///p')
for ref in $(git -C "$BARE_DIR" for-each-ref --format='%(refname:short)' refs/heads/); do
echo "$CHECKED_OUT" | grep -qx "$ref" || git -C "$BARE_DIR" branch -D "$ref"
done
fi
# Always fetch latest
git -C "$BARE_DIR" fetch origin
Invariant: in a bare clone, never name a local branch — always origin/<branch>
or origin/HEAD. A local main/master is a clone-day fossil (deleted by the
setup above, but the habit still bites on an older clone). To read the live tree
in one fetch-then-read step that can't point at a stale ref, use:
# dev-show <BARE_DIR> <path> — current source from origin/HEAD, always fetched.
git -C "$BARE_DIR" fetch -q origin && git -C "$BARE_DIR" show origin/HEAD:"$path"
Use this (or git -C "$BARE_DIR" log origin/HEAD, git -C "$BARE_DIR" show origin/main:<path>) for any hand-rolled verification read. Never git show main:<path> / git log master against a bare clone.
TASK_ID="$ISTOTA_TASK_ID"
SLUG="add-auth" # short description, lowercase, hyphens
BRANCH="{BOT_DIR}/${TASK_ID}-${SLUG}"
BARE_DIR="$DEVELOPER_REPOS_DIR/namespace/project.git"
WORK_DIR="$DEVELOPER_REPOS_DIR/namespace/project--{BOT_DIR}-${TASK_ID}-${SLUG}"
# Create branch from latest main (or master — check which exists)
git -C "$BARE_DIR" fetch origin
DEFAULT_BRANCH=$(git -C "$BARE_DIR" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||' || echo "main")
git -C "$BARE_DIR" worktree add -b "$BRANCH" "$WORK_DIR" "origin/$DEFAULT_BRANCH"
All work happens inside $WORK_DIR.
Before writing any code, read enough of the codebase to understand the existing patterns:
.claude/rules/ files in the repo — these contain project-specific conventions and architecture notes that must be followed.If the project has a test suite (check for tests/, pytest.ini, pyproject.toml [tool.pytest], jest.config, etc.):
If the project has linters or type checkers configured (ruff, mypy, eslint, tsc --noEmit, etc.), run those too before committing.
cd "$WORK_DIR"
# Common patterns:
make test # Makefile
pytest # Python
npm test # Node.js
go test ./... # Go
cd "$WORK_DIR"
git add -A
# If $DEVELOPER_AUTHOR_CREDIT is set, append it after a blank line
CREDIT="${DEVELOPER_AUTHOR_CREDIT:+
$DEVELOPER_AUTHOR_CREDIT}" git commit -m "Add user authentication middleware
Implements JWT-based auth with refresh token support. Closes #123${CREDIT}"
- **Commit style**: Never include LLM/AI attribution in commit messages (no `Generated by` or similar). If `$DEVELOPER_AUTHOR_CREDIT` is set, append it after a blank line at the end of every commit message. Otherwise, write commits without any trailer.
### Common Mistakes to Avoid
- **Wrong env var names**: Always grep for how existing env vars are named and set. Don't invent new conventions.
- **Hardcoded paths or user-specific values**: Use env vars and config — never hardcode usernames, server paths, or workspace names.
- **Stale metadata**: If you change a module's purpose, update all descriptions, docstrings, and config manifests to match.
- **Missing dependency wiring**: Adding a new package to `pyproject.toml` / `package.json` isn't enough — also run the install command (`uv sync`, `npm install`) and commit the lockfile.
- **Not reading existing patterns**: The single most common source of bugs. Five minutes reading existing code saves hours of debugging.
## GitLab: Pushing and Creating a Merge Request
Push the branch (git credentials are configured automatically):
```bash
cd "$WORK_DIR"
git push origin "$BRANCH"
Create MR via GitLab API:
# Get project ID from path
PROJECT_PATH="namespace/project"
ENCODED_PATH=$(echo "$PROJECT_PATH" | sed 's|/|%2F|g')
PROJECT_INFO=$($GITLAB_API_CMD GET "/api/v4/projects/$ENCODED_PATH")
PROJECT_ID=$(echo "$PROJECT_INFO" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
# REQUIRED: Verify the resolved namespace matches the intended target.
# Extract the namespace from the project info and confirm it is correct
# before creating any MR. Abort and ask the user if it doesn't match.
RESOLVED_NS=$(echo "$PROJECT_INFO" | python3 -c "import sys,json; print(json.load(sys.stdin)['path_with_namespace'].split('/')[0])")
if [ "$RESOLVED_NS" != "namespace" ]; then
echo "ERROR: Resolved namespace '$RESOLVED_NS' does not match expected 'namespace'. Aborting MR creation."
exit 1
fi
# Create merge request (assign configured reviewer)
MR_RESPONSE=$($GITLAB_API_CMD POST "/api/v4/projects/$PROJECT_ID/merge_requests" \
--header "Content-Type: application/json" \
--data "{
\"source_branch\": \"$BRANCH\",
\"target_branch\": \"$DEFAULT_BRANCH\",
\"title\": \"Add user authentication\",
\"description\": \"Implements JWT auth.\\n\\nCreated by istota task $TASK_ID.\",
\"remove_source_branch\": true,
\"reviewer_ids\": [$GITLAB_REVIEWER_ID]
}")
# REQUIRED: Verify the MR was actually created. Parse the response for
# web_url and iid. If the response contains "error" or "message" fields
# instead, treat it as a failure.
MR_URL=$(echo "$MR_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('web_url',''))")
MR_IID=$(echo "$MR_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('iid',''))")
if [ -z "$MR_URL" ] || [ -z "$MR_IID" ]; then
echo "ERROR: MR creation failed. Response: $MR_RESPONSE"
exit 1
fi
echo "MR created: !$MR_IID — $MR_URL"
# Verify the MR appears in the open MRs list
$GITLAB_API_CMD GET "/api/v4/projects/$PROJECT_ID/merge_requests?state=opened" \
| python3 -c "import sys,json; mrs=json.load(sys.stdin); match=[m for m in mrs if m['iid']==$MR_IID]; assert match, 'MR !$MR_IID not found in open MRs'"
Push the branch:
cd "$WORK_DIR"
git push origin "$BRANCH"
Create PR via GitHub API:
OWNER="myorg" # or $GITHUB_DEFAULT_OWNER
REPO="project"
# REQUIRED: Verify the owner/repo resolves to the intended target before creating a PR.
REPO_INFO=$($GITHUB_API_CMD GET "/repos/$OWNER/$REPO")
RESOLVED_OWNER=$(echo "$REPO_INFO" | python3 -c "import sys,json; print(json.load(sys.stdin)['owner']['login'])")
if [ "$RESOLVED_OWNER" != "$OWNER" ]; then
echo "ERROR: Resolved owner '$RESOLVED_OWNER' does not match expected '$OWNER'. Aborting PR creation."
exit 1
fi
PR_RESPONSE=$($GITHUB_API_CMD POST "/repos/$OWNER/$REPO/pulls" \
--header "Content-Type: application/json" \
--data "{
\"head\": \"$BRANCH\",
\"base\": \"$DEFAULT_BRANCH\",
\"title\": \"Add user authentication\",
\"body\": \"Implements JWT auth.\\n\\nCreated by istota task $TASK_ID.\"
}")
# REQUIRED: Verify the PR was actually created. Parse the response for
# html_url and number. If missing, treat as failure.
PR_URL=$(echo "$PR_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('html_url',''))")
PR_NUMBER=$(echo "$PR_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('number',''))")
if [ -z "$PR_URL" ] || [ -z "$PR_NUMBER" ]; then
echo "ERROR: PR creation failed. Response: $PR_RESPONSE"
exit 1
fi
echo "PR created: #$PR_NUMBER — $PR_URL"
Request a reviewer:
$GITHUB_API_CMD POST "/repos/$OWNER/$REPO/pulls/$PR_NUMBER/reviews" \
--header "Content-Type: application/json" \
--data "{\"reviewers\": [\"$GITHUB_REVIEWER\"]}"
To push additional commits to an open MR/PR, reuse the existing worktree:
WORK_DIR="$DEVELOPER_REPOS_DIR/namespace/project--istota-42-add-auth"
cd "$WORK_DIR"
# Make changes, commit, push
git add -A
git commit -m "Address review feedback: add input validation"
git push origin HEAD
# List open MRs
$GITLAB_API_CMD GET "/api/v4/projects/$PROJECT_ID/merge_requests?state=opened" \
| python3 -c "import sys,json; [print(f'!{mr[\"iid\"]} {mr[\"title\"]} ({mr[\"web_url\"]})') for mr in json.load(sys.stdin)]"
# Merge an MR
$GITLAB_API_CMD PUT "/api/v4/projects/$PROJECT_ID/merge_requests/$MR_IID/merge"
Options: add "squash": true or "should_remove_source_branch": true via --data '{"squash": true}'.
OWNER="myorg"
REPO="project"
# List open PRs
$GITHUB_API_CMD GET "/repos/$OWNER/$REPO/pulls?state=open" \
| python3 -c "import sys,json; [print(f'#{pr[\"number\"]} {pr[\"title\"]} ({pr[\"html_url\"]})') for pr in json.load(sys.stdin)]"
# Merge a PR
$GITHUB_API_CMD PUT "/repos/$OWNER/$REPO/pulls/$PR_NUMBER/merge" \
--header "Content-Type: application/json" \
--data '{"merge_method": "squash"}'
Merge methods: "merge", "squash", or "rebase".
BARE_DIR="$DEVELOPER_REPOS_DIR/namespace/project.git"
WORK_DIR="$DEVELOPER_REPOS_DIR/namespace/project--istota-42-add-auth"
git -C "$BARE_DIR" worktree remove "$WORK_DIR"
git -C "$BARE_DIR" branch -d "istota/42-add-auth"
Use $GITLAB_API_CMD METHOD ENDPOINT [extra curl args] for all API calls.
The API wrapper enforces an endpoint allowlist — only the operations below are permitted. Deleting and admin operations are blocked.
| Action | Method | Endpoint |
|---|---|---|
| Get project by path | GET | /api/v4/projects/:encoded_path |
| List branches | GET | /api/v4/projects/:id/repository/branches |
| List open MRs | GET | /api/v4/projects/:id/merge_requests?state=opened |
| Get single MR | GET | /api/v4/projects/:id/merge_requests/:iid |
| Create MR | POST | /api/v4/projects/:id/merge_requests |
| Merge MR | PUT | /api/v4/projects/:id/merge_requests/:iid/merge |
| Add MR comment | POST | /api/v4/projects/:id/merge_requests/:iid/notes |
| Create issue | POST | /api/v4/projects/:id/issues |
| Add issue comment | POST | /api/v4/projects/:id/issues/:iid/notes |
| Look up user by username | GET | /api/v4/users?username=:name |
Use $GITHUB_API_CMD METHOD ENDPOINT [extra curl args] for all API calls.
The API wrapper enforces an endpoint allowlist — only the operations below are permitted. Deleting and admin operations are blocked.
| Action | Method | Endpoint |
|---|---|---|
| Get repo | GET | /repos/:owner/:repo |
| List branches | GET | /repos/:owner/:repo/branches |
| List open PRs | GET | /repos/:owner/:repo/pulls?state=open |
| Get single PR | GET | /repos/:owner/:repo/pulls/:number |
| Create PR | POST | /repos/:owner/:repo/pulls |
| Merge PR | PUT | /repos/:owner/:repo/pulls/:number/merge |
| Update PR | PATCH | /repos/:owner/:repo/pulls/:number |
| Add PR comment | POST | /repos/:owner/:repo/pulls/:number/comments |
| Request PR review | POST | /repos/:owner/:repo/pulls/:number/reviews |
| Create issue | POST | /repos/:owner/:repo/issues |
| Add issue comment | POST | /repos/:owner/:repo/issues/:number/comments |
| Update issue | PATCH | /repos/:owner/:repo/issues/:number |
| Search code | GET | /search/code?q=... |
| Look up user | GET | /users/:username |
| List org repos | GET | /orgs/:org/repos |
Important: When piping API wrapper output, always redirect to a temp file first, then read:
$GITHUB_API_CMD GET "/repos/$OWNER/$REPO" > /tmp/result.json
DEFAULT_BRANCH=$(python3 -c "import sys,json; print(json.load(sys.stdin)['default_branch'])" < /tmp/result.json)
cd "$WORK_DIR"
git fetch origin "$DEFAULT_BRANCH"
git rebase "origin/$DEFAULT_BRANCH"
# Resolve conflicts if any, then force-push
git push origin "$BRANCH" --force-with-lease