Skip to content

Commit fa9973e

Browse files
AxGordclaude
andcommitted
docs(skills): workflow-authoring loop re-entry gotcha; sync claude-code-config/hxq with live
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent a080e5f commit fa9973e

4 files changed

Lines changed: 38 additions & 1 deletion

File tree

templates/skills/claude-code-config/SKILL.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,30 @@ SubagentStop → fires when a Task-tool subagent completes
128128
PostToolUse → fires after each individual tool call within a turn
129129
```
130130

131+
## Capturing a Real Hook Payload
132+
133+
To see the exact JSON a hook receives, run headless claude with a dump hook — faster and more reliable than docs or grepping the compiled CLI binary:
134+
135+
```bash
136+
# settings.json: {"hooks":{"Stop":[{"hooks":[{"type":"command","command":"cat > /tmp/stop-payload.json"}]}]}}
137+
printf 'start a background sleep 180 via Bash run_in_background, reply done' \
138+
| claude -p --settings settings.json --allowedTools "Bash(sleep:*)"
139+
```
140+
141+
Gotchas:
142+
- Pass the prompt via **stdin** — the argument form can fail with `Error: Input must be provided either through stdin or as a prompt argument` when a PreToolUse rewriter hook mangles quoting.
143+
- A nested `claude -p --dangerously-skip-permissions` gets denied by the auto-mode classifier; a narrow `--allowedTools "Bash(<cmd>:*)"` passes.
144+
145+
## Stop Hook `background_tasks` (v2.1.145+)
146+
147+
Entry shape: `{"id","type","status","description"}` + type-specific fields (`command` for shell, `agent_type` for subagent, `server`/`tool` for MCP). `type` ∈ shell | subagent | monitor | workflow | teammate | cloud session | MCP task.
148+
149+
- `id` is stable across Stop firings; there are **NO timestamps** or turn counters. Distinguishing "started this turn" from "long-lived since an earlier turn" (dev server, watcher) requires persisting seen ids between Stop firings yourself (state file keyed by `session_id`).
150+
- Non-empty `background_tasks` ≠ new work in progress: a persistent dev server stays listed at EVERY Stop — a hook that suppresses on non-empty silences itself for the whole session.
151+
- Only live tasks are listed (completed ones are removed), and `session_crons` entries also carry `id`.
152+
- `prompt_id` (v2.1.196+) identifies the current prompt; `last_assistant_message` (v2.1.145+) holds the final text.
153+
- JSON string values escape quotes as `\"`, so a naive grep for `"key":"` cannot false-match text inside `command`/`description` values.
154+
131155
## npx Argument Re-parsing Under PreToolUse Hooks
132156

133157
PreToolUse Bash hooks that rewrite commands can mis-parse `npx -y <pkg>@latest <subcmd>`, with `<pkg>@latest` landing as an **npm** subcommand, producing `Unknown command: "<pkg>@latest"`.

templates/skills/hxq/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ hxq extract-var f.hx --match 'a * 2' doubled --write
105105
| List all top-level type decls across a scope (cross-file) | `symbols <scope> [--kind ClassDecl/…]` |
106106
| Which files import a module (cross-file) | `importers <module> <scope>` |
107107
| Declaration site(s) of ONE named type + ambiguity check | `declares <type> <scope>` (matches simple name or qualified path; >1 row = ambiguous, 0 = not declared) |
108-
| Run analysis checks + report violations (grouped-by-file) | `lint <scope> [--rule <id>]… [--fix] [--all] [--flat] [--fail-on <sev>] [--format text\|json\|checkstyle]` (analysis/check layer; **lint --list-rules** prints every check as id+description — THE authoritative list. Inline suppression: trailing // noqa[: rules] or CHECKSTYLE:OFF/ON region. --fix has a HUGE blast radius — scope to a file and/or --rule. Full semantics: BEFORE relying on lint verdicts or configuring apqlint.json → Read references/lint.md) |
108+
| Run analysis checks + report violations (grouped-by-file) | `lint <scope> [--rule <id>]… [--fix] [--all] [--flat] [--fail-on <sev>] [--format text\|json\|checkstyle]` (analysis/check layer; **lint --list-rules** prints every check as id+description — THE authoritative list. Inline suppression: trailing // noqa[: rules] or CHECKSTYLE:OFF/ON region. --fix has a HUGE blast radius — scope to a file and/or --rule; looping --fix to a fixpoint without a typecheck between passes compounds cascade deletions. Full semantics: BEFORE relying on lint verdicts or configuring apqlint.json → Read references/lint.md) |
109109
| Every occurrence of name X (incl. case-patterns) | `mentions X <dir>` |
110110
| Who does function F call (direct/transitive) | `callees 'Type.method' <scope> [--depth N] [--kinds call,ref,new,virtual,contains]` (approximate call graph: name + declared-type resolution, `Null<T>` unwrapped, virtual edges to overrides, `Ref` edges for lambdas/method-values/`.bind` with the receiving call as `via`; out-of-scope targets `[external]`; unresolved sites counted to stderr; builds graph over the whole scope — ~25s on TM-sized 800 files) |
111111
| Who calls function F (direct/transitive) | `callers 'Type.method' <scope> [--depth N]` (same graph, in-edges) |
@@ -233,6 +233,7 @@ Create-side + whole-file counterparts of the node-level writer-emit ops. **Raw `
233233
- **Want `| head -N`** — every walker has `--limit N`; walkers also auto-cap at 500 hits without `--limit`. `--limit 0` disables.
234234
- **Script-checking "is X gone?"** — pass `--exit-on-empty` (alias `--require-match`) to a find-walker (refs/uses/meta/lit/cases/blast/mentions/search): 0 hits → non-zero exit instead of the default 0. Without the flag a walk always exits 0, so a bare `hxq refs X …; echo $?` can't distinguish found-vs-gone.
235235
- **Output flood** (e.g. `lit '/*' src/ --any-kind`) — auto-truncated per-hit content (multi-line block-comments → first line + ` … +N lines`); auto-cap at 500 hits with stderr nudge. Both opt-out via `--limit`.
236+
- **`#if``#end` bodies are invisible to walkers and lint — a 0-hit `refs` is NOT proof of "unused".** A conditional-compilation region projects as one `Conditional` node whose interior is raw preserved trivia; `refs`/`uses`/`mentions`/`lit` and lint rules (notably `unused-private`) never see identifiers used only inside an `#if` body. Before deleting something a structural query calls unused in a codebase with conditional compilation, cross-check with a raw-text search (`hxq source`) over the file — `lint --fix` can and does delete private members whose only callers live behind an `#if`.
236237
- **`test-summary -` TRUNCATES a large piped stream**`node bin/test.js | hxq test-summary -` read only ~1583 of 5023 tests and still printed `0 failures` (a partial slice looks green; same hxnodejs stdin pipe limitation as the `Sys.stdin().readAll()` gotcha). For a full-suite run ALWAYS redirect to a file first: `node bin/test.js > out.txt 2>&1; hxq test-summary out.txt`. Piping is safe only for short filtered runs (`APQ_TEST=X node bin/test.js | hxq test-summary -`).
237238

