grigo@grigosserver:~/to_services$ ls
3x            ai-tools   funkwhale  jenkins  kokoro  minecraft  netdata      openclaw-data     qbtorrent  spotify.cred      taiga                xtts
LoraTester    aiChatBot  gitea      jitsi    lidarr  navidrome  octo-fiesta  openclaw-gateway  server     spotizerr         taiga-manual-broken
acore-docker  fishtts    jellyfin   juice    metube  neko       open-meteo   project-agent     spotdown   stable-10888.zip  voice-server
grigo@grigosserver:~/to_services$ ls ai-tools/
taiga-tasker
grigo@grigosserver:~/to_services$ ls ai-tools/taiga-tasker/
create_taiga_task.py  .env                  .venv/                
grigo@grigosserver:~/to_services$ ls ai-tools/taiga-tasker/create_taiga_task.py 
ai-tools/taiga-tasker/create_taiga_task.py
grigo@grigosserver:~/to_services$ cat ai-tools/taiga-tasker/create_taiga_task.py 
#!/usr/bin/env python3
import json
import os
import re
import sys
from typing import Any
from pathlib import Path
import requests
from dotenv import load_dotenv

SCRIPT_DIR = Path(__file__).resolve().parent
load_dotenv(SCRIPT_DIR / ".env")

TAIGA_BASE_URL = os.getenv("TAIGA_BASE_URL", "http://127.0.0.1:9000").rstrip("/")
TAIGA_USERNAME = os.getenv("TAIGA_USERNAME")
TAIGA_PASSWORD = os.getenv("TAIGA_PASSWORD")
TAIGA_DEFAULT_PROJECT_ID = os.getenv("TAIGA_DEFAULT_PROJECT_ID")

AIMTR_BASE_URL = os.getenv("AIMTR_BASE_URL", "https://aimtr.wellflow.dev/v1").rstrip("/")
AIMTR_API_KEY = os.getenv("AIMTR_API_KEY")
AIMTR_MODEL = os.getenv("AIMTR_MODEL", "claude-haiku-4.5")


def require_env() -> None:
    missing = []
    for key, value in {
        "TAIGA_USERNAME": TAIGA_USERNAME,
        "TAIGA_PASSWORD": TAIGA_PASSWORD,
        "TAIGA_DEFAULT_PROJECT_ID": TAIGA_DEFAULT_PROJECT_ID,
        "AIMTR_API_KEY": AIMTR_API_KEY,
    }.items():
        if not value:
            missing.append(key)

    if missing:
        raise SystemExit(f"Missing env vars in .env: {', '.join(missing)}")


def strip_markdown_json(text: str) -> str:
    text = text.strip()

    fenced = re.search(r"```(?:json)?\s*(.*?)\s*```", text, re.DOTALL | re.IGNORECASE)
    if fenced:
        return fenced.group(1).strip()

    return text


def aimtr_make_task(raw_text: str) -> dict[str, Any]:
    system_prompt = """
Ты технический ассистент для Taiga.
Преобразуй сырое описание задачи в строгий JSON.
Отвечай только JSON, без markdown и пояснений.

Схема:
{
  "title": "короткое название задачи",
  "description": "понятное описание задачи",
  "type": "Story",
  "priority": "low|normal|high",
  "tags": ["tag1", "tag2"],
  "acceptance_criteria": [
    "критерий приемки 1",
    "критерий приемки 2"
  ],
  "children": [
    {
      "title": "название подзадачи",
      "description": "описание подзадачи",
      "type": "Task",
      "priority": "low|normal|high"
    }
  ],
  "questions": [
    "уточняющий вопрос, если данных не хватает"
  ]
}

Правила:
- Пиши на русском.
- Не придумывай бизнес-ограничения, которых нет в запросе.
- Если данных мало, добавь вопросы в questions.
- Acceptance criteria должны быть проверяемыми.
- children должны быть техническими подзадачами.
""".strip()

    response = requests.post(
        f"{AIMTR_BASE_URL}/chat/completions",
        headers={
            "Authorization": f"Bearer {AIMTR_API_KEY}",
            "Content-Type": "application/json",
        },
        json={
            "model": AIMTR_MODEL,
            "messages": [
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": raw_text},
            ],
            "temperature": 0.2,
        },
        timeout=60,
    )
    response.raise_for_status()

    content = response.json()["choices"][0]["message"]["content"]
    clean = strip_markdown_json(content)

    try:
        return json.loads(clean)
    except json.JSONDecodeError as exc:
        print("LLM returned invalid JSON:")
        print(content)
        raise SystemExit(exc)


