Taiga stories
There is no browser control. Uploads go through the Taiga REST API with
scripts/taiga_stories.py, which uses only the Python standard library. The same
tooling works for any Taiga project, which is configured in .env.
Setup (once per repository)
- Copy
.env.exampleto.envat the repository root and setTAIGA_PROJECT_URLto the project link, e.g.https://taiga.example.com/project/my-project. - Optionally set
STORIES_DIR(defaultstories).
.env holds no secrets and is ignored by git. The token never goes in it.
Writing stories
Each story is one .md file in stories/, following story-template.md.
-
File name: lowercase, no accents, words with hyphens (
automatic-slug.md). -
First line:
# Story: <title>. The text after "Story:" becomes the Taiga title. For bug fixes, useFix — <what was happening>. -
Header:
**Project:**,**Branch:**,**Version:**,**Date:**and**Points:**, one per line. -
Points: there is no default. Before saving a new story to
stories/, estimate its points with thestory-pointsskill (commit history ofCODE_REPO_PATH) and have the user confirm them. Write the value in**Points:**(**Pontos:**in Portuguese). It must be a name in the project's point scale (1, 2, 3, 5, 8, 13, 21). -
Sections, in this order:
## User story(As / I want / so that, each bold on its own line),## Description(prose for the product owner, no code jargon),## Acceptance criteria(- [ ]checklist,[x]for what is done and tested),## Tasksand## Deployment notes(or## Noteswhen it is not about deployment). -
Tasks: group them under
### Backend,### Frontend,### Testsand### Documentation, in that order, leaving out empty groups. Each task isN. **Title**with its description indented on the next line. Numbering is continuous across groups. -
Language: write the whole story, headings included, in the language of the user's request. A request in Portuguese gets a story in Portuguese. Both heading sets are parsed:
English Portuguese # Story:# História:**Project:**/**Branch:**/**Version:**/**Date:**/**Points:****Projeto:**/**Branch:**/**Versão:**/**Período:**/**Pontos:**## User story(As / I want / so that)## História de usuário(Como / quero / para)## Description## Descrição## Acceptance criteria## Critérios de aceite## Tasks(### Backend,### Frontend,### Tests,### Documentation)## Tarefas(### Backend,### Frontend,### Testes,### Documentação)## Deployment notes/## Notes## Observações para implantação/## ObservaçõesFix — ...Correção — ... -
Firewall-friendly text: in descriptions, avoid a
;right beforecode(use a full stop), literal tags like<script>(write "the script tag"), and chains ofClass::methodnext to routes. The script works around these, but the text gets worse.
Check the format (no network, no token):
python -X utf8 .claude/skills/taiga-stories/scripts/taiga_stories.py validate [file.md ...]
Fix errors. A task without description is an acceptable warning. A missing **Points:**
is only a warning here, but it blocks the upload.
Uploading to Taiga
Always ask first, in the chat, every time
Use AskUserQuestion, or a plain question if it is unavailable. Do not reuse earlier answers:
- Destination: which sprint (exact name, e.g.
Sprint 34) or backlog. - Session token: in the open Taiga tab, F12 > Console >
localStorage.getItem("token"). Use it only as an inline environment variable for the command. Never write it to.envor to any file. - Missing points: if any story to upload has no
**Points:**(the--dry-runshowsMISSING), estimate those with thestory-pointsskill and have the user confirm, listing them by title. Write each value to the story's**Points:**line (**Pontos:**in Portuguese), right after**Date:**/**Período:**. Stories that already have points are not estimated again. The upload stops before creating anything while any story lacks a valid value.
If .env is missing, also ask for the Taiga project link and create .env from
.env.example.
Steps
S=.claude/skills/taiga-stories/scripts/taiga_stories.py
python -X utf8 $S validate
python -X utf8 $S upload --sprint "Sprint 34" --dry-run # or --backlog
TAIGA_TOKEN='<token>' python -X utf8 $S upload --sprint "Sprint 34"
Pass file names at the end to upload only some stories. Then report to the user how many stories and tasks were created, plus any texts the script rewrote (and any task whose description was dropped).
Defaults: the token owner is the assignee of stories and tasks, statuses are the project
defaults, and points come from each story's **Points:** field, on the project's
computable role.
The script:
- skips stories whose title already exists in the project. Use
--forceonly if the user asks; - resumes an interrupted run without duplicating (state in
stories/.taiga_state.json). If it stops, just run it again; - rewrites texts blocked by the firewall and lists them at the end.
Do not chain other creating commands with ; after a step that can fail. A failure
halfway through can create duplicates.
Lessons learned
- Cloudflare 403 with an HTML body ("Just a moment...") means the WAF blocked the text,
not a permission problem. The script retries with plainer variants. If a task ends up
with no description, rewrite it as plain prose (no backticks, no
::) and PATCH it, sending the currentversion. - 403 with JSON
PermissionDeniedis a real permission error. Regular members may be unable to delete user stories (tasks are fine), so avoid duplicates. If one happens, reuse the story with PATCH (subject, description, points, milestone, assigned_to,version) instead of deleting it. - Points go in
points: {"<computable role id>": <point id>}, and the point id is looked up by its name in the project scale. - On Windows, Python does not see Git Bash's
/tmp. Use Windows paths, and run Python with-X utf8so accents print correctly.