Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,10 @@ openclaw/workspace/.openclaw/
services/linkedin/chromadb/
services/linkedin/cookies/
services/linkedin/posts.json

# Runtime ecosystem map — real container names/ports; this repo is PUBLIC.
# Bind-mounted from disk (see docker-compose.yml), so it never needs to be tracked.
# Track ironclaw/workspace/TOOLS.md.example instead.
ironclaw/workspace/TOOLS.md
# Internal build roadmap — absolute local paths into private repos, auth routes, token names
ironclaw/workspace/todo.md
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ clawbot/
│ ├── .mcp.json # MCP server connections (Drupal, Apify, etc.)
│ ├── IDENTITY.md # Agent name, persona, avatar
│ ├── SOUL.md # Agent values and behavioural guidelines
│ ├── TOOLS.md # Available tools and how to use them
│ ├── TOOLS.md # Available tools and how to use them — gitignored (real container
│ │ # names); copy from TOOLS.md.example
│ ├── USER.md # Info about the user (fill in as needed)
│ └── skills/ # Custom skill playbooks (SKILL.md per skill)
├── docker-compose.yml # Main Compose file
Expand Down
33 changes: 20 additions & 13 deletions ironclaw/workspace/TOOLS.md → ironclaw/workspace/TOOLS.md.example
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
# TOOLS.md - Ecosystem Map

> **Template.** Copy to `TOOLS.md` and replace the placeholder container names and ports with
> your actual ones: `cp TOOLS.md.example TOOLS.md`
>
> `TOOLS.md` is gitignored on purpose — this repo is public, and the real ecosystem map is
> internal infrastructure detail. The workspace is bind-mounted, so the agent reads your local
> copy directly; a `docker compose restart ironclaw` picks up changes.

## MCP Servers

| Server | Container | Port | Tools | Use For |
|---|---|---|---|---|
| **`gateway`** | mcpserver-gateway | 8100 | 29 | **Unified entry point — all tools via single auth** |
| **`site-files`** | clawbot-openclaw-1 (local) | — | ~10 | **Read/write Hugo source at `/site`** |
| `ragchat` | ragchat-nginx-1 | 8080 | 21 | CMS content, RAG knowledge base, Drupal |
| `seobot` | seo-tools-mcp | 3001 | 3 | Website SEO audits, crawl reports |
| `osintbot` | osintbot-mcp | 8200 | — | LinkedIn (Apify)/OSINT research, prospect intel |
| `sales` | sales-assistant-assistant-1 | 8000 | 3 | Product Q&A, knowledge base search |
| **`gateway`** | mcp-gateway | 8100 | 29 | **Unified entry point — all tools via single auth** |
| **`site-files`** | <agent-container> (local) | — | ~10 | **Read/write Hugo source at `/site`** |
| `ragchat` | ragchat | 8080 | 21 | CMS content, RAG knowledge base, Drupal |
| `seobot` | seo-mcp | 3001 | 3 | Website SEO audits, crawl reports |
| `osintbot` | osint-mcp | 8200 | — | LinkedIn (Apify)/OSINT research, prospect intel |
| `sales` | sales-api | 8000 | 3 | Product Q&A, knowledge base search |

### Gateway tool naming

Expand All @@ -36,15 +43,15 @@ Tools routed through the gateway use `{server}__{tool}` format, e.g.:

## Docker Network

All containers on `nginx-proxy` bridge network.
OpenClaw container: `clawbot-openclaw-1`.
All containers on a shared bridge network (see `docker-compose.yml`).
OpenClaw container: `<agent-container>`.
Reach services by container name over the network.

## Key URLs

