# 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 of one skill lives under that skill's `scripts/`, the hook scripts 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 its fixed `KEY=value` block only after `gh` succeeded and its output parsed (self-verifying), else 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; `red-check.sh` by `implementor` alone (`fragments/red-check.true.md`).
- 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` per fragment block; `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`; `red-check.sh` runs `plan-index.sh` and `config.sh --red-check`.

## 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 and unique ignoring case, 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 (a task commit for a skipped task), 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 as `plan-path.sh` printed its `path:` line minus `/plan.md`, and refuses 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). Every `--with` and `--defer` path and extras line is first folded to its repo-relative form (`named_path()`), so ownership and the `.temp/` refusal see one spelling; the fix-number and `--repair`/`--chore`/... file lists are not. It refuses any `.temp/` path, and derives the `work/` trail paths it stages from the task id or round, never from the caller. A named path whose staged mode differs from HEAD gets that exec bit on disk before `git add -u` (`sync_tree_modes`), so every form commits an index mode change under either `core.fileMode`.
- Extras files: the coder's `<run-dir>/work/<id>-extra.md` and the reviewer's `<id>-extra-review.md`, one path per line (blank lines ignored; a carriage return and a leading `./` stripped; an absolute path inside the repository made relative). `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; `--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.
- `plan-index.sh` read-after (validate only, skipped under `--split`): exit 4 when a task's `Verification` (the part after ` -> ` included) or `DoD` names, as a whole path token, a file a lower-numbered task lists with no dependency path to it; a file the reader lists itself is skipped.
- `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>`, or with `--first-of <options-file>` 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`.
- `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. 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`; the newest reply to `<id>` (gh lists comments oldest first) 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`, running only a ```` ```! ```` block whose single body line is the exact `switch-text.sh` call shape (header) and replacing it, fences 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; the placeholders of every other line are expanded here. A block never closed before end of file prints unchanged.
- `config.sh`'s `--branching` report is read by `run-branch.sh` and `pr-facts.sh` only, its `--red-check` report by `red-check.sh` only; every other reader takes the default block. A report line added or renamed changes those readers.
- `red-check.sh "<plan>" "<id>"` prints one `red-check:` line (`FAIL-AS-EXPECTED <log>`, `PASSED-WITHOUT-CHANGE <files>`, `UNRUNNABLE <reason>`) with exit 0, exit 2 on bad arguments or plan. Its worktree lies in the system temp directory; its trap removes the links first, then `git worktree remove --force` and `git worktree prune`, so the forced remove never reaches the main tree through a link.

## 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.
- 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 (`covnums`) and the `;` cut of a `DoD` line outside backtick spans (`dodcut`) each live in two copies in `plan-index.sh`, validate and split: 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.
- 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.
- `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.
