---
name: takumi-runner-cicd-sensor-integration
description: "Wire cicd-sensor into a GitHub repository's Actions jobs, or onto a GitLab project's self-managed runner, so their execution traces reach Takumi Runner, and open the pull request (merge request on GitLab). Checks the environment with the helper's `doctor` (gh or glab login and token scopes, git identity, checkout, write access), asks for whatever is missing (repository, jobs), clones when needed, settles the Takumi side with takumictl (the user signs in; the organization and the bot are created when the user has none, with a trust condition per repository), edits the chosen jobs with the helper's `add`, which pins cicd-sensor as the Takumi user guide does, then commits, pushes and opens the PR, and ends by checking one run of the lowest-impact job it changed. Use when asked to set up, add, or integrate cicd-sensor for a repository or its CI jobs, to onboard a repository to Takumi Runner tracing, or to update, troubleshoot or otherwise work on an existing cicd-sensor integration, for which it reads the Takumi user guide and cicd-sensor's own documentation and repository. Arguments: '[<owner>/<repo> ...] [<workflow>:<job> ...] [<bot ID>]' — all optional; missing pieces are asked for one layer at a time: in a checkout the jobs right away, otherwise the repository (and its provider when the name alone does not say), then the jobs; the resulting plan is shown, not asked about."
---

# Set up cicd-sensor in a repository's GitHub Actions jobs

Outcome: a pull request per target repository whose only change is the
cicd-sensor steps (plus `environment` and `permissions`) in the jobs the user
chose, a bot that admits those jobs, one run of an instrumented job read to
its end, and a closing message listing what remains.

The helper `takumi_cicd-sensor_helper` does every edit; Step 0 puts it in
`scripts/` next to this file, and its `--help` is the source of truth for its
commands. This skill does the surroundings: environment, inputs, clone,
account and bot, branch, pull request.

## What this skill downloads

- Its own pages and scripts, version `0.1.0`, from
  `https://shisho.dev/releases/takumi-runner-cicd-sensor-integration/0.1.0/`, as listed
  with their SHA-256 in `manifest.json` there, into the directory holding
  this file (Step 0). Nothing is run before its digest matches.
- For a stable version, whether a newer one exists, from
  `https://shisho.dev/releases/takumi-runner-cicd-sensor-integration-versions.json`.
- `takumictl`, from `https://shisho.dev/releases/takumictl-versions.json`,
  after asking the user (Step 5).
- The Takumi user guide's cicd-sensor page as Markdown,
  `https://shisho.dev/docs/t/runner/features/cicdsensor-integration.md`,
  read by the helper's `list` and `add` for the cicd-sensor-action release
  to pin and for the supported runners (Step 3).
- For tasks beyond the setup, the three pages listed under "Beyond the
  setup" below, read when such a task comes up.

Nothing else.

## Inputs: ask, never guess, one layer at a time

| Input | Where it comes from |
| --- | --- |
| Repository | the current checkout; else the user, plus the provider (GitHub or GitLab) when the name alone does not say |
| Jobs, per repository | `list --json` candidates, chosen by the user |
| Bot ID (`BT` + 26 characters) | the user, or an already-instrumented job of the plan (`bot_id` in `list --json`); otherwise created in Step 5 with `takumictl`, with the **Takumi Runner Trace Sender** role only |
| Credential (self-hosted only) | created by `takumictl` straight into a file only the user can read, when the host change is ready (`reference/self-hosted.md`); it reaches the host's secret channel by path, never through the conversation or a committed file |

Users rarely provide everything up front. Never mix repositories and jobs
in one question. In a checkout the repository is known and the first
question is the jobs (Step 3); otherwise the repository comes first (Step
2), its provider when unclear, then the jobs. The whole plan is shown
before anything is edited (Step 4), without asking for a yes: the pull
request is where the user reviews the change. A job is always named with its
repository, `owner/repo: workflow.yml:job`, in every question, summary, PR
text and report, never bare, so the user can tell which repository a job
belongs to. Never invent a bot ID, an organization ID or a repository name.