- OpenClaw dashboard: http://127.0.0.1:18790
- **MCP Gateway: http://mcpserver-gateway:8100** (internal) / http://127.0.0.1:8100 (host)
- RagChat: http://ragchat-nginx-1:8080
- Sales assistant: http://sales-assistant-assistant-1:8000
- SEO crawler MCP: http://seo-tools-mcp:3001
- OSINT MCP: http://osintbot-mcp:8200
- **MCP Gateway: http://mcp-gateway:8100** (internal) / http://127.0.0.1:8100 (host)
- RagChat: http://ragchat:8080
- Sales assistant: http://sales-api:8000
- SEO crawler MCP: http://seo-mcp:3001
- OSINT MCP: http://osint-mcp:8200
161 changes: 161 additions & 0 deletions ironclaw/workspace/skills/redmine-time-tracking/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
---
name: redmine-time-tracking
description: >-
Log the time an agent spends on a project to the self-hosted Redmine (CRM).
Resolve the working directory to a Redmine project, find or create an issue for
the task, then file a time entry under the "AI Agent" activity. Run automatically
after ~15+ minutes of substantive work on a project, or when asked to "log my
time" / "track this work" / "record hours in Redmine".
---

# Redmine Time Tracking

Log agent work time to Redmine. Data model: **Project → Issue → Time entry**.

> **ClawBot copy.** The canonical version lives in Luděk's monorepo at
> `ai/skills/redmine-time-tracking/SKILL.md`; keep the two in sync when the procedure
> changes. This copy is deliberately narrower — `clawbot` is a public repository, so the
> stack's endpoints and the admin procedure stay in the canonical copy only. You don't need
> them: you reach Redmine through the gateway's `redmine__*` tools.

## Environment

- **Tools:** `redmine__<tool>` through the MCP gateway.
- **Session start time:** the `WORK SESSION` line you appended to `MEMORY.md`.
- **Clock:** `date -u +%Y-%m-%dT%H:%M:%SZ`; if you have no shell, estimate conservatively
from the conversation.

## When to run

- **Automatically**, after roughly 15 minutes or more of real effort on one project
(research, coding, audits, multi-step tasks). This is internal bookkeeping on Luděk's
own infrastructure — you do **not** need to ask permission first.
- On explicit request ("log my time", "track this").
- On an end-of-day flush: process every open work session that hasn't been logged.

Skip trivial work (a one-line fix, a quick question) — it's not worth an issue.

## Procedure

### 1. Resolve the Redmine project

Map the working directory to a Redmine project **identifier**:

| Subproject / area | Redmine identifier |
|---|---|
| `customers/plymovent`, `plymovent` | `plymovent` |
| `ragchat` | `ragchat` |
| `osintbot` | `osintbot` |
| `sales-assistant` | `sales-assistant` |
| `mcpserver`, mcpserver.cz site | `mcpserver-cz` |
| `seo-tools` | `seo-tools` |
| `geo` | `geo` |
| `startupfactory` | `startupfactory` |
| `drupal-lama` | `drupal-lama` |
| `ludekkvapil` / `/site` (ludekkvapil.cz) | `ludekkvapil-cz` |
| `clawbot` — you, your workspace and your skills | `clawbot` |
| `customers/drupal-cz`, `drupalcz` (drupal.cz) | `drupal-cz` |
| `ai`, `redmine`, `docs`, `scripts`, `ops-proxy`, `observability`, `reception`, infra, monorepo root | `lkv` |

Call `list_redmine_projects` and resolve the mapped identifier to its **numeric `id`** —
`create_redmine_issue` and the time-entry tools want the integer project id, not the
identifier string. If you can't tell which project the work belongs to, **ask** — don't
guess, don't invent a project.

### 2. Compute hours

`elapsed = now − session start`, in hours, **rounded to the nearest 0.25, minimum 0.25**.
Count actual working time, not wall-clock time you were idle.

### 3. Resolve the "AI Agent" activity id

Call `list_time_entry_activities`, find the entry named **"AI Agent"**, keep its `id`.
If it's missing, the one-time host-side setup hasn't been run — tell Luděk and stop.

### 4. Find or create the issue (one issue per task)

