---
model_tier: medium
name: script-writing
description: "Use when adding or editing any script under `scripts/` — `--quiet` flag, `_lib/script_output` helpers, silent Taskfile wiring, Iron-Law carve-outs — even when you just say 'add a check script for X'."
domain: process
workspaces:
  - agent-config-maintainer
packs:
  - meta
---

<!-- cloud_safe: degrade -->

# script-writing

## When to use

* Creating a new Python script in `scripts/{name}.py` (linters, checks, generators, measure tools)
* Editing an existing script that prints progress or success lines
* Wiring a script into `Taskfile.yml` or `taskfiles/*.yml`
* Reviewing a PR that adds scripts and asking "why does this still print on minimal?"

Do NOT use this skill when:

* The content is a one-off / archival under `scripts/ai_council/one_off_archive/` — those carry an `_one_off_` prefix and are exempt from the verbosity convention
* The content is a shell entrypoint with secret prompts (install-keys, release confirms) → see § 3 Iron-Law carve-outs
* The content is a `.mjs` / Node script under `scripts/cost/` — different runtime; convention covered in `agents/settings/contexts/cost-tracking.md`

## Script vs other writers — critical test

| Intent | Artifact |
|---|---|
| "Maintenance script the agent or CI runs" | **This skill** |
| "User types `/foo` to invoke" | `command-writing` |
| "Constraint the agent must always honor" | `rule-writing` |
| "Reference knowledge agents cite" | `guideline-writing` |

Scripts orchestrate file checks, generators, and validators. They are
neither user-invoked nor agent-routed — `task` and CI call them.

## Procedure

### 0. Run the Drafting Protocol

Creating or materially rewriting a script that joins the linter / check
family **must** go through Understand → Research → Draft from the
[`artifact-drafting-protocol`](../../rules/artifact-drafting-protocol.md) rule.

* **Understand** — what failure does this script catch that no existing
  check catches? Is it a one-off (then archive it) or evergreen?
* **Research** — `ls scripts/check_*.py scripts/lint_*.py`, grep for
  overlap, skim 1–2 peer scripts (e.g. `lint_handoffs.py`,
  `check_md_language.py`).
* **Draft** — propose name + one-line purpose first. Only fill the body
  after the shape is confirmed.

### 1. `--quiet` flag — argparse + sys.argv fallback

Every `check_*.py` / `lint_*.py` script MUST accept `--quiet` so the
silent Taskfile layer (§ 4) can suppress success-only output.

**With argparse (preferred):**

```python
parser = argparse.ArgumentParser(...)
parser.add_argument("--quiet", action="store_true",
                    help="Only print on failure")
args = parser.parse_args()
...
if not args.quiet:
    print("✅ All clean")
```

**Plain script (no argparse) fallback:**

```python
import sys

QUIET = "--quiet" in sys.argv
...
if not QUIET:
    print("✅ All clean")
```

Failure output (`❌`, non-zero exit) is **never** gated — failures must
always print regardless of `--quiet`.

### 2. `_lib/script_output` helpers — preferred for new scripts

For anything richer than a single `✅`/`❌`, import the verbosity-aware
router instead of raw `print()`:

```python
from _lib.script_output import info, success, warn, error, flush_summary

info("Loading manifest")          # drops on silent + minimal
success("Wrote 3 files")          # collected at minimal, printed at verbose
warn("Skipping stale entry")      # stderr unless silent
error("Manifest missing")         # stderr always

flush_summary("Done — 3 entries") # one-line summary at minimal
```

Resolution order (first wins, cached for the process):

1. `AGENT_SCRIPT_VERBOSITY` env (`silent` / `minimal` / `verbose`)
2. `SCRIPT_OUTPUT_VERBOSE=1` alias (== `verbose`)
3. `.agent-settings.yml` → `verbosity.script_output`
4. Default `minimal`

The resolved level is exported back into `AGENT_SCRIPT_VERBOSITY` so
child processes inherit it. Tests reset via `reset_level()` from the
same module — see `tests/test_script_output.py`.

### 3. Iron-Law carve-outs — never silenced

The following surfaces **MUST** use plain `print()` and never the
helpers, so verbosity settings cannot suppress them:

* Release confirms — every task in `taskfiles/release.yml`
* Secret prompts — `install-anthropic-key`, `install-openai-key`,
  `setup-evals`, `install-hooks` interactive sections
* `runtime-e2e` and `test-triggers-live`
* CI orchestration sentinels — `_ci-start`, `_ci-end`, root `ci`
* Any prompt that asks the user for confirmation per
  [`non-destructive-by-default`](../../rules/non-destructive-by-default.md) — Hard Floor cannot be silenced

If unsure, check `scripts/ai_council/one_off_archive/2026-05/_one_off_silent_taskfiles.py`
`CARVE_OUTS` for the canonical list.

### 4. Taskfile wiring — `silent: true` + `{{.QUIET_FLAG}}`