Logins and other prompting commands are the user's to run: give the exact
command and let them run it in their own terminal, or through whatever the
agent offers for commands the user runs themselves. Tool installs go through
the package manager after the user agrees, with `sudo` when not root, and a
`sudo` that asks for a password is the user's too. The question that asks
for that agreement says, in one sentence, what the tool is used for here;
the sentences are in `reference/environment-fixes.md`, which also has the
details.

## Step 0: complete the install

`SKILLDIR` is the absolute path of the directory containing this file. Take
it from the way the skill was loaded when the agent exposes it (a variable
it substitutes into skill text, or the path it read this file from);
otherwise find the file and take its directory:

```bash
find "$HOME" -maxdepth 6 -path '*/takumi-runner-cicd-sensor-integration/SKILL.md' 2>/dev/null | head -1
```

Resolve it before any `cd`, then run this program, which needs python3 and
nothing else:

```bash
python3 - "$SKILLDIR" <<'EOF'
import hashlib, json, os, posixpath, sys, tempfile, urllib.request

VERSION = "0.1.0"
TREE = "https://shisho.dev/releases/takumi-runner-cicd-sensor-integration/"
CATALOG = "https://shisho.dev/releases/takumi-runner-cicd-sensor-integration-versions.json"

skill_dir = os.path.abspath(sys.argv[1]) if len(sys.argv) > 1 and sys.argv[1] else ""
if not skill_dir or not os.path.isfile(os.path.join(skill_dir, "SKILL.md")):
    sys.exit(f"SKILLDIR must be the directory holding SKILL.md, got {sys.argv[1:2]!r}")
if os.path.exists(os.path.join(skill_dir, ".dev")):
    print(f"development copy ({skill_dir}/.dev exists): using its files as they are")
    sys.exit(0)


def fetch(url):
    with urllib.request.urlopen(url, timeout=60) as response:
        return response.read()


def digest_of(path):
    digest = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(1 << 16), b""):
            digest.update(chunk)
    return digest.hexdigest()


prefix = f"{TREE}{VERSION}/"
saved = os.path.join(skill_dir, ".manifest.json")  # the manifest the last complete run applied
try:
    raw = fetch(prefix + "manifest.json")
    fetched = True
except Exception as exc:
    unreachable = f"cannot read {prefix}manifest.json ({exc})"
    try:  # the saved manifest stands in only when it is this version's
        with open(saved, "rb") as f:
            raw = f.read()
        if json.loads(raw).get("version") != VERSION:
            raise ValueError("the saved manifest is for another version")
    except Exception:
        sys.exit(f"{unreachable}; this SKILL.md ({VERSION}) cannot complete its install")
    print(f"warning: {unreachable}; checking the installed files against the manifest of the last run")
    fetched = False
manifest = json.loads(raw)
if manifest.get("version") != VERSION:
    sys.exit(f"{prefix}manifest.json is for version {manifest.get('version')!r}, this file is {VERSION}")
artifacts = manifest.get("artifacts")
if not isinstance(artifacts, list) or not all(
    isinstance(a, dict) and all(isinstance(a.get(k), str) for k in ("path", "url", "sha256")) for a in artifacts
):
    sys.exit(f"{prefix}manifest.json lists artifacts in a shape this program does not understand; nothing was installed")
print(f"takumi-runner-cicd-sensor-integration {VERSION}: completing the install in {skill_dir} from {prefix}")
installed = []
for artifact in artifacts:
    path, url, sha256 = artifact["path"], artifact["url"], artifact["sha256"]
    # The manifest came over the network: only plain relative POSIX paths, only this version's tree.
    parts = path.split("/")
    if "\\" in path or path != posixpath.normpath(path) or posixpath.isabs(path) or any(p in ("", "..") or p.startswith(".") for p in parts):
        sys.exit(f"refusing to install {path!r}: not a plain relative path")
    if url != prefix + path:
        sys.exit(f"refusing to fetch {path!r} from {url!r}: not under {prefix}")
    dest = os.path.join(skill_dir, *parts)
    if path == "SKILL.md":  # the file this program is part of: checked, never replaced
        if digest_of(dest) != sha256:
            print(f"warning: {dest} differs from the released {VERSION} (edited, or an incomplete download)")
        continue
    if os.path.isfile(dest) and digest_of(dest) == sha256:
        continue
    try:
        data = fetch(url)
    except Exception as exc:
        sys.exit(f"cannot fetch {url} ({exc}); stopping")
    if hashlib.sha256(data).hexdigest() != sha256:
        sys.exit(f"{url} does not match the digest in the manifest; nothing was installed from it")
    os.makedirs(os.path.dirname(dest), exist_ok=True)
    fd, tmp = tempfile.mkstemp(dir=os.path.dirname(dest), prefix=".installing-")
    try:
        with os.fdopen(fd, "wb") as f:
            f.write(data)
        os.chmod(tmp, 0o755 if path.startswith("scripts/") else 0o644)
        os.replace(tmp, dest)
    finally:
        if os.path.exists(tmp):
            os.unlink(tmp)
    installed.append(path)
if fetched:
    fd, tmp = tempfile.mkstemp(dir=skill_dir, prefix=".installing-")
    with os.fdopen(fd, "wb") as f:
        f.write(raw)
    os.replace(tmp, saved)
    if "-" not in VERSION:  # a stable version says when a newer one exists; a release candidate reads no catalog
        try:
            latest = json.loads(fetch(CATALOG))["latest"]
            if tuple(int(n) for n in latest.split(".")) > tuple(int(n) for n in VERSION.split(".")):
                print(f"a newer version exists: {latest}, at {TREE}{latest}/SKILL.md (download that one file to upgrade)")
        except Exception as exc:
            print(f"warning: could not read {CATALOG} ({exc})")
print(f"installed {len(installed)} file(s): {', '.join(installed) or 'none, everything was in place'}")
EOF
```