def taiga_auth() -> str:
    response = requests.post(
        f"{TAIGA_BASE_URL}/api/v1/auth",
        json={
            "type": "normal",
            "username": TAIGA_USERNAME,
            "password": TAIGA_PASSWORD,
        },
        timeout=15,
    )
    response.raise_for_status()
    return response.json()["auth_token"]


def taiga_headers(token: str) -> dict[str, str]:
    return {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
    }


def format_story_description(task: dict[str, Any], raw_text: str) -> str:
    lines = []

    lines.append(task.get("description") or raw_text)

    acceptance = task.get("acceptance_criteria") or []
    if acceptance:
        lines.append("")
        lines.append("## Acceptance criteria")
        for item in acceptance:
            lines.append(f"- {item}")

    questions = task.get("questions") or []
    if questions:
        lines.append("")
        lines.append("## Вопросы / уточнения")
        for item in questions:
            lines.append(f"- {item}")

    tags = task.get("tags") or []
    if tags:
        lines.append("")
        lines.append("## Теги")
        lines.append(", ".join(tags))

    lines.append("")
    lines.append("## Исходное описание")
    lines.append(raw_text)

    return "\n".join(lines).strip()


def create_userstory(token: str, project_id: int, task: dict[str, Any], raw_text: str) -> dict[str, Any]:
    subject = (task.get("title") or raw_text).strip()[:500]
    description = format_story_description(task, raw_text)

    response = requests.post(
        f"{TAIGA_BASE_URL}/api/v1/userstories",
        headers=taiga_headers(token),
        json={
            "project": project_id,
            "subject": subject,
            "description": description,
        },
        timeout=15,
    )
    response.raise_for_status()
    return response.json()


def create_subtask(token: str, project_id: int, userstory_id: int, child: dict[str, Any]) -> dict[str, Any]:
    subject = (child.get("title") or "Подзадача").strip()[:500]
    description = (child.get("description") or "").strip()

    response = requests.post(
        f"{TAIGA_BASE_URL}/api/v1/tasks",
        headers=taiga_headers(token),
        json={
            "project": project_id,
            "user_story": userstory_id,
            "subject": subject,
            "description": description,
        },
        timeout=15,
    )
    response.raise_for_status()
    return response.json()


def main() -> None:
    require_env()

    if len(sys.argv) < 2:
        raise SystemExit('Usage: ./create_taiga_task.py "Описание задачи" [PROJECT_ID]')

    raw_text = sys.argv[1].strip()
    project_id = int(sys.argv[2]) if len(sys.argv) > 2 else int(TAIGA_DEFAULT_PROJECT_ID)

    print("Generating structured task with AIMTR...")
    structured = aimtr_make_task(raw_text)

    print("Authenticating in Taiga...")
    token = taiga_auth()

    print("Creating user story...")
    story = create_userstory(token, project_id, structured, raw_text)

    created_tasks = []
    for child in structured.get("children", []):
        if isinstance(child, dict):
            created_tasks.append(create_subtask(token, project_id, story["id"], child))

    print("")
    print(f"Created user story: #{story['ref']} / id={story['id']}")
    print(f"Subject: {story['subject']}")
    print(f"Subtasks created: {len(created_tasks)}")

    for task in created_tasks:
        print(f"- #{task.get('ref')} {task.get('subject')}")


if __name__ == "__main__":
    main()
grigo@grigosserver:~/to_services$ ls open
openclaw-data/    openclaw-gateway/ open-meteo/       
grigo@grigosserver:~/to_services$ ls openclaw-data/
auth/      config/    workspace/ 
grigo@grigosserver:~/to_services$ ls openclaw-data/workspace/
advanced_integration.c     final-checklist.sh         main_full.c                PINOUT_WIRING.md           SOUL.md                    STEPPER_WIRING.md
AGENTS.md                  fix-acore-docker.sh        memory/                    PROJECT_SUMMARY.sh         START_HERE.md              test_stepper.c
auto-setup.sh              fix-docker-compose.sh      MEMORY.md                  QUICK_REFERENCE.md         stepper_advanced.c         TOOLS.md
catalog_images.py          .git/                      motor_advanced.py          quick-start.sh             stepper_examples.py        TUNING_GUIDE.md
CMakeLists.txt             HEARTBEAT.md               motor_integration.py       README_COMPLETE.md         stepper_motor.c            USER.md
C_SDK_GUIDE.md             IDENTITY.md                motor_web_server.py        rk3566-yc-p6602.dts        stepper_motor_control.py   verify-installation.sh
diagnostics.c              install-dependencies.sh    .openclaw/                 setup_stepper_project.sh   stepper_motor.h            viewer.html
FILES_MANIFEST.md          main_basic.c               organize_stepper_files.sh  skills/                    STEPPER_QUICK_START.md     viewer_server.py
grigo@grigosserver:~/to_services$ ls openclaw-data/workspace/skills/project_agent/SKILL.md 
openclaw-data/workspace/skills/project_agent/SKILL.md
grigo@grigosserver:~/to_services$ cat openclaw-data/workspace/skills/project_agent/SKILL.md 
---
name: project_agent
description: Manage project-agent projects through API, add/register/update/list/sync projects, create Taiga tasks from natural language, analyze repository code, and choose the next Pomodoro action. Use this instead of MEMORY.md when the user asks to add, register, update, sync, list, or show projects.
---

