# viber/scripts - the plugin-level bundled scripts

This directory holds the scripts more than one viber skill or agent calls: the config parser, the run lifecycle (`plan-path.sh`, `plan-index.sh`, `commit-task.sh`, `archive-run.sh`, `run-clock.sh`), the switch fragment selector, the skill part printer and the GitHub and browser wrappers. A script used by one skill alone lives under that skill's `scripts/`, and the hook scripts live under `hooks/scripts/`.

## Terms

- Run scripts: the four bash scripts of the run lifecycle, `plan-path.sh`, `plan-index.sh`, `commit-task.sh` and `archive-run.sh`, all `set -euo pipefail`, each refusal a distinct exit code.
- GitHub wrappers: the `#!/bin/sh` scripts `issue-facts.sh`, `issue-templates.sh`, `issue-create.sh`, `post-comment.sh`, `pr-facts.sh`, `pr-create.sh`, `pr-threads.sh`, `pr-reply.sh` and `qa-comment.sh`. Each prints a fixed `KEY=value` block on stdout only after `gh` succeeded and its output parsed (self-verifying), and one `ERROR <script>: ...` line on stderr otherwise; the bash scripts report on stderr as `error: ...`.

## Relationships

- Callers: `config.sh` is preloaded by `implementor`, `planner`, `e2e` and run by `skills/extension/scripts/extension.sh` and `hooks/scripts/plan-gate.sh`; `run-clock.sh` by `implementor` alone; `switch-text.sh` by `implementor`, `planner`, `intent`, `fixer`, `triage` and `prototype`; `skill-part.sh` by `implementor` alone (its `close` part); `plan-path.sh` by `planner`, `implementor`, `intent` and `fixer` (`--start`/`--checkout`) and `e2e` (no argument, the latest run); `plan-index.sh` by `planner` (validate), `implementor` (`--split`), `task-reviewer` (`--extra`, through its `scope` line) and `closeout`; `commit-task.sh` by `implementor`, `e2e` (`--e2e`) and `closeout` (`--outside`); `archive-run.sh` by the `closeout` agent alone.
- GitHub callers: `issue-facts.sh` by `intent`, `fixer`, `triage`, `prototype`; `issue-templates.sh` and `issue-create.sh` by `issue-create`, `intent`, `fixer`; `post-comment.sh` by `intent`, `triage`, `prototype`; `pr-facts.sh` and `pr-create.sh` by `pr-create`; `pr-threads.sh` and `pr-reply.sh` by `pr-feedback`; `qa-comment.sh` by `pr-create` and the `implementor` close (`qa.true.md`); `open-page.sh` by `setup`, `help`, `prototype`; `check-playwright.sh` by `e2e`.
- Sibling calls inside the directory: `skill-part.sh` runs `switch-text.sh` once per fragment block of the part; `switch-text.sh`, `issue-templates.sh` and `pr-facts.sh` run `bash "$here/config.sh"`; `run-branch.sh` runs `config.sh --branching` and `sh issue-facts.sh` (for the issue type); `plan-path.sh` sources `run-branch.sh`.

## Contracts

- `run-branch.sh` is a library: sourced by `plan-path.sh`, never invoked, mode 100644 in the index, no `set` line of its own. Its functions return 6 with the reason on stderr and HEAD, index and tree untouched; `plan-path.sh` turns that into its own exit 6 (run branch could not be set).
- Exit codes of the run scripts are part of their contract, each documented in the header: `plan-path.sh` 2 argv, 3 no plan, 4 bad `--into` target, 5 copy failed, 6 run branch, 7 unusable `attachments:` map (checked on every `--land` before the branch step; a bad `--into` key still wins; a name is one path component, extension optional, its source a file or a directory copied recursively); `plan-index.sh` 2 unusable plan or argv (an unknown `--extra` id included), 3 no task blocks, 4 broken task contract; `commit-task.sh` 2 bad arguments (`--repaired` on a task not done included), 3 no such task, 4 no change (or a bad `--landed`), 5 staging or commit failed with `status.md` restored; `archive-run.sh` 2 argv outside `docs/<runs>/`, 3 destination exists, 4 run unfinished, 5 a git step failed.
- `archive-run.sh` takes the run directory exactly as `plan-path.sh` printed its `path:` line minus `/plan.md`, and refuses (never corrects) any path outside `docs/<runs>/`, an absolute one outside the repository or one with a `..` segment. An absolute path inside the repository is folded into its relative form by `cwd_relative()`, copied verbatim into `archive-run.sh`, `plan-index.sh` (the plan) and `commit-task.sh` (the plan, a `--from`/`--first-of` file): the run scripts share no library, so a fix to it changes all three.
- `commit-task.sh` exports `GIT_LITERAL_PATHSPECS=1`: every path it stages is one exact path, never a pattern (bracketed App Router segments). It refuses any `.temp/` path, and derives the `work/` trail paths it stages from the task id or round, never from the caller.
- Extras files: the coder's `<run-dir>/work/<id>-extra.md` and the reviewer's `<id>-extra-review.md`, one repo-relative path per line (blank lines ignored, a carriage return and a leading `./` stripped). `plan-index.sh --extra <id>` reads only the coder's, validating the plan as `--split` does, and prints `extra:` (minus a repeat, a `.temp/` path, a path the task's own `Files` lists and one a task not yet done lists) and one `recheck:` per done task owning an extra path, nothing when none remains; `--split` removes both files of every task not done before printing the index, so a cut-off run's leftovers reach no later review or commit. A plain task commit stages the paths of both files as if on `--with` (same ownership warnings), plus the two files and `<id>-options.md`; `--landed` and a fix number ignore them.
- Owed paths: `commit-task.sh <plan> <id> --owe <path>...` stores `<id>:<path>` on the `owed:` line of `status.md`, staging and checking nothing, in the task's own commit; `--repaired <plan> <id>` (a done task only) appends the id to `repaired:` with no commit. Both lines are append-only, and `plan-index.sh` prints `owed:` only for entries whose task is not on `repaired:`, no line when none remain.
- `commit-task.sh --rule <subject> --from <ruling-file>` and `--decide <id> --from <ruling-file>` read the arbiter's ruling file, which must lie under `<run-dir>/work/` with its `RULING:`, `WHY:` and `COST:` lines non-empty (else nothing is recorded and the file stays); `--decide` records `auto: <ruling>`. `--decide <id> --first-of <options-file>` records the first option of the file's latest `DECIDE:` line. Any form taking a plan stages `work/ruling-*.md` with the register.
- `qa-comment.sh` posts on the run's issue, the frontmatter `issue:` of the `spec.md` beside the qa file, and on the `--pr` or branch pull request only when the run has none. It posts at most once per run: the body's first line `<!-- viber:qa <path> -->` marks the run key (the qa file's parent directory name), and a comment already carrying the same key, from the run directory or its archive, yields `STATUS=skip`, `REASON=exists`. It writes the body to `.temp/viber/qa-comment/<run key>.md` and leaves it there.
- `pr-threads.sh` lists only unresolved review threads and lists nothing (`REASON=wrong-branch`, no threads file) unless the checked-out branch is the pull request's head branch; it never checks out or pushes. A ready run replaces `.temp/viber/pr-feedback/<N>/threads.md`; `PUSHED=` says whether local `HEAD` is the head commit. A thread whose latest comment opens with the reply marker is left out and counted in `SKIPPED=`.
- `pr-reply.sh` replies only through the `.../pulls/<N>/comments/<id>/replies` endpoint, `<id>` the thread's first comment (GitHub has no reply to a reply), at most once per comment: the body is the reply marker `<!-- viber:pr-feedback <id> -->`, a blank line and the reply file, kept in `.temp/viber/pr-feedback/<N>/posted/<id>.md`; a reply to `<id>` already opening with the marker yields `STATUS=skip`, `REASON=exists`. A relative reply file resolves from the cwd, unlike `qa-comment.sh`.
- `skill-part.sh <skill dir> <part>` prints `<skill dir>/parts/<part>.md` (exit 1 with `error:` on stderr for a missing argument, a part outside `[a-z0-9-]+` or an absent file). Only a fenced block opening with ```` ```! ```` whose single body line is the exact `switch-text.sh` call shape is run, and it is replaced, fence lines included, by that call's output; any other block prints as plain text. A fragment's output is appended as `switch-text.sh` printed it, never expanded twice; `${CLAUDE_SKILL_DIR}` and `${CLAUDE_PLUGIN_ROOT}` in every other line are expanded here, the plugin root being the skill dir minus its last two segments. A block never closed before end of file prints unchanged.
- The `--branching` report of `config.sh` is read by `run-branch.sh` and `pr-facts.sh` only; every other reader takes the default block.

## Commands

- Each script's test is `tests/viber/<basename>.test.ts`, integration tier (CI only). `run-branch.sh` has no file of its own: `tests/viber/plan-path.test.ts` covers it through `--branch`, `--start`, `--checkout` and `--land`.

## Change together

- The issue reference grammar (`<N>`, `#<N>`, or `https://<host>/<owner>/<repo>/issues/<N>` with any `#...` fragment or `?...` query dropped) is parsed the same way in `issue-facts.sh` and `post-comment.sh`; `qa-comment.sh` applies it to the run's `issue:` and to `--pr` pull request URLs.
- A `config.sh --branching` line added or renamed: its readers in `run-branch.sh` and `pr-facts.sh`.
- The reply marker `<!-- viber:pr-feedback <comment-id> -->` is matched in `pr-threads.sh` (its awk block) and written and matched in `pr-reply.sh`; the `--jq` output shape of each (`pr-threads.sh`: `T`/`C`/`B` tab lines; `pr-reply.sh`: `<in_reply_to_id> <first body line>`) is emulated by the stub of `tests/viber/pr-threads.test.ts` / `pr-reply.test.ts`, so a filter change changes the stub lines. The pull request reference grammar (`<N>`, `#<N>`, `https://<host>/<owner>/<repo>/pull/<N>`) is parsed the same way in both.
- The `Covers:` token grammar lives in the two `covnums` copies of `plan-index.sh`: one edit changes both.

## Traps

- `skill-part.sh` is invoked bare by `implementor/SKILL.md`, so its index mode must stay 100755: the portability exec-bit sweep fails on 100644, and `core.fileMode` is false on some machines, so `git update-index --chmod=+x` is the only place the bit holds.
- The GitHub wrappers are `#!/bin/sh` and must stay POSIX: CI runs them under every POSIX shell present, and the portability sweep flags bash-only syntax in them. The other scripts are bash and may use arrays and `[[ ]]`.
- A failed GitHub call is never retried by its caller: `post-comment.sh` exits 1 when whether the comment landed is unknown, and `pr-create.sh` exits 1 after the push may already have happened (its ERROR line names the pushed branch); `pr-reply.sh` exits 1 when a reply may or may not have landed; the caller reports instead.
- The portability sweep reads the awk block of `pr-threads.sh` as shell: it flags `function NAME` and `((` there, so the block defines no function (a sentinel `T` line closes the last thread) and the `--jq` filter avoids `((`.
- `issue-create.sh` never removes a created issue when applying its type or its `--parent` attach fails: the type failure is reported as `TYPE=dropped` (benign: types not enabled, 403, 404) or `TYPE=error`, the attach failure as `PARENT=error` with `PARENT_ERROR=`, always with exit 0. `--parent` takes a positive number of the same repository and attaches through the REST sub-issues endpoint; a bad value exits 2 before `gh` runs.
- `plan-index.sh` counts only `AC<n>` tokens of a `Covers:` value as criteria; `#<n>` and a bare `<n>` cover nothing, so a task writing them fails with exit 4.