It reads this version's `manifest.json` and puts every file listed there
that is missing or differs into `SKILLDIR`, checking each SHA-256 before
writing, and keeps that manifest as `SKILLDIR/.manifest.json`. When the
release host cannot be reached, a complete install verified against the
saved manifest continues with a warning; a missing or changed file stops.
A `.dev` file in `SKILLDIR` marks a development copy, which it leaves
alone. Any failure stops the run: report its message, never guess and
never fetch anything by hand. Then locate the helper:

```bash
bash "$SKILLDIR/scripts/resolve-helper.sh"
```

It prints the path of a runnable helper; use that path as `HELPER` below via
`python3`. Without python3, nothing here can run: install it first (install
rule above).

## Step 1: check the environment

```bash
python3 "$HELPER" doctor --json
```

Read `checks[]`. For every `fail`, apply the remedy from
`reference/environment-fixes.md` (read it now), then run `doctor` again.
Repeat until `ok` is true, with two exceptions. Without a checkout, only
`git_checkout` fails (the checkout-dependent checks are not reported) and
`platform` is unknown (`warn`), so `doctor` checks neither `gh` nor
`glab`: fix the rest, go to Step 2, and come back with `doctor --platform
github` or `--platform gitlab` for each platform in the list, before
cloning. And `repository` failing on access (`viewer_permission` READ or
TRIAGE) is settled by a question, not by retries: the user either gets
write access, after which `doctor` passes, or chooses the fork flow of
Step 7.4, in which case that one `fail` stays and the run continues.
Mention `warn` entries to the user and continue. The fresh-machine traps
(`gh_scopes`, `git_push_auth`) are in the reference.

## Step 2: the repository (layer 1)