# Project Agent

Use this skill when the user asks to:
- add, register, update, list, show, or sync projects in project-agent;
- create a development task in Taiga;
- formalize a feature request, bug, fix, or idea;
- decompose work into subtasks;
- analyze repository code before creating a task;
- choose what to work on next;
- pick the best task for a Pomodoro;
- report progress after a Pomodoro.

The project-agent service is available at:

http://project-agent:8787

## Available projects

Project list is dynamic. To see current projects, call:

    curl -sS http://project-agent:8787/projects

Known projects may include:
- AISHub
- AIsMas-Web-Service
- AndroidAisMap
- PrivateTest
- Testing
- ClawSetUp

Default project: AISHub.

## Manage project-agent projects

Use this section when the user asks to:
- add a project;
- register a project;
- create a project in project-agent;
- update a project;
- sync a project;
- list projects;
- show project config.

Important:
- Do not write project definitions to MEMORY.md.
- Do not store project definitions in notes, memory, markdown files, or local summaries.
- Project definitions must be stored in project-agent via API.
- Do not say a project was added unless the POST /projects API returned "ok": true.
- If the user asks to add a project without sync, do not call /sync.
- Only call /projects/PROJECT_NAME/sync if the user explicitly says the Gitea repository already exists or asks to sync.
- If required fields are missing, ask only for the missing fields.

Required fields for adding a project:
- project name;
- kind: home or work;
- Taiga project id;
- Gitea repo in OWNER/REPO format;
- default branch. Use main if not specified.

To list projects, call:

    curl -sS http://project-agent:8787/projects

To show one project, call:

    curl -sS http://project-agent:8787/projects/PROJECT_NAME

To add a project, call exactly:

    curl -sS -X POST http://project-agent:8787/projects \
      -H "Content-Type: application/json" \
      -d '{"name":"PROJECT_NAME","kind":"home","ai_tags":["home"],"taiga_project_id":9,"taiga_slug":"PROJECT_SLUG","gitea_repo":"OWNER/REPO","repo_url":"ssh://git@host.docker.internal:222/OWNER/REPO.git","repo_path":"/repos/PROJECT_NAME","default_branch":"main"}'

To update a project, call exactly:

    curl -sS -X PUT http://project-agent:8787/projects/PROJECT_NAME \
      -H "Content-Type: application/json" \
      -d '{"name":"PROJECT_NAME","kind":"home","ai_tags":["home"],"taiga_project_id":9,"taiga_slug":"PROJECT_SLUG","gitea_repo":"OWNER/REPO","repo_url":"ssh://git@host.docker.internal:222/OWNER/REPO.git","repo_path":"/repos/PROJECT_NAME","default_branch":"main"}'

To delete a project, call only when the user explicitly asks to delete or remove it:

    curl -sS -X DELETE http://project-agent:8787/projects/PROJECT_NAME

To sync a project only when requested, call:

    curl -sS -X POST http://project-agent:8787/projects/PROJECT_NAME/sync

After adding or updating a project:
- call GET /projects/PROJECT_NAME;
- show the returned config;
- mention whether sync was skipped or executed;
- if sync was skipped, say that the project is registered but the repository was not cloned.

Expected response format for project add or update:

Проект сохранён в project-agent:
- Name: PROJECT_NAME
- Kind: home/work
- Taiga ID: ID
- Gitea repo: OWNER/REPO
- Repo path: /repos/PROJECT_NAME
- Default branch: main
- Sync: skipped/executed

Do not mention MEMORY.md.

## Create code-aware Taiga task

When the user asks to create, formalize, decompose, or register a Taiga task with repository/code context, call exactly this command:

    curl -sS -X POST http://project-agent:8787/tasks/from-code \
      -H "Content-Type: application/json" \
      -d '{"project":"PROJECT_NAME","text":"USER_TASK_TEXT"}'

