| 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}] |
Developer Skill — Git, GitLab & GitHub
Work in git repositories, manage merge requests on GitLab and pull requests on GitHub. Uses bare clones + git worktrees for branch isolation.
Environment Variables
| 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):
- Namespace verification: Before creating any MR or PR, extract the resolved namespace/owner from the API response and confirm it matches the intended target. If the user said "submit to
cynium/istota", verify the project resolves to cynium, not some other namespace. Abort and ask the user if there is any mismatch.
- Response verification: After creating an MR/PR, parse the API response to extract the URL and ID. If the response contains an error, treat it as failure. Then query the open MR/PR list to confirm it actually exists before reporting success.
- No live source editing: Never edit files under production installation paths (e.g.,
/srv/app/*/src/). All source changes must go through worktrees in $DEVELOPER_REPOS_DIR and be submitted as MRs/PRs.
Directory Layout
$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
- Bare clones go in
<namespace>/<project>.git/
- Worktrees are siblings:
<namespace>/<project>--<branch-slug>/
Cloning a Repository
First time — create a bare clone:
BARE_DIR="$DEVELOPER_REPOS_DIR/namespace/project.git"
if [ ! -d "$BARE_DIR" ]; then
mkdir -p "$(dirname "$BARE_DIR")"
git clone --bare "$GITLAB_URL/namespace/project.git" "$BARE_DIR"
git -C "$BARE_DIR" config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
git -C "$BARE_DIR" fetch origin
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"
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
git -C "$BARE_DIR" fetch origin
Reading current source from a bare clone
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:
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.
Creating a Worktree for Development
TASK_ID="$ISTOTA_TASK_ID"
SLUG="add-auth"
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}"
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.
Development Process
1. Understand Before Changing
Before writing any code, read enough of the codebase to understand the existing patterns:
- Read CLAUDE.md, AGENTS.md, and any
.claude/rules/ files in the repo — these contain project-specific conventions and architecture notes that must be followed.
- Read existing code that does something similar to what you're implementing. Match the naming conventions, error handling patterns, env var names, and module structure already in use. Never guess — grep for how other modules solve the same problem.
- Check how the module integrates with the rest of the system. If you're adding a new skill/plugin/module, look at how existing ones are wired in (env vars, config, imports, tests). Copy the established pattern exactly.
2. Write Tests First (TDD)
If the project has a test suite (check for tests/, pytest.ini, pyproject.toml [tool.pytest], jest.config, etc.):
- Write failing tests first that cover the expected behavior, edge cases, and error paths.
- Run the tests to confirm they fail.
- Implement the feature.
- Run tests again and iterate until all pass.
- Also run the full test suite to catch regressions — not just the new tests.
If the project has linters or type checkers configured (ruff, mypy, eslint, tsc --noEmit, etc.), run those too before committing.
3. Edit and Verify
4. Commit
$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:
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'])")
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
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]
}")
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"
$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'"
GitHub: Pushing and Creating a Pull Request
Push the branch:
cd "$WORK_DIR"
git push origin "$BRANCH"
Create PR via GitHub API:
OWNER="myorg"
REPO="project"
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.\"
}")
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\"]}"
Follow-Up Work on Existing MRs/PRs
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"
git add -A
git commit -m "Address review feedback: add input validation"
git push origin HEAD
GitLab: Listing and Merging 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)]"
$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}'.
GitHub: Listing and Merging PRs
OWNER="myorg"
REPO="project"
$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)]"
$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".
Cleanup After Merge
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"
GitLab API Quick Reference
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 |
GitHub API Quick Reference
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)
Error Handling