Skip to content

[problem] /wr-itil:update-upstream Step 1 ticket-lookup glob silently false-no-ops under zsh #256

@tompahoward

Description

@tompahoward

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/itilpackages/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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions