Metadata-Version: 2.5
Name: get-fsa-training-done
Version: 2.6.0
Summary: One agent skill, and the CLI behind it, for getting FSA training done: programmes, teaching material and assessments. Installs into Claude Code, Codex, GitHub Copilot and Antigravity.
Project-URL: Homepage, https://github.com/phuongbv00/get-fsa-training-done
Project-URL: Issues, https://github.com/phuongbv00/get-fsa-training-done/issues
Author: Phuong Bui
License: MIT
License-File: LICENSE
Keywords: agent-skill,antigravity,assessment,claude-code,codex,curriculum,github-copilot,grading
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: Education
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Education :: Testing
Requires-Python: >=3.12
Description-Content-Type: text/markdown

# get-fsa-training-done

One agent skill, and the CLI behind it, for getting FSA training done:
designing a programme, writing its teaching material, and designing and grading
its assessments. It installs into [Claude Code](https://claude.com/claude-code),
[Codex](https://developers.openai.com/codex) (the app, the CLI and the IDE extension),
[GitHub Copilot](https://docs.github.com/en/copilot) (the Copilot CLI and agent
mode in VS Code) and [Antigravity](https://antigravity.google) (the app and the IDE).

The [landing page](https://phuongbv00.github.io/get-fsa-training-done/) shows
how it works, in real files; its source is in `docs/`.

The skill has three features, each with its own CLI namespace:

| Namespace | What it does | Docs |
| --- | --- | --- |
| `program` | design a training programme and export its workbooks | [README](src/get_fsa_training_done/features/program/README.md) |
| `material` | write slide decks on the FSA template, one lecture note per module and lab guides in HTML, and export them to `.pptx` and `.pdf` | [README](src/get_fsa_training_done/features/material/README.md) |
| `assessment` | design and grade quizzes, assignments, exams and capstones | [README](src/get_fsa_training_done/features/assessment/README.md) |

They divide one job. A programme's session plan names, in its
`Training Materials` column, the file that serves each session — so `program`
declares that a slot exists and what it weighs, `material` writes the slides,
note or lab guide, and `assessment` writes the quiz, brief or exam. No feature
authors another's artifact, and each checks its own half of that manifest.

The skill is built from **tasks** — one step each, such as writing a rubric,
rendering a brief, or merging a retake — composed into **workflows**. Ask for a
whole job and the agent follows the workflow; ask for one step and it runs that
task on its own.

The skill is **stateless**. It assumes nothing about the directory layout it
was installed into and never scans a project on a hunch — it asks for its
inputs first, echoes back the resolved paths, and waits for a go-ahead.

## Install

```bash
pip install get-fsa-training-done
```

This gives you the `gftd` command, which every example below uses; it is also
installed under its full name, `get-fsa-training-done`.
It needs Python 3.12+.

Then install the skill into your agent:

```bash
gftd install
```

In a terminal this asks which agents to install to (↑↓ to move, space to
select, enter to confirm) and whether globally or for this project, shows an
installation summary, and writes nothing until you answer yes. Nothing starts
ticked.

The skill is written **once**, to `.agents/skills/get-fsa-training-done` —
`~/.agents/skills` globally, `./.agents/skills` for a project. Codex and GitHub
Copilot read that folder directly, and Antigravity does for a project; every
other agent gets a symlink to it, so one update reaches all of them. Where the
system refuses symlinks (Windows without Developer Mode) that agent gets its
own copy instead.

| Agent | Global | Project |
| --- | --- | --- |
| Codex | `~/.agents/skills/…` | `./.agents/skills/…` |
| GitHub Copilot | `~/.agents/skills/…` | `./.agents/skills/…` |
| Claude Code | `~/.claude/skills/…` → link | `./.claude/skills/…` → link |
| Antigravity | `~/.gemini/config/skills/…` → link | `./.agents/skills/…` |

Releases before this one wrote Codex to `~/.codex/skills` and `./.codex/skills`
and Copilot to `~/.copilot/skills` and `./.github/skills`. `gftd update` moves
such an install to `.agents/skills`, so the agent does not load the skill twice.

| Command | Effect |
| --- | --- |
| `gftd install` | ask which agents and where, then install |
| `gftd install --platform all --scope user` | every agent, globally, without asking |
| `gftd install --platform claude --scope project` | Claude Code, for this project, without asking |
| `gftd update` | upgrade the CLI from PyPI, then every install it finds, preserving files you edited |
| `gftd uninstall` | show every install and what goes, then remove all of it on yes |
| `gftd status` | what is installed where, and whether it drifted |
| `gftd doctor` | check this machine for the skill's prerequisites |

Give `--platform` or `--scope` and `install` asks nothing; outside a terminal
(CI, a pipe) it installs for Claude Code globally. `--yes` skips the questions.
`uninstall` is all or nothing — the agents share one copy, so it cannot be
taken from just one of them — and outside a terminal it needs `--yes`.

Add `--dry-run` to any of `install`, `update`, or `uninstall` to see the exact
file-by-file plan without touching anything.

`gftd update` first upgrades the package itself to the latest PyPI release —
through pip, pipx or `uv tool`, whichever installed it — then updates the skill
in every agent and scope that holds an install (`--platform`/`--scope` narrow
that). `--no-self-update`, `--offline` or
`GET_FSA_TRAINING_DONE_NO_SELF_UPDATE=1` skip PyPI; an editable checkout is
never upgraded, only reported.

## Dependencies

Installing the package pulls nothing: `dependencies = []` is deliberate, so the
lifecycle commands run on a bare interpreter. The libraries the worker commands
need live in the **managed virtualenv** instead — declared once in
`src/get_fsa_training_done/lifecycle/envmgr/requirements/core.txt`:

| Library | Used by |
| --- | --- |
| `xhtml2pdf` | `assessment render`, for the brief PDF |
| `py7zr` | `assessment grade preprocess`, for `.7z` submissions |

`gftd install` builds that environment, so the first install
needs a network and everything after it does not — which is the property that
matters on an exam machine.

Two external tools are optional, and `doctor` reports each without failing for
its absence:

- **A `.rar` extractor** (`unar`, `7z` or `bsdtar`). `.rar` is proprietary and
  has no pure-Python reader; every other archive format is read in-process.
- **Docker**, for `assessment sandbox`: running a seed script, a JUnit suite or
  a pytest run that is not ours — an exam's supplied files, or a submission —
  in a disposable container with no network.

Rendering embeds its own fonts (see
[`features/common/render/fonts/`](src/get_fsa_training_done/features/common/render/fonts)), so a
Vietnamese brief comes out identical on every machine.

## Layout

```
src/get_fsa_training_done/
  cli.py  skill.py          the CLI, and the one skill it installs
  lifecycle/                install, update, uninstall, status, doctor, env
  features/                 program, material, assessment (+ common)
  payload/get-fsa-training-done/
    SKILL.md                the router: workflows and tasks per feature
    references/<feature>/   overview, tasks/, workflows/, generated references
    references/common/      writing style, Vietnamese conventions, shared tasks
```

## Development

```bash
pip install -e .
pytest
ruff check . && ruff format --check .
python scripts/sync_version.py --check
for generator in scripts/*/gen_*.py; do python "$generator" --check; done
```

`src/get_fsa_training_done/__about__.py` holds the canonical version;
`sync_version.py` propagates it to the payload's `VERSION`.

### Testing the skill from source

`gftd update` installs the payload of whichever package `gftd` runs from, so
point `gftd` at the checkout and update:

```bash
pip install -e .   # replaces a PyPI install of the package
gftd update        # copies src/.../payload/ into every install it finds
```

An editable checkout is never upgraded from PyPI, only reported, and the worker
commands run the checkout's code too. The payload is copied, not linked: run
`gftd update` again after each payload change (`--dry-run` shows the plan,
`--force` overwrites files edited in place, keeping `*.bak`).

To go back to a released version:

```bash
pip uninstall -y get-fsa-training-done   # drops the editable .pth
pip install get-fsa-training-done        # or ==<version> to pin one
gftd update                              # syncs the installs to that payload
```

Add `--no-self-update` to that last `update` when you pinned an older version,
or it upgrades to the latest release. `gftd --version` and `gftd status`
confirm what is running and whether the installs match it.

## Releasing

A release is a pushed `v<version>` tag; nothing else publishes. Ordinary
commits stay on the current version.

```bash
# 1. bump the canonical version, then propagate it
$EDITOR src/get_fsa_training_done/__about__.py      # __version__ = "X.Y.Z"
python scripts/sync_version.py

# 2. run every gate in "Development" above — CI runs the same ones

# 3. commit, tag, push the commit and the tag together
git commit -am "Release X.Y.Z"
git tag -a vX.Y.Z -m "vX.Y.Z"
git push origin main vX.Y.Z
```

The tag triggers `.github/workflows/release.yml`:

1. **guard** — refuses the tag unless it equals `v` + `__about__.py`'s
   version, and re-runs the version and generated-file gates.
2. **pypi** — builds, checks the wheel carries the skill payload, and
   publishes through PyPI Trusted Publishing (environment `release`; no token).
3. **github-release** — creates the GitHub release with generated notes.

The landing page reads the latest version from PyPI, so it needs no edit.
Users pick the release up with `gftd update`, which upgrades the package from
PyPI and then every installed skill.

If the guard fails, the tag is wrong, not the workflow: delete it
(`git push origin :refs/tags/vX.Y.Z && git tag -d vX.Y.Z`), fix, and tag
again. Once PyPI has accepted a version it can never be re-uploaded — ship a
fix as the next patch version instead.

## Licence

MIT
