zheref

izanami

Repeat a READ-ONLY task until a condition holds. Use when the maintainer invokes hatsu:izanami <task> until <condition>, or asks to watch, poll, or keep checking something until it changes. Observation only — it reads, reports and stops; it never writes to GitHub, never labels, never merges. For a loop that must act, use hatsu:izanagi, which requires an explicit iteration cap.

zheref 0 Updated 4w ago
GitHub

Install

npx skillscat add zheref/hatsu/izanami

Install via the SkillsCat registry.

SKILL.md

Izanami — watch until it is true

Nature: Manipulator (GitHub-side ops/observation) unless the thing being watched is product, in
which case say so and switch mode out loud. Either way izanami only ever looks. Kurapika says so
when he runs it.

Keep checking this until that is true, then tell me.

Izanami is the read-only half of the loop pair. Its mutating twin is
`hatsu:izanagi`, which requires an explicit cap in its grammar precisely
because it acts. The split is the safety property: a loop that cannot write cannot compound a
mistake, however many times it runs.

The old <reference-repo> skill enforced that split by an agent reading a hand-authored allow/refuse table
by eye, every invocation. This port replaces both halves of that with nen: nen parse izanami
splits and classifies the invocation before iteration 1, and nen watch until runs the loop itself —
fetch, evaluate, report one line, pace, stop. Judgment stays where the old skill kept it: naming which
mode is in play, deciding whether a condition is genuinely decidable, and saying what the next action
is without taking it.


1. Invocation — nen parse izanami does the split, never prose

hatsu:izanami <task> until <condition>
hatsu:izanami until <condition>
<a second read-only command to repeat>
...

Before running anything, echo the parse through the verb itself — this is not optional ceremony, it
is the enforcement mechanism:

nen parse izanami "<the invocation line, or a newline-separated block for the second form>"

nen parse izanami (no --grammar — it is one of the three built-in grammars, alongside futon and
izanagi) matches until case-insensitively as the LAST whole-word occurrence, echoes the parsed
condition back as until: <condition>, and then prints every task/command line with its
classification tag — [read-only], [mutating] or [unknown] — before anything is allowed to run.

Verified live, first form, all read-only:

$ nen parse izanami "gh pr checks 925 --repo <reference-repo> until they are all green"
until: they are all green
  [read-only] gh pr checks 925 --repo <reference-repo>
$ echo $?
0

Second form (until <condition> with commands on following lines — pass the whole block as one
newline-separated argument):

$ nen parse izanami $'until they are all green\ngh pr checks 925 --repo <reference-repo>'
until: they are all green
  [read-only] gh pr checks 925 --repo <reference-repo>
$ echo $?
0

The task/command line must be a literal, executable command — never a prose description of intent.
Verified live: a prose task line classifies [unknown] and refuses the whole run exactly like a
genuinely unrecognized command does —

$ nen parse izanami "watch PR #925 checks until they are all green"
until: they are all green
  [unknown] watch PR #925 checks
nen: at least one command does not classify as read-only -- the WHOLE run is refused. Use 'nen parse
izanagi <task> until <condition> up to <N>' for a loop that must act.
$ echo $?
1

This is the same enforcement the loop itself applies (§4) — write the real gh/git invocation, not
a description of what it does.

Exit codes, verified live:

Exit Meaning
0 The line parsed AND every command classifies read-only
1 The line parsed but at least one command classifies mutating or unknown — the whole run is refused, and the message names nen parse izanagi <task> until <condition> up to <N> as the mutating twin
2 The line itself does not parse — no until found at all (nen: no 'until <condition>'. Expected '<task> until <condition>'.), or until <condition> with no task and no following command at all (nen: no task and no commands to repeat -- expected a task on the first line, or a command per following line.) — the trigger to ask for the missing part and re-parse, not the end

Never reconstruct this parse by hand once nen is available — echo its output verbatim and act on the
exit code. A missing argument or configuration item is asked for and set up inline (missing-argument,
missing-configuration; `WORKFLOW.md` § 4 Ask, set up, continue); exit
1 is not a gap — a command that writes is refused, never reworded to pass.

2. Read-only, enforced before the first iteration — nen's classifier IS the table