The very first thing to settle is whether a checkout is already here:

- `checkout` is not null: that repository is the target and `doctor`'s
  `platform` is its provider. Do not ask about it; go straight to Step 3.
  If the user then names a different repository, follow them: it is on the
  same provider as the checkout unless they say otherwise, and it is cloned
  as below before its jobs are asked.
- `checkout` is null: ask which repository to change (one question, no
  jobs in it). When the answer does not settle the provider (a bare
  `owner/repo`, no GitHub or GitLab word, no URL), ask which it is, GitHub
  or GitLab, then keep narrowing the repository down: `doctor --platform
  <provider>` and fix what it reports (this is where `gh` or `glab` gets
  installed and logged in), then `gh repo view <owner>/<repo>` or `glab repo
  view <group>/<project>`; a name that is not found is reported in one line,
  naming the provider, and asked again, never looked for on the other one.

A repository that is not the current checkout: `gh repo clone <owner>/<repo>`
or `glab repo clone <group>/<project>` into its own directory, `cd` into it,
and run `doctor` there. For every target, bring the default branch up to
date before branching: `git switch <default_branch>` and `git pull
--ff-only`; if the user is on a branch with their own work, ask whether to
build on it instead. Several repositories are handled one at a time, the
same way, each always named with its provider. `default_branch` and
`viewer_permission` come from `doctor`'s `repository`.

## Step 3: the jobs, per repository (layer 2)

```bash
python3 "$HELPER" list --json      # in each repository
```

One question per repository, titled with its name; in a checkout it is the
first question of the run. Candidates are jobs with `"sensor": "no"` on a runner that can
take the sensor. Show them as a short table (`owner/repo: workflow.yml:job`,
runs-on, name) and ask which to instrument (one question that allows several answers).
Jobs on `self-hosted` labels appear in that same question as one extra
option, in plain words: "the self-hosted runner of <owner/repo> (<labels>):
covering it means a change where that runner runs, which affects every
workflow that uses that runner". When the user already named jobs, check
they exist and are candidates in the repository they belong to; explain any
that are not. Skip and mention in one line the jobs marked `native` (Takumi
Hosted Runner traces them), `yes` (already instrumented; note their
`bot_id` for Step 5), `-` (reusable workflow calls) and `?` (inline YAML).

Which runners can run cicd-sensor is the Takumi user guide's call; each
job's `runner_support` is the helper's built-in default for it.
`list --json` carries the guide's supported-pipelines section, verbatim, as
`guide.supported_runners`: read it and hold every candidate's runner labels
against it.

- The guide supports the runner: a candidate, and its PR gets no runner
  note, whatever `runner_support` says. If `add` still refuses it, the
  helper is older than the guide: leave the job out and say so.
- The guide excludes the runner: leave the job out, with the guide's words
  as the reason.
- The guide does not settle it, or `guide.supported_runners` is null (a
  warning says why): `runner_support` decides. `unsupported` jobs (Windows,
  macOS, `ubuntu-slim`, or a matrix with such a leg) are refused by `add`,
  so leave them out and give the `reason`; `unverified` and `expression`
  jobs proceed, and their `reason` goes into the PR notes.

On a GitLab project (`platform` in `list --json`) there is no hosted path:
tagged jobs are `host`, untagged ones `no`, the only option is the
self-managed runner; `reference/self-hosted.md` has the GitLab differences.

## Step 4: show the plan (layer 3)

Before any branch, edit or push, show the whole plan as one table, one row
per `owner/repo: workflow.yml:job` and one row per self-hosted runner
(`owner/repo: self-hosted runner (<labels>)`), and carry on without asking
for a yes: every change of the plan arrives as a pull request, which is
where the user reviews it. If the user corrects the plan, go back to Step 2
(a repository) or Step 3 (a job) and show the table again; never patch the
plan inside a side question.