238239
## Report on hxq gaps
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
# hxq lint — full semantics: per-check behavior, --fix scope, suppression, apqlint.json / checkstyle.json config
22

33
**`lint --list-rules`** prints every registered check as `id description`, one per line, and exits — THE authoritative live listing; use it instead of any hardcoded rule list. analysis/check layer; built-in checks — **`unused-import`**: an import whose bound name has no word-boundary occurrence outside the imports → `Warning` ONLY when its declaring module is in the lint file set (verifiable); an out-of-scope NAMED import (stdlib, haxelib, module outside the linted paths) is an unverifiable `Info` advisory that `--fix` never deletes — so narrow-scope runs (`lint test/`) can't false-flag load-bearing imports; a wildcard (`import pkg.*;`) stays `Info`; a `using` is LIVE when its bound name is referenced (a static / type use such as `StringTools.fastCodeAt`) OR one of its extension methods appears as a `.method` member-access — for a known stdlib module (`StringTools` / `Lambda`, resolved via the `GrammarPlugin.knownExtensionMethods` seam, a table extracted from the installed std) a verified-unused `using` is a deletable `Warning`, while an UNKNOWN module stays an unverifiable `Info`; **`unused-local`**: a local `var`/`final` (`VarStmt`/`FinalStmt`) never referenced in its enclosing scope → `Warning` (conservative word-boundary text scan via `RefactorSupport.referencedInRange`; skips the plugin's `opaqueKinds` macro-reification subtrees so splice-injected uses are not false-flagged; params/`for`-iterators/`catch`-vars/fields out of scope); **`duplicate-import`**: an import/using whose (kind, module-path, alias) all match an earlier one in the file → `Warning` on the 2nd+ occurrence (distinct kinds — `import a.B` vs `using a.B` — and distinct aliases kept); **`--fix`** deletes the fixable subset in place (unused imports; unused locals with no initializer or a side-effect-free one; duplicate imports) via the DELETE verb, batched per file, canonical-gated. **NOT delete-only**: it also applies every rule's non-delete `fix()` — member-order is a whole-file canonical REORDER (one out-of-order member relocates ALL members, e.g. 149 relocations/10 files from ~10 findings, one file 239 diff lines) and `prefer-*` transforms (`prefer-single-quotes` `"x"`→`'x'`) apply too. These are canonical-gated on the project's `hxformat.json` — testing in `/tmp` (no config discovered) silently no-ops them, falsely looking delete-only; always test `--fix` on an in-tree file. `lint <dir> --fix` has a HUGE blast radius (one call rewrote 184 files) — scope with a specific file path and/or `--rule <id>`. Info hidden unless `--all`; report-only exit UNLESS `--fail-on <error\|warning\|info>` → exit non-zero when a finding at-or-above `<sev>` survives. **Inline suppression** is applied in `Linter.run` so BOTH the report and `--fix` honour it: a trailing `// noqa` (or `// noqa: <rule>,<rule>` for named rules) clears any finding whose source span COVERS its line — so a `noqa` the writer reflowed onto a continuation line still lands; `// CHECKSTYLE:OFF`…`// CHECKSTYLE:ON` clears a region by the finding's REPORT line (`anyparse.check.Suppression`, an `Entry.region` flag discriminates the two, string-aware comment scan, no parse). **`--format`** machine output via `anyparse.query.format.LintFormat`: `json` records / `checkstyle` XML (symmetric with the `checkstyle.json` the loaders consume). Builtins span the `unused-*` (`import`/`local`/`private`/`parameter`), `dead-code`/`empty-block`/`empty-statement`, structural-correctness (`identical-operands`, `self-assignment`, `duplicate-case`, `comparison-to-boolean`, `redundant-parens`, `redundant-else-after-return`, `collapsible-if`, `double-negation`, `swallowed-exception`, `assignment-in-condition`, `duplicate-ternary-branches`, `constant-condition`, `redundant-map-iter-key`), modernization `prefer-*` (`prefer-final`/`prefer-ternary-return`/`prefer-switch`/`prefer-bind`/`prefer-null-coalescing`/`prefer-array-literal`/`prefer-map-literal`/`prefer-interpolation`/`prefer-single-quotes`, `simplify-boolean-ternary`, `fold-adjacent-string-literals`) and project-style (`naming`, `missing-visibility`, `modifier-order`, `explicit-type`, `complexity`) families. The authoritative, current set is `Linter.builtins()` / the README check table — this line is NOT kept in lockstep, so don't trust a hardcoded count. **Project config `apqlint.json`** (apq-native, walk-up discovered like `checkstyle.json`/`hxformat.json`, in `anyparse.check.LintConfig`): per rule `"enabled":false` drops it from the default set (an explicit `--rule` still runs it), `"severity":"error|warning|info"` overrides the reported severity (remapped in `Linter.run` before report/`--fail-on`), any other key is a rule option (`complexity`'s `"max"`, precedence over a `checkstyle.json` threshold). Missing/malformed → no-op. Walk-up extracted to shared `anyparse.query.ConfigFinder.findUp`; `Severity.fromName` shared with `--fail-on`
4+
5+
**`#if`-blindness and `unused-private` (verified live).** A `#if…#end` conditional-compilation region projects as a single `Conditional` node whose interior is raw preserved trivia — the walkers backing `unused-private` (and `refs`/`uses`/`mentions`/`lit`) never see identifiers used only inside an `#if` body. A member whose only callers live behind a `#if PLATFORM`/`#if debug` guard reads as unused and is a `--fix` deletion candidate even though it compiles and is called under that define. Before trusting a 0-hit "unused" verdict (or letting `--fix` delete on it) in a codebase with conditional compilation, cross-check with a raw-text search over the file (`hxq source`, or grep if the gate allows) instead of relying on the structural walk alone.
6+
7+
**`--fix` fixpoint-looping compounds damage — gate on the project typecheck, not on lint convergence.** Re-running `lint <dir> --fix` pass after pass until it reports 0 findings is unsafe at project scale: if any single fix is wrong (see `#if`-blindness above, or any other fixer bug), the WRONG deletion makes its own helpers/imports look unused on the next pass, and those get deleted too — a cascade that compounds with each iteration and gets harder to unwind the longer it runs. Discipline: run ONE full `--fix` pass, then run the project's own typecheck/build before running `--fix` again — treat the compiler as the oracle, not the linter's own report. Also narrow `--fix` (specific file + `--rule`) rather than a bare directory sweep when the project uses conditional compilation, `using`-extensions invoked via bare `this.method()`, `dynamic` callback-slot functions, extern classes, or serialization-contract typedefs — several fixers are blind to those shapes (deleting an extern member that's part of a native contract, stripping `this.` off a call that only resolves through a `using` extension, dropping a parameter off a `dynamic` function whose signature is a wire/callback contract, or renaming a typedef field that breaks a serialization contract).

templates/skills/workflow-authoring/SKILL.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,14 @@ bug class a single flag eliminates.
186186
**Multi-step pipeline**: chain action states — exec → exec → fetch → prompt
187187
(max chain depth: 20, no agent involvement between action states)
188188

189+
**Loop re-entry needs its own state, not a jump into assess**: when a
190+
fix/retry transition routes back into a general decision state, audit that
191+
state's OUTBOUND transitions first — an early-exit there (`skip → done`)
192+
lets the agent end the loop without re-running the verification the loop
193+
exists to guarantee. Give the retry a dedicated re-entry state that offers
194+
only loop-preserving routes. Dual of the inbound rule: adding a gate before
195+
state X requires auditing ALL transitions INTO X for bypasses.
196+
189197
### Gotchas
190198

191199
- stdout/stderr truncated to `max_output` bytes (default 10KB, configurable per state). Truncation keeps the **tail** (last N bytes), not the head — errors at the end of output are preserved. Use higher values for verbose builds (e.g. `max_output: 51200`)

0 commit comments

Comments
 (0)