Every Taskfile task that wraps a `--quiet`-aware script MUST set
`silent: true` and pass `{{.QUIET_FLAG}}` to the script:

```yaml
# Per-task in taskfiles/*.yml:
tasks:
  lint-handoffs:
    silent: true
    cmd: python3 scripts/lint_handoffs.py {{.QUIET_FLAG}}
```

The `QUIET_FLAG` var is defined once at the root of `Taskfile.yml`
and resolves to `""` only when `AGENT_SCRIPT_VERBOSITY=verbose`:

```yaml
# Root Taskfile.yml — already in place, do not duplicate:
vars:
  QUIET_FLAG:
    sh: '[ "$AGENT_SCRIPT_VERBOSITY" = "verbose" ] && echo "" || echo "--quiet"'
```

Carve-out tasks (release, install secrets, CI orchestration — see § 3)
do **not** add `silent: true` and do **not** use `{{.QUIET_FLAG}}`.

### 5. Validate

* Run `python3 scripts/skill_linter.py .agent-src.uncondensed/skills/script-writing/SKILL.md` → 0 FAIL
* Run `python3 scripts/{your-script}.py --quiet` and the verbose path — exit code 0 on clean, non-zero on failure regardless of flag
* If the script uses `_lib/script_output`, add a test under `tests/` patterned on `tests/test_script_output.py` — assert `silent` / `minimal` / `verbose` behave per § 2
* Run the full CI pipeline locally (see `Taskfile.yml` in this repo for the script list) — must exit 0 except for tolerated warnings

## Output format

1. Script file at `scripts/{name}.py` with `--quiet` accepted
2. Taskfile wiring with `silent: true` + `{{.QUIET_FLAG}}` (unless carve-out)
3. Test under `tests/` if `_lib/script_output` is used
4. Linter output showing 0 FAIL

## Gotchas

* Forgetting `--quiet` — the silent Taskfile layer wraps the script and the call fails with `unrecognized arguments: --quiet`
* Gating `❌` failures behind `--quiet` — failures must always print
* Using raw `print()` for progress lines — drops on `minimal`, no inheritance
* Adding `silent: true` to a release / install-keys task — bypasses the Hard Floor confirmation
* Editing `Taskfile.yml`'s `QUIET_FLAG` var — single source of truth, do not duplicate
* Forgetting that `from _lib.script_output import …` requires the script's resolution to walk up to `scripts/` — copy the `Path(__file__).resolve().parent` pattern from a peer

## Frugality Standards

Apply the [Frugality Charter](../../contexts/contracts/frugality-charter.md)
to every script you author. Phase 10 of the charter (settings hooks
row — `verbosity.script_output` / `verbosity.taskfile_command_echo`)
is what this skill exists to teach.

**Examples in this artifact:**
- Per the charter's default-terse rule, success lines are gated behind
  `--quiet` so the only thing visible at `minimal` is the end-of-run
  summary or a failure.
- Per the post-action summary suppression, scripts with multi-step
  output collect via `success()` and emit one `flush_summary()` line.
- Per the cheap-question check, scripts never prompt unless they hit
  a Hard Floor surface (§ 3 carve-outs).

**Pre-save self-check:**
1. Does every `print("✅ ...")` line sit behind `--quiet` / the helper?
2. Does the script add `silent: true` + `{{.QUIET_FLAG}}` to its
   Taskfile entry (unless a carve-out)?
3. Are failure lines (`❌`, exit non-zero) **never** gated by quiet?
4. Are Iron-Law surfaces using plain `print()` and not the helper?

## Do NOT

* Do NOT gate `❌` / failure output behind `--quiet`
* Do NOT use `_lib/script_output` for release confirms or secret prompts
* Do NOT add `silent: true` to carve-out tasks
* Do NOT hardcode `print()` for progress in new scripts — use `info()`
* Do NOT skip the Taskfile wiring — without it the verbosity gates leak
* Do NOT edit `dist/agent-src/`, `.augment/`, or `.claude/` projections

## Cloud Behavior

On cloud surfaces (Claude.ai Web, Skills API) the package's `task`
runner and `_lib/script_output` are not reachable. The skill still
applies — with prose-only validation:

* Emit the full script + Taskfile snippet as copyable Markdown blocks. Do not attempt to write to disk.
* Self-check: `--quiet` accepted, failures never gated, success lines gated, helper imports look syntactically right.
* Tell the user to save under `scripts/{name}.py`, wire the Taskfile entry, and run `task lint-skills && task ci` locally before committing.
* Skip every reference to running the linter or `task` commands yourself — they only run on the user's machine.

## Examples

Good description (trigger-shaped, names domain + symptoms):

> "Use when adding or editing any script under `scripts/` — `--quiet` flag, `_lib/script_output` helpers, silent Taskfile wiring, Iron-Law carve-outs — even when you just say 'add a check script for X'."

Bad description (vague, no trigger):

> "Script conventions"
