Description
The /wr-itil:update-upstream skill's Step 1 documented ticket-lookup snippet silently no-ops under zsh. The dual-tolerant glob
ls docs/problems/${LOCAL_ID}-*.{open,known-error,verifying,closed,parked}.md docs/problems/*/${LOCAL_ID}-*.md 2>/dev/null
aborts the whole command under zsh's default no-match behaviour ("no matches found: docs/problems/085-*.open.md") whenever the flat-layout half of the brace expansion matches nothing. On post-RFC-002 per-state-subdir-layout repos the flat half ALWAYS matches nothing, so the command aborts, LOCAL_TICKET resolves empty, and the skill's Step 1 takes the "No ## Reported Upstream section" no-op exit without ever reading the ticket.
On a ticket that does carry a ## Reported Upstream section this is a silent false no-op: the upstream lifecycle-update comment is never posted, defeating the bidirectional update-upstream contract with zero error surfaced to the operator.
Symptoms
- Under zsh, the Step 1 lookup errors with
(eval):1: no matches found: docs/problems/<ID>-*.open.md and the brace-glob command aborts before head -1 runs.
- A trailing
ugrep: warning: : No such file or directory was also observed when the empty LOCAL_TICKET flowed into a later grep.
LOCAL_TICKET is empty, so Step 1 concludes "no ## Reported Upstream section" and exits without posting the lifecycle comment, even on tickets that do carry that section.
- Observed 2026-06-11 during a known-error transition iter: the no-op conclusion happened to be correct only because the agent had independently read the full ticket; on any other ticket the upstream comment would have been silently dropped.
Workaround
Split the two layout globs into separate ls invocations, or wrap the lookup in a shell that tolerates no-match (setopt null_glob under zsh, shopt -s nullglob under bash). The agent operating the skill can also read the ticket directly rather than relying on the documented snippet.
Affected plugin / component
@windyroad/itil — packages/itil/skills/update-upstream/SKILL.md, Step 1 (ticket-lookup snippet).
Frequency
Every zsh invocation of /wr-itil:update-upstream against a per-state-subdir-layout repository (the macOS default shell is zsh, and per-state layout is the post-RFC-002 norm).
Versions
- Local plugin:
@windyroad/itil@0.49.4
- Upstream package: not applicable (the gap is in the windyroad plugin itself)
- Claude Code CLI: 2.1.177
- Node: v22.17.1
- OS: Darwin 25.3.0 x86_64
Evidence
- The same dual-tolerant brace-glob lookup pattern is the documented snippet in
packages/itil/skills/update-upstream/SKILL.md Step 1 (one site), and any zsh-shell adopter hits it on every invocation against a per-state-layout repo.
- The companion
report-upstream SKILL.md Step 1 carries a structurally similar dual-tolerant lookup (ls docs/problems/${LOCAL_ID}-*.{...}.md docs/problems/*/${LOCAL_ID}-*.md 2>/dev/null | head -1), which has the same zsh no-match-abort exposure and is worth fixing in the same pass.
Suggested fix shape
Make the Step 1 snippet shell-portable (split the two layout globs into separate ls invocations, or guard with setopt null_glob / shopt -s nullglob), AND add an explicit empty-LOCAL_TICKET assertion distinct from the missing-section no-op exit, so a lookup failure halts loudly instead of resolving to a false no-op.
Cross-reference
Reported from https://github.com/voder-ai/voder-mcp-hub docs/problems/open/095-update-upstream-ticket-lookup-glob-silently-false-no-ops-under-zsh.md (private repo).
This issue is tracked locally as P095 in the downstream project's docs/problems/ directory.
Related
Shares the zsh shell-portability class with #139 (migrate-problems-layout.sh bashisms under zsh in work-problems Step 0a), but is a distinct gap: different file, different skill, different mechanism (brace-glob no-match abort vs shopt builtin missing).
Description
The
/wr-itil:update-upstreamskill's Step 1 documented ticket-lookup snippet silently no-ops under zsh. The dual-tolerant globaborts the whole command under zsh's default no-match behaviour ("no matches found: docs/problems/085-*.open.md") whenever the flat-layout half of the brace expansion matches nothing. On post-RFC-002 per-state-subdir-layout repos the flat half ALWAYS matches nothing, so the command aborts,
LOCAL_TICKETresolves empty, and the skill's Step 1 takes the "No## Reported Upstreamsection" no-op exit without ever reading the ticket.On a ticket that does carry a
## Reported Upstreamsection this is a silent false no-op: the upstream lifecycle-update comment is never posted, defeating the bidirectional update-upstream contract with zero error surfaced to the operator.Symptoms
(eval):1: no matches found: docs/problems/<ID>-*.open.mdand the brace-glob command aborts beforehead -1runs.ugrep: warning: : No such file or directorywas also observed when the emptyLOCAL_TICKETflowed into a later grep.LOCAL_TICKETis empty, so Step 1 concludes "no## Reported Upstreamsection" and exits without posting the lifecycle comment, even on tickets that do carry that section.Workaround
Split the two layout globs into separate
lsinvocations, or wrap the lookup in a shell that tolerates no-match (setopt null_globunder zsh,shopt -s nullglobunder bash). The agent operating the skill can also read the ticket directly rather than relying on the documented snippet.Affected plugin / component
@windyroad/itil—packages/itil/skills/update-upstream/SKILL.md, Step 1 (ticket-lookup snippet).Frequency
Every zsh invocation of
/wr-itil:update-upstreamagainst a per-state-subdir-layout repository (the macOS default shell is zsh, and per-state layout is the post-RFC-002 norm).Versions
@windyroad/itil@0.49.4Evidence
packages/itil/skills/update-upstream/SKILL.mdStep 1 (one site), and any zsh-shell adopter hits it on every invocation against a per-state-layout repo.report-upstreamSKILL.md Step 1 carries a structurally similar dual-tolerant lookup (ls docs/problems/${LOCAL_ID}-*.{...}.md docs/problems/*/${LOCAL_ID}-*.md 2>/dev/null | head -1), which has the same zsh no-match-abort exposure and is worth fixing in the same pass.Suggested fix shape
Make the Step 1 snippet shell-portable (split the two layout globs into separate
lsinvocations, or guard withsetopt null_glob/shopt -s nullglob), AND add an explicit empty-LOCAL_TICKETassertion distinct from the missing-section no-op exit, so a lookup failure halts loudly instead of resolving to a false no-op.Cross-reference
Reported from https://github.com/voder-ai/voder-mcp-hub
docs/problems/open/095-update-upstream-ticket-lookup-glob-silently-false-no-ops-under-zsh.md(private repo).This issue is tracked locally as P095 in the downstream project's
docs/problems/directory.Related
Shares the zsh shell-portability class with #139 (
migrate-problems-layout.shbashisms under zsh in work-problems Step 0a), but is a distinct gap: different file, different skill, different mechanism (brace-glob no-match abort vsshoptbuiltin missing).