Use this for phrases like:
- "создай задачу";
- "оформи хотелку";
- "заведи bug";
- "разложи на подзадачи";
- "создай задачу с учетом кода";
- "вот список ошибок и хотелок";
- "добавь это в Taiga".

Replace:
- PROJECT_NAME with one of the available projects. Use AISHub if the project is not specified.
- USER_TASK_TEXT with the user's full task description.

After the call, always include these fields from the JSON response:
- Taiga story: story.ref, story.id, story.subject
- Gitea issue: gitea_issue.url
- Suggested branch: gitea_issue.branch
- Labels: gitea_issue.labels
- Created subtasks: subtasks[].ref and subtasks[].subject
- Useful code notes: structured.code_notes
- Blocking questions only: structured.questions

Do not omit Gitea issue, branch, or labels if they exist in the JSON response.

Mandatory response format for /tasks/from-code:

Создано:
- Taiga: #{{story.ref}} — {{story.subject}}
- Gitea: {{gitea_issue.url}}
- Рекомендуемая ветка: {{gitea_issue.branch}}
- Labels: {{gitea_issue.labels}}

Подзадачи:
{{for each item in subtasks}}
- #{{ref}} — {{subject}}

Заметки по коду:
{{for each item in structured.code_notes}}
- {{item}}

Вопросы/блокеры:
{{only include structured.questions that block implementation}}

Rules:
- Use the exact URL from gitea_issue.url.
- Use the exact branch from gitea_issue.branch.
- Use the exact labels from gitea_issue.labels.
- Do not say "branch created" unless the JSON explicitly contains "branch_created": true.
- Do not claim a subtask count unless you count the subtasks array exactly.
- Do not rewrite the response into a generic project-management summary.

## Pick next Pomodoro action

When the user asks what to work on next, what is the current priority, or what task to do during a Pomodoro, call exactly this command:

    curl -sS -X POST http://project-agent:8787/next-action \
      -H "Content-Type: application/json" \
      -d '{"project":"PROJECT_NAME","minutes":25,"energy":"normal","notes":"USER_CONTEXT"}'

Use this for phrases like:
- "Я завожу помидор, что делать?";
- "Что сейчас в приоритете?";
- "Какую задачу взять на 25 минут?";
- "Что делать дальше по AISHub?";
- "Выбери следующую задачу";
- "Помоги выбрать задачу на сейчас".

Replace:
- PROJECT_NAME with one of the available projects. Use AISHub if the project is not specified.
- minutes with the user's Pomodoro duration. Use 25 if not specified.
- energy with low, normal, or high. Use normal if not specified.
- USER_CONTEXT with the user's current context, constraints, and notes.

Important:
- Return exactly ONE recommended task.
- Start the answer with the task ref and title.
- Do not offer alternative tasks unless the selected task is blocked.
- Do not say "or you can do X".
- Do not ask "what do you choose?" after recommending a Pomodoro task.
- Give a concrete execution plan for the requested timebox.
- Give a clear definition of done.
- Mention open questions only if they block the selected task.
- Use only POST /next-action.
- Do not call /tasks.
- Do not call /next-task.

After the call, format the response like this:

Бери #REF — "TITLE".

Почему сейчас:
...

План на 25 минут:
1. ...
2. ...
3. ...

Definition of done:
- ...
- ...

Блокер/вопрос:
...

If there is no blocker, omit the blocker section.

## Finish Pomodoro / report progress

When the user reports what they did after a Pomodoro, call exactly this command:

    curl -sS -X POST http://project-agent:8787/pomodoro/finish \
      -H "Content-Type: application/json" \
      -d '{"project":"PROJECT_NAME","task_ref":TASK_REF,"minutes":25,"result":"USER_REPORT","done":false}'

Use this for phrases like:
- "я закончил помидор";
- "отчет по задаче";
- "сделал X, не успел Y";
- "закрой задачу";
- "готово";
- "задача #17 сделана".

Rules:
- If the user says the task is finished, set done=true.
- If the user only reports partial progress, set done=false.
- If task ref is missing, ask for the task number.
- After the call, summarize what was recorded in Taiga.
- Then return the next recommended Pomodoro action from the response.

## Safety

- Use only the project-agent API.
- Do not access Taiga, Gitea, Jenkins, or repositories directly.
- Do not ask the user for Taiga URL, token, username, or password.
- Do not print secrets, .env contents, tokens, passwords, SSH keys, or Authorization headers.
- Do not delete, push, merge, deploy, or modify repositories.
- Do not run arbitrary shell commands unrelated to project-agent.
- Do not write project definitions to MEMORY.md.
grigo@grigosserver:~/to_services$ 