Hosted jobs then go through Steps 5 to 7, repository by repository. If the
plan includes a self-hosted runner, finish the hosted jobs of every
repository first, then read `reference/self-hosted.md` and follow it: it
opens by explaining the impact and asking who makes the machine change,
before any investigation, and ends in its own PR or a handoff.

## Step 5: account, organization and bot

Every hosted job needs a bot, and every self-hosted environment an API key
of one. Both come from `takumictl`, the Takumi CLI, signed in as the user.
Read `reference/account-and-bot.md` and follow it, in order: install
`takumictl` after asking, when it is missing; the user signs in
(`takumictl auth signin`, a browser device flow, theirs to run) and
registers from that page if they have no Takumi account; the organization is
settled from `takumictl org list` (none: the user names an ID and it is
created; one: confirm it; several: ask); the bot is the one the user gave,
else the one an already-instrumented job of the plan names, else a new one
with only the Takumi Runner Trace Sender role; then a trust condition per
hosted GitHub repository of the plan, with the **bare** repository name. A
refusal is read with `reference/environment-fixes.md` (its `takumictl`
table: no session, bot session, missing role, MFA, taken ID, and the
organization cap). The API key of a self-hosted environment is created
later, in `reference/self-hosted.md`, once the host change is ready.

## Step 6: branch and edit, in each repository of the plan

Read `reference/pr-template.md` for the branch name and the texts.

1. `git switch -c takumi/cicd-sensor` from the updated default branch.
2. For each of the repository's jobs, first `python3 "$HELPER" add <job> <bot ID> --dry-run --json`
   and collect `diff`; show the user a summary of what will change.
   `add` pins cicd-sensor-action to the release the user guide pins
   (`cicd_sensor_action.source` is `guide`), or to its built-in release
   when the guide cannot be read (`built-in`, with a note saying so).
3. Then run the same command without `--dry-run`. A refusal (`ok: false`) is
   final for that job: relay the `error`, continue with the others.
4. Keep every `notes` entry, except a runner-support note for a runner the
   guide supports (Step 3); they become the PR checklist.

## Step 7: commit, push, open the PR, in each repository

1. Stage only the workflow files the helper reported in `workflow`, by path.
   Never `git add -A` or a directory.
2. Commit and push: `git push -u origin takumi/cicd-sensor`. A push that asks
   for a username or is refused for the `workflow` scope means Step 1 was skipped.
3. `gh pr create --base <default_branch>` with the title and body from
   `reference/pr-template.md`.
4. `viewer_permission` READ or TRIAGE (the choice made in Step 1): use the
   fork flow in `reference/environment-fixes.md` instead of pushing to origin.

## Step 8: report, and say who does what next

Give the user, per repository, the PR URL, the jobs changed and the jobs
skipped and why, each as `owner/repo: workflow.yml:job`, the cicd-sensor-action
release pinned and whether it came from the user guide, the bot and
organization used, whether the trust condition was created in Step 5 or
already existed, the path of the helper (`HELPER`) for running its `list`
or `doctor` by hand later, and the follow-ups from `notes` that remain: the
environment caveat on private repositories, and where the first run of each
job shows up with its trace: the Takumi Runner console's jobs page,
`https://cloud.shisho.dev/<organization ID>/runner/jobs` with the
organization of Step 5 filled in. Whenever the console comes up, give that
URL; "open the Takumi Runner console" alone leaves the user to find it.

Then split everything that remains into three labelled groups, each item
with its exact command, never a bare instruction like "recreate the VM":

- **I can do now, say the word**: merge the PR, the run check of Step 9,
  anything `gh` reaches from here.
- **I can do with access**: a rebuild or deploy whose tool is installed and
  logged in where the agent runs; check with `command -v` and the tool's own
  auth status before claiming either way, and when it is missing, name what
  is missing and give the command the user can run instead.
- **Only you**: placing a secret, and anything that needs a login to a
  machine the agent cannot reach.

When the user earlier chose "the agent investigates and prepares", that choice
covers this stage too: offer to carry on and do not stop at the PR while the
next step is within reach.

## Step 9: check one run, the last step

The setup is done when one job has run with the sensor and its steps have
been read, not when the PR is open. In each repository of the plan, on the
PR branch, run `python3 "$HELPER" run-check --json`. It ranks the jobs that
now run cicd-sensor by what a run costs and risks (how it can be started,
and, over the whole workflow since a run executes every job in the file,
steps, matrix legs, secrets, deployment environments and deploy-like steps;
then the job's own `needs` and `if`) and names the cheapest as `pick`; each
candidate's `how` says what starts its run, and its `commands` are the `gh`
commands that start and read it, in order.

Suggest the pick to the user in one sentence with its `reasons`, name the
runner-up, then act on `how`:

- `pull_request`: the run is already underway. Watch it with the commands
  and read the job's steps; no question needed.
- `workflow_dispatch`: ask before dispatching (a run on the user's
  repository, on their minutes), then watch and read.
- `push`: on the default branch the merge has already run it: read that
  run. On the PR branch the run comes with the merge: when the user merges,
  or asks for the merge, read the run on the default branch.
- `none`: say which trigger the workflow waits for and leave the check with
  the user, together with the command that finds the run.

A passing check: the steps in `steps_to_check` (`Issue cicd-sensor manager
token`, `Start cicd-sensor`, `Post Start cicd-sensor`) all conclude
`success`. A failed token step means the bot or the trust condition does
not match the repository (Step 5); a failed start step names its cause in
the step's log, usually the runner (Step 3). Report the run's URL and the
three conclusions, then the console's jobs page URL of Step 8, where the
job appears with its trace; seeing it there is the user's part, the agent
has no view of the console.

One run is the check. Never start every instrumented workflow to prove the
setup: the others run in their own time.

## Beyond the setup: updates, troubleshooting, other cicd-sensor tasks

The steps above are the setup. Asked to update the cicd-sensor release in
jobs or on hosts, to find out why a sensor step fails or a trace does not
appear, or for anything else about a cicd-sensor integration, read first,
then act. The sources, in order:

- The Takumi user guide's cicd-sensor page,
  https://shisho.dev/docs/t/runner/features/cicdsensor-integration/
  (Japanese: https://shisho.dev/docs/ja/t/runner/features/cicdsensor-integration/):
  what Takumi supports, the cicd-sensor-action release to pin, the
  supported runners, and where traces appear. The helper reads this page
  for `list` and `add`, so `list --json` reports its current pin as
  `cicd_sensor_action`, next to the `sensor` state of every job.
- The cicd-sensor documentation, https://cicd-sensor.github.io/index.html:
  how the sensor works, its configuration, the action and the host agent,
  and its troubleshooting pages.
- The cicd-sensor repository, https://github.com/cicd-sensor/cicd-sensor:
  the releases and their notes, and the issues, for behaviour the
  documentation does not cover.

The rules below hold for these tasks as for the setup: the helper does the
edits it has a command for, every change arrives as a pull request on its
own branch, and a release is never guessed, since the guide names the one
to pin and the repository the ones that exist.

## Rules

- The helper's refusals are deliberate (Takumi Hosted Runner jobs, already
  instrumented jobs, reusable workflow calls, inline YAML, `read-all`
  permissions). Relay the reason; never work around them by editing YAML.
- Never push to the default branch, never force-push, never include changes
  the user did not ask for.
- A failing `doctor` check is fixed in the environment, not by bypassing the
  helper.
- A question that asks to install a tool says in one sentence what the
  tool is used for here (`reference/environment-fixes.md`, "What each tool
  is for"): the user agrees to a purpose, not to a package name.
- `takumictl`'s refusals are the API's: fix the cause from the reference,
  never retry with another account's session or a guessed ID.