The old skill's allow/refuse table was hand-applied prose. Verified against the real binary at the pin
that then stood (v0.3.0; the contract's pinned_ref has since moved forward — v0.5.0 added the
stop --mark and commit check rows, and v0.7.0 moves exactly one row, the gh api --jq fold in
the box below),
nen parse izanami / nen watch until implement a fixed classifier that
reproduces the same shape mechanically — every row below is a live-verified [read-only], [mutating] or
[unknown] tag, not a transcription from memory. nen v0.2.0 widened the allowlist (plain file reads
and nen's own verbs, #74) and v0.2.0/v0.3.0 tightened the metacharacter seam (#76, #104), so every
row was re-run rather than carried over:

Allowed — verified [read-only] Refused — verified [mutating]
gh pr view, gh pr checks, gh pr list, gh pr diff, gh pr status gh pr merge, gh pr comment, gh pr close, gh pr reopen
gh issue view, gh issue list gh issue create, gh issue edit, gh issue close
gh run view, gh run list, gh run watch gh label create, gh label edit, gh label delete
gh repo view gh release create, gh release edit, gh release delete
gh api (a plain GET, no -X/--method) gh api with -X POST/PUT/PATCH/DELETE, and the --method POST spelling
git fetch, git log, git diff, git status, git ls-tree, git show the mutating git family: push, commit, merge, tag, rebase, reset, clean
git branch — listing forms only: bare, -a, --list git branch -D, git branch -m (same family, delete/rename mutate)
git remote — listing forms only: bare, -v, show git remote add, remove, rm, set-url, rename, prune, set-head (same family, every mutating subcommand)
plain file reads — cat <file>, test -f <file>, type <name> (new in v0.2.0) git checkout -b (branch creation)
nen's own read-only verbs — nen pr ready …, nen backlog fetch …, nen board build …, nen schema check …, nen shu detect, nen warmup … (new in v0.2.0) nen's own mutating verbs, in every form — nen label apply … (with or without --run), nen tag cut …, nen shu detect --write, nen shu warmup … (--dry-run included — the one dry run that still classifies mutating, by design), nen shu tools --install (with or without --dry-run); and the 0.13 git verbs nen wc catch-up, nen commit write, nen wc publish, nen pr open, mutating by design, their --dry-run forms treated as refused until re-run here
the dry-run forms of the gated verbs — nen shu build --dry-run (and test, lint, coverage, ui-test …), nen scaffold init … --dry-run, nen scaffold new … --dry-run, nen issue comment … --dry-run, nen shu tools --dry-run the bare forms of the same — nen shu build, nen shu test, nen scaffold init …, nen issue comment … (the argv comes from somebody else's file, or writes to GitHub)
nen shu deploy --target <name> bare, and with --dry-run — the plan; nen spawns nothing without --run, whatever the declaration says nen shu deploy --target <name> --run
any invocation nen parse izanagi would need instead

And three shapes that classify [unknown] — refused exactly like [mutating], verified live at
v0.3.0:
a script path (./scripts/check.sh — nen cannot read what it does), a pipeline or any
shell metacharacter
(gh pr view 1 | grep MERGED; there is no shell, so the whole line is one thing nen
does not recognise), and a -- end-of-options marker in front of further tokens (gh api … -- --fields, nen pr ready 42 -- --run) — the classification seam refuses a line it cannot prove has no
hidden write flag behind the marker. nen shu tools bare is [unknown] too, deliberately: the check
spawns the target repository's declared version probes, so nen labels it "not provably a read" rather
than "writes"; nen shu tools --dry-run spawns nothing and is [read-only]. Quoted arguments are fine
(gh pr view 1 --json state --jq '.state' is [read-only]); a pipe is not.

This table is a sample, spot-verified live against the shipped binary, not an exhaustive
transcription
— a command not listed here is checked with nen parse izanami before it is ever
handed to nen watch until, never assumed from this table by analogy. Note in particular that
git branch and git remote split within the same family: the bare/listing form is
[read-only], a specific mutating subcommand of the same command is [mutating] — the classifier
looks at the full shape, not just the leading verb. nen's own verbs split the same way, on the flag that
acts (--run, --write, --install) or on the dry run that renders (--dry-run), per verb.

RETIRED at nen 0.7: a gh api read carrying a single-quoted --jq

gh api … --jq '<expr>' classifies [read-only], and it is the commonest spelling of that read
there is.
Through the pinned v0.6.0 it came back [unknown] and the whole run was refused —
verified live at both pins, the same line through both binaries (docs/ab/izanami.md
§ Retired at nen 0.7):

v0.6.0  [unknown]   gh api repos/zheref/nen/pulls/1 --jq '.state'        # exit 1, whole run refused
0.7.0   [read-only] gh api repos/zheref/nen/pulls/1 --jq '.state'        # exit 0

A '…' span with no inner quote and no newline is exactly ONE word to every shell, and its
content is literal — no expansion, no substitution, no word splitting — so the gh api row folds
such a --jq value into one inert placeholder before scanning. That is not a weaker gate: the
fold changes neither the argument vector's length nor any other word in it, so the absences the
read-only verdict rests on (no non-GET method, no -f/-F/--field/--raw-field/--input, not
graphql) are still scanned over a line the shell would agree with.

Narrow three times over, and each narrowing is a line this skill must not cross. Only SINGLE
quotes
— a double-quoted span expands $x, a backtick and \\, so it is one word but not an
inert one, and it still refuses: verified live at the pinned 0.7.0,
gh api … --jq ".state" is [unknown] at exit 1, and the refusal names both workarounds
(respell it with single quotes, or watch the bare read and apply jq downstream). Only
--jq/-q
, not every quoted span — a general fold would also swallow -X 'DELETE'. Only a
whole token
— --jq'.name' is the single word --jq.name to a shell and refuses as before. And
a --jq carrying a metacharacter ('.a | .b') is still refused by the whole-line seam that runs
before any row vouches for anything.

So the rule for composing a watch is now spellable: write the --jq value in single quotes,
keep it free of pipes and other metacharacters, and check the line with nen parse izanami before
handing it to nen watch until — which is the rule this section already gives for every line.

Where nen is stricter than the old skill's prose table — a behavior change, not a bug: the old
allow table admitted "reading a file, running a checker script" by category. Since v0.2.0 the classifier
admits the plain file read (cat, test -f, type); it still has no category for a checker
script
— a script is a program nen cannot read, so it is [unknown], and [unknown] refuses exactly
like [mutating]
. A script the old skill would have allowed by eye is refused outright. This is
disclosed, not routed around: express the check as the git/gh/nen read the script itself would run,
or run the script by hand outside the watch and say so.

git fetch is allowed and is usually required — it writes only to local refs, and a watch that
never fetches watches a frozen picture. It is the one write-shaped thing that is genuinely
observation, and it classifies [read-only] exactly as the old skill's table said it should.

Skill-level refusals the classifier cannot see — because they are not shell commands at all — stay a
judgment rule, unchanged from the old skill: never run `sharingan`,
`build`, `file`, `tensho`,
`jujisho`, `getsuga`,
`backlog-synthesis` or `backlog-loop`
inside a watch, and never post a comment, apply a label or publish an Artifact as part of one.
`backlog-state`, reading a page, and a genuinely read-only checker script
remain allowed in spirit — the first two now classify [read-only] when handed to nen as a
--command; the script does not (above).

Two findings this port filed against nen v0.1.0, both closed by nen v0.2.0 (#74, closes
zheref/nen#31) — recorded here so the A/B file's § 4 is read as history, not as a live limit:

  1. A plain file read classified [unknown]. Fixed. Verified live at v0.3.0:
    nen parse izanami "cat README.md until it says DONE" → [read-only] cat README.md, exit 0; test -f and type the same. A watch over a local file's contents goes through nen watch until
    directly now
    — with one caveat the verb's own --help states: it spawns <bin> with no shell, so
    a builtin (type, or cat/test on a host where they are not real executables on PATH) fails at
    spawn on every observation and the watch stops on the error streak. On macOS and Linux cat and test
    are real binaries and the watch runs; if the first observation errors, that is why — read the file
    through a git show/git diff against a tracked path instead, and say so.
  2. nen's own verb surface classified [unknown]. Fixed. Verified live at v0.3.0: nen parse izanami "nen pr ready BC#925 until it is ready" → [read-only] nen pr ready BC#925, exit 0; nen backlog fetch and nen board build the same, and a mutating nen label apply … — with or without
    --run — is [mutating] rather than [unknown] (#104). A watch may poll a computed nen verdict
    directly
    : nen watch until --command "nen pr ready <CODE>#<N> --repo <path> --gates <abs path>"
    is the natural form for "tell me when it is Ready" (exit 0 on ready is the truth reading; export
    GH_TOKEN first, as every nen pr ready call needs). The old workaround — poll the underlying gh
    read instead — is no longer needed, though it still classifies fine.

Refuse the whole run, not the offending step. Both nen parse izanami and nen watch until do
this themselves — a mixed command list refuses before iteration 1 ever runs, verified live in
docs/ab/izanami.md § 2.2 and re-verified at v0.3.0.

3. The condition

nen parse izanami echoes the parsed condition back as until: <condition> before iteration 1 —
say how it will be decided (the exact --true-pattern, or "exit code 0" when none is given) alongside
that echo.

A good condition is decidable from one observation and expressible as either a regex against
stdout or a command's own exit code. If the condition as typed is not decidable this way, say what is
ambiguous and offer the concrete predicate you propose to use — do not guess and proceed.

Conditions that can never become true are named, not waited on. Watching a closed PR for a green
check, or a deleted branch for a push, ends the run with "this cannot become true, because…" rather
than looping until a bound.

4. The loop — nen watch until runs it, not hand-rolled prose

nen watch until --command "<the classified read-only command>" \
  [--true-pattern <regex>] [--interval-ms 5000] [--max-iterations <n>] \
  [--cwd <path>] [--error-exit-threshold <n>]
  • --command is classified against izanami's table (§2) before the first run — a mutating or
    unknown command is refused outright (exit 2), naming nen parse izanagi instead. Never hand it a raw
    string you have not already checked with nen parse izanami. It is spawned directly, with no shell:
    <bin> must be a real executable on PATH (a shell builtin fails at spawn), and a pipeline is not a
    command it can classify — one observation, one program.
  • --true-pattern <regex> — tested against stdout. Omit it to treat exit code 0 as true (the
    default a check-style command uses). When given, a non-zero exit is an OBSERVATION ERROR, not a
    false reading — the pattern alone decides truth.
  • --error-exit-threshold <n> — only meaningful when --true-pattern is not given: an exit
    code at or above this is an observation error rather than false. Default 2 (codes 0/1 are the
    ordinary true/false pair most CLIs use).
  • --interval-ms — default 5000. Pace to what is being watched: CI checks move on the order of
    minutes, a merge on the order of a human's attention. Say the interval chosen and why.
  • --max-iterations — a safety bound, never izanagi's mandatory cap. Omit it for an unbounded
    watch — izanami can compound no mistake, so an unbounded watch costs time and nothing else. Three
    consecutive observation errors stop the run regardless of this bound
    , verified live below.
  • --cwd <path> — where a git command needs to run against a specific checkout (a gh command
    instead takes its target via its own --repo/-R flag, so --cwd is rarely needed alongside gh).

One line per iteration, no banner — nen renders this itself:

[1] condition is not yet true (exit 0)
[2] condition is not yet true (exit 0)
...
[5] condition is true (exit 0)
condition became true after 5 observation(s)

A watch is not a gate event; the drawn Kurapika header fires only when the run ends and the
maintainer is needed. Do not restate this per-iteration output as a screenful of status yourself — it
already prints one line and stops there.

Exit codes, verified live:

Exit Meaning
0 The condition became true
1 A 3-consecutive-observation-error streak, or --max-iterations reached without becoming true
2 The invocation itself was refused before the loop ever started (a mutating/unknown --command)

Verified live A/B transcripts backing every row above are in docs/ab/izanami.md § 2 — a genuine
false→true multi-iteration watch over a real git diff --stat condition, a <reference-repo> read-only watch
that is already true on the first observation (gh pr view … --json state), a refused mutating
--command (a push of main), and a 3-consecutive-error stop against a nonexistent PR number.

Stops that are not the condition, each said plainly:

Stop Reported as
The condition became true ✅ with the evidence (the matching stdout / exit code) and the elapsed time
The condition became impossible ⛔ with why, e.g. the PR closed unmerged — say this instead of starting nen watch until at all
The observation itself keeps failing ⛔ nen itself stops after 3 consecutive observation errors — relay its own line, don't reclassify it
The maintainer interrupts reported as interrupted, with the last observation

5. Authority

None. izanami applies no label, opens nothing, comments nowhere, merges nothing and publishes
nothing. It carries no CON-25-equivalent delegation of any kind, and being invoked inside another run
does not lend it one.

If the watch makes the next action obvious, say what the action is — do not take it. That is
`backlog-state`'s discipline and it holds here for the same reason: a
"watch this" request that mutates the repo is a betrayal of the request.

6. Hard limits

  • Never writes to GitHub — no comment, label, merge, push, tag, issue, PR or Artifact.
  • Never runs a mutating skill, and never a mutating step "just once to check".
  • Never hands nen watch until a command it has not first checked with nen parse izanami — both
    enforce the same classification, but the parse step is the one that lets you see the whole list
    before anything runs.
  • Never guesses an ambiguous condition — it asks.
  • Never loops on a condition that cannot become true.
  • Never fires the gate banner mid-watch, and never reports a condition met without the evidence.
  • Never routes around a [unknown] refusal by hand-rolling the same observation outside nen watch until and presenting it as though the verb had run it — a checker script, a pipeline or a ---marked
    line the classifier refuses (§2) is reported as a finding, not quietly worked around. (A plain file read
    and a nen-verb poll no longer need routing around: both classify [read-only] since nen v0.2.0.)