- Call `search_redmine_issues` (`query` = a few words from the task) or `list_redmine_issues`
scoped to the project for an **open** issue that already matches this task (you may be
resuming work). If found, use its `id`.
- Otherwise `create_redmine_issue`:
- `project_id`: the **numeric id** from step 1
- `subject`: a short, specific task title (e.g. *"Fix CLS on blog list pages"*, not *"work"*)
- `description`: 1–3 sentences — what and why
- `fields`: an object with `tracker_id` (from `list_project_trackers` for this project —
use the one that matches the project name), `status_id` for **"In progress"** (look it
up with `list_redmine_issue_statuses`), and `assigned_to_id` = your own user id
(`get_current_user` → `mcp-agent`).

`status_id` and `parent_issue_id` are usually **dropped silently** — see
[Permission limits of `mcp-agent`](#permission-limits-of-mcp-agent). The issue is still
created; it just lands in the project's default status and unparented. Don't retry, and
don't treat it as a failure of the whole step.

### 5. File the time entry

`manage_time_entry`:
- `action`: `"create"`
- `issue_id`: from step 4
- `hours`: from step 2
- `activity_id`: the "AI Agent" id from step 3
- `spent_on`: today (`YYYY-MM-DD`)
- `comments`: one factual sentence on what you did, suffixed with your agent tag —
`… (ClawBot)`. Max 255 chars, no filler.
- Do **not** pass `user_id` — it defaults to the current MCP user (`mcp-agent`).

### 6. Close the task if it's done

If the task is complete and produced its deliverable, `update_redmine_issue` to set the
status to **"Done"** and `done_ratio` 100 (pass these in `fields`). Leave it "In progress"
if there's follow-up work.

Then **read the issue back** and check the status actually changed — the update reports
success even when the status was ignored. If it didn't land, say so in your reply and leave
the status alone; don't loop on it.

### 7. Record that it's logged

Change that `WORK SESSION` line's trailing state in `MEMORY.md` to `logged <time-entry-id>`.

## Verify

`list_time_entries` scoped to the project + today — confirm the entry shows user
`mcp-agent`, activity `AI Agent`, and the agent-tag comment suffix.

Human-readable check, for Luděk: the project's **Time entries** tab, and its **Report**
grouped by Activity — "AI Agent" hours are a separate line from human "Development" /
"Design" hours.

## Permission limits of `mcp-agent`

The `mcp-agent` account holds the role **"Agent"**, which can create and edit issues and
file time entries, but **not** manage subtasks or move issues through the workflow. Redmine
does not reject the disallowed attributes — it drops them and still answers `200`/`204`, so
a write can look like it succeeded while nothing changed. Confirmed on 2026-09-12 for:

| Attribute | What happens |
|---|---|
| `status_id` (on create and update) | ignored; issue keeps the project's default status |
| `parent_issue_id` | ignored; the issue stays top-level |

What works: `subject`, `description`, `tracker_id`, `assigned_to_id`, `done_ratio`, notes,
and all time-entry writes.

So: **after any status or parent write, read the issue back**. If it didn't take, report it
in your reply rather than retrying or escalating on your own.

Anything the Agent role can't do — nesting subtasks, forcing a status, re-running the
setup — needs admin access you don't have. Say what you couldn't do and stop; the host-side
copy of this playbook carries the admin procedure. Lifting the limitation permanently is
Luděk's call: Administration → Roles → Agent → *Manage subtasks*, plus the workflow
transitions for the Agent role per tracker.

## Notes

- Never log to a project that isn't in the map above.
- If issue creation fails with a tracker or permission error, the setup script needs to be
(re-)run for that project — report it, don't retry blindly.
- The MCP server authenticates as `mcp-agent` (legacy mode) — you send no credentials.
- The server wraps free-text fields in returned JSON in `<insecure-content-…>` markers as a
prompt-injection guard. That's a display wrapper on reads only — the stored value is
clean; don't include those markers when you write text back.
Loading