java/src/main/resources/targets/claude/skills/core/internal/ops/x-internal-report-write/SKILL.md
Renders _TEMPLATE-*.md templates by substituting {{KEY}} placeholders (simple and nested, dot-path) and resolving {{#each}} loops against a structured JSON data payload, then writes the result atomically to an output path. Supports an --append mode with per-section deduplication keyed by `## ID: <value>` markers. Centralises phase reports, epic execution reports and planning reports so orchestrators (x-epic-implement, x-epic-orchestrate, x-story-implement) stop duplicating `Read template + inline Edit/Write` logic. Second skill in the x-internal-* convention (after x-internal-status-update pilot): internal visibility, non-user-invocable, subdir scoping under internal/ops/.
npx skillsauth add edercnj/claude-environment x-internal-report-writeInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
🔒 INTERNAL SKILL Esta skill é invocada apenas por outras skills (orquestradores). NÃO é destinada a invocação direta pelo usuário. Caller principal: x-epic-implement, x-epic-orchestrate, x-story-implement. Segunda skill da convenção
x-internal-*(após x-internal-status-update, a story PILOTO 0049-0005): frontmattervisibility: internal, subdirinternal/ops/, marker 🔒 e filtragem do menu/helpvia generator.
Render a template file under .claude/templates/ (or an explicit path) by:
@path/to/data.json).{{KEY}} placeholders, including nested dot-paths
({{stories.story-0049-0001.status}}).{{#each <collection>}}…{{/each}} blocks with block-local
placeholder resolution against each array element.--append=true.This replaces the ad-hoc Read template + Write output pattern currently
inlined in x-epic-implement, x-epic-orchestrate, and x-story-implement
(documented in EPIC-0049 S9, S10, S15 analyses), centralising report
rendering so templates can evolve without editing N orchestrator skills.
| Aspect | Value | Rationale |
| :--- | :--- | :--- |
| Path | internal/ops/x-internal-report-write/ | internal/ prefix scopes visibility; ops/ aligns with sibling runtime-ops skills |
| Frontmatter visibility | internal | Generator filters these from /help menu |
| Frontmatter user-invocable | false | Declarative complement to visibility: internal |
| Body marker | > 🔒 **INTERNAL SKILL** block as first non-frontmatter content | Visible to humans browsing the repo; no parsing required |
| Allowed tools | Bash only | Minimal surface; renderer is a single shell pipeline built on jq |
| Naming | x-internal-{subject}-{action} | Mirrors Rule 04 skill taxonomy; report-write = subject+action |
Audit rule: Rule 22 (Lifecycle Integrity) validates every skill under
internal/** satisfies all 6 anchors above. Violations fail the
LifecycleIntegrityAuditTest.
Bare-slash form is intentionally omitted — this skill is never invoked
by a human typing /x-internal-report-write in chat. All invocations
follow Rule 13 INLINE-SKILL pattern from a calling orchestrator:
Skill(skill: "x-internal-report-write",
args: "--template _TEMPLATE-EPIC-EXECUTION-PLAN.md \
--output plans/epic-0049/reports/exec-plan.md \
--data @plans/epic-0049/reports/exec-plan.data.json")
| Parameter | Required | Default | Description |
| :--- | :--- | :--- | :--- |
| --template <name> | M | — | Template file name resolved under .claude/templates/<name>. An absolute path is accepted verbatim |
| --output <path> | M | — | Target path for the rendered file. Created with parent directories |
| --data <json-or-@path> | M | — | Structured payload: inline JSON string, or @path/to/data.json to read from disk |
| --append | O | false | When true, merge the rendered output into an existing file using ## ID: <value> section markers for deduplication (see Step 6) |
When successful, the skill writes a single-line JSON object to stdout:
| Field | Type | Always Present | Description |
| :--- | :--- | :--- | :--- |
| outputPath | String | yes | Absolute path of the written file |
| bytesWritten | Integer | yes | Final size of the output file in bytes |
| placeholdersReplaced | Integer | yes | Total count of {{KEY}} substitutions (loop expansions count each inner substitution once) |
| entriesAppended | Integer\|Null | only on --append=true | Number of new ## ID: sections appended (existing sections updated in place do not increment this count) |
| Code | Name | Condition | Message Format |
| :--- | :--- | :--- | :--- |
| 0 | SUCCESS | Render and write completed | — |
| 1 | TEMPLATE_NOT_FOUND | Template file cannot be read | Template '<name>' not found in .claude/templates/ |
| 2 | INVALID_JSON | --data payload fails to parse | Invalid JSON in --data |
| 3 | UNRESOLVED_PLACEHOLDER | Strict-mode placeholder has no value in data | Placeholder '{{KEY}}' has no value (strict mode) |
| 4 | WRITE_FAILED | Atomic mv of tmp file failed | Failed to write <path> |
{{key}} — top-level scalar lookup against the data object.{{a.b.c}} — dot-path into nested objects. Arrays are indexable via
{{stories.0.id}} but this form is discouraged outside tests; use
{{#each}} for list rendering.UNRESOLVED_PLACEHOLDER
(exit 3). The error message includes the exact placeholder token.{{#each <collection>}} … {{/each}}stories, findings, tasks.pending).{{id}}, {{status}}) with
outer context ({{epicId}}).{{#each}} needs exactly one matching
{{/each}} at the same nesting depth. Nested loops are allowed up to
depth 3 (templates-in-the-wild never exceed depth 2).Parse flags; reject unknown flags. Reject duplicate flags. Enforce:
--template, --output, --data all mandatory.--append is boolean; absence defaults to false.When --template is a bare name (no /), resolve against
<CLAUDE_PROJECT_DIR>/.claude/templates/<name>; an absolute or
relative path containing / is used verbatim (subject to traversal
guard against .. above the project root per Rule 06).
if [[ ! -r "${template_path}" ]]; then
echo "Template '${template_name}' not found in .claude/templates/" >&2
exit 1
fi
template_body=$(cat "${template_path}")
if [[ "${data_arg}" == @* ]]; then
data_path="${data_arg#@}"
[[ -r "${data_path}" ]] || { echo "Invalid JSON in --data" >&2; exit 2; }
data_json=$(cat "${data_path}")
else
data_json="${data_arg}"
fi
if ! echo "${data_json}" | jq -e '.' >/dev/null 2>&1; then
echo "Invalid JSON in --data" >&2
exit 2
fi
{{#each}} expansion (inside-out)Expand the deepest {{#each}} block first, substituting per-element
placeholders against the current array element (with fallback to root
data). Repeat until no {{#each}} tokens remain. A simple regex-based
state machine is sufficient because blocks are well-bracketed and
never straddle line continuations.
Implementation contract:
while echo "${body}" | grep -q '{{#each '; do
body=$(expand_innermost_each "${body}" "${data_json}")
done
The helper expand_innermost_each:
{{#each <expr>}} … {{/each}} match.<expr> via jq against ${data_json}; if the result is
not an array, exit with a clear error (non-zero, message includes
the expression).{{field}}
or {{field.nested}}), falling back to the root data.{{#each}}…{{/each}} span in the body.After all {{#each}} blocks are resolved, walk remaining
{{<dot-path>}} tokens and substitute each via jq -r. When the
lookup yields null or the path is absent, exit 3 with
UNRESOLVED_PLACEHOLDER and the exact token in the message. Count
every successful substitution into placeholdersReplaced.
Overwrite mode (default):
mkdir -p "$(dirname "${output_path}")"
tmp="${output_path}.tmp.$$"
printf '%s' "${rendered}" > "${tmp}"
if ! mv "${tmp}" "${output_path}"; then
rm -f "${tmp}"
echo "Failed to write ${output_path}" >&2
exit 4
fi
Append mode (--append=true): the rendered body is interpreted as
a sequence of one or more sections, each beginning with a line of the
form ## ID: <value> (leading ## required; value is the trimmed
remainder). For every such section in the rendered body:
## ID: <value>
marker.## ID:
marker or EOF) with the new section body. Do not increment
entriesAppended.entriesAppended.Both branches end with the same atomic mv tmp → final contract as
overwrite mode. The append merge is performed in-memory (a single
awk/jq pipeline), never line-by-line via interleaved writes.
bytes=$(wc -c < "${output_path}" | tr -d ' ')
if [[ "${append_mode}" == true ]]; then
printf '{"outputPath":"%s","bytesWritten":%s,"placeholdersReplaced":%s,"entriesAppended":%s}\n' \
"${output_path}" "${bytes}" "${replaced}" "${appended}"
else
printf '{"outputPath":"%s","bytesWritten":%s,"placeholdersReplaced":%s,"entriesAppended":null}\n' \
"${output_path}" "${bytes}" "${replaced}"
fi
Exit 0.
Template .claude/templates/_TEMPLATE-EPIC-HEADER.md:
# Epic {{epicId}} — {{title}}
Status: {{status}}
Invocation:
Skill(skill: "x-internal-report-write",
args: "--template _TEMPLATE-EPIC-HEADER.md \
--output plans/epic-0049/reports/header.md \
--data '{\"epicId\":\"0049\",\"title\":\"Skill hygiene\",\"status\":\"IN_PROGRESS\"}'")
Output file contains Epic 0049 — Skill hygiene and Status: IN_PROGRESS.
Stdout:
{"outputPath":"plans/epic-0049/reports/header.md","bytesWritten":52,"placeholdersReplaced":3,"entriesAppended":null}
Exit: 0.
{{#each}} loop with per-item fieldsTemplate fragment:
{{#each stories}}
- {{id}} ({{status}})
{{/each}}
Data:
{"stories":[{"id":"story-0049-0001","status":"DONE"},
{"id":"story-0049-0002","status":"DONE"},
{"id":"story-0049-0006","status":"IN_PROGRESS"}]}
Rendered body:
- story-0049-0001 (DONE)
- story-0049-0002 (DONE)
- story-0049-0006 (IN_PROGRESS)
placeholdersReplaced = 6 (two per iteration).
--append with update in place (no duplication)Existing plans/epic-0049/reports/status.md:
## ID: story-0049-0001
status: PENDING
## ID: story-0049-0002
status: PENDING
Invocation with data {"id":"story-0049-0001","status":"DONE"} and a
template producing the two-line ## ID: ... / status: ... block:
Skill(skill: "x-internal-report-write",
args: "--template _TEMPLATE-STATUS-ENTRY.md \
--output plans/epic-0049/reports/status.md \
--data '{\"id\":\"story-0049-0001\",\"status\":\"DONE\"}' \
--append true")
Final file:
## ID: story-0049-0001
status: DONE
## ID: story-0049-0002
status: PENDING
Stdout:
{"outputPath":"plans/epic-0049/reports/status.md","bytesWritten":74,"placeholdersReplaced":2,"entriesAppended":0}
Exit: 0. Note entriesAppended=0 because the rendered ID already
existed and was updated in place.
--append with new entriesSame output file as Example 3; data now introduces an unseen ID:
{"id":"story-0049-0006","status":"IN_PROGRESS"}
Final file gains a third section at the end:
## ID: story-0049-0001
status: DONE
## ID: story-0049-0002
status: PENDING
## ID: story-0049-0006
status: IN_PROGRESS
Stdout: "entriesAppended":1.
Skill(skill: "x-internal-report-write",
args: "--template _TEMPLATE-NONEXISTENT.md \
--output /tmp/out.md \
--data '{}'")
Stderr:
Template '_TEMPLATE-NONEXISTENT.md' not found in .claude/templates/
Exit: 1.
Template contains {{undefined_key}}; data is {"epicId":"0049"}.
Stderr:
Placeholder '{{undefined_key}}' has no value (strict mode)
Exit: 3.
--data '{epicId:0049}' (unquoted keys).
Stderr:
Invalid JSON in --data
Exit: 2.
| Artifact | Path | Description |
| :--- | :--- | :--- |
| Rendered report | <--output> | Markdown file written via tmp+rename; overwrite or merged-append |
| Response envelope | stdout | Single-line JSON (outputPath / bytesWritten / placeholdersReplaced / entriesAppended) |
| Temp file | <--output>.tmp.$$ | Removed on successful rename; removed on failure |
| Scenario | Action |
| :--- | :--- |
| Missing required flag | Print usage: banner to stderr; exit 64 (sysexits EX_USAGE) |
| jq absent on PATH | Exit 127 with jq is required; abort before any write |
| --data @path points to unreadable file | Exit 2 (INVALID_JSON) with the path echoed |
| {{#each expr}} resolves to non-array | Exit non-zero; message includes the expression and the resolved type |
| Unbalanced {{#each}} / {{/each}} | Exit non-zero; message identifies the dangling token |
| Strict-mode miss on any placeholder | Exit 3 (UNRESOLVED_PLACEHOLDER) with the full token text |
| mv fails mid-write | Delete tmp file; exit 4 (WRITE_FAILED); existing output file is left untouched |
| Output parent directory not writable | Exit 4 after cleaning up the tmp file |
| Path traversal attempt (.. above project root) | Exit non-zero; message names the offending path (Rule 06) |
{{#each}}
levels (DoD-local target of story-0049-0006).Story-0049-0006 ships acceptance test scenarios that every future
x-internal-report-write consumer MUST be able to rely on:
{{epicId}} substituted; placeholdersReplaced=1.{{stories.story-0049-0001.status}} resolves.{{#each}} happy path — array of three stories renders three lines.{{#each}} empty collection — zero iterations, no error.--append update in place — existing ## ID: section replaced;
entriesAppended=0; file sha256 changes; no duplicate section.--append new entry — unseen ID appended at EOF;
entriesAppended=1.--data; exit 2.Goldens under
src/test/resources/golden/internal/ops/x-internal-report-write/ lock
the SKILL.md rendering. Coverage requirement: ≥ 95% line / ≥ 90%
branch across the invoking Bash codepaths.
The ia-dev-env generator MUST exclude skills with
visibility: internal from:
.claude/README.md skill-inventory table./help menu listing surfaced by Claude Code.Internal skills are still copied into .claude/skills/ (flat layout)
so Skill(skill: "x-internal-report-write") invocations from other
skills resolve correctly. The invariant — set by the pilot story
(0049-0005) — holds here: user cannot see it; orchestrators can
invoke it.
Internal skills DO NOT emit phase.start / phase.end markers —
telemetry is produced by the invoking orchestrator (the phase wrapping
the orchestrator's own step is the correct aggregation boundary).
Passive hooks still capture tool.call for the underlying Bash
invocation.
Reference: Rule 13 (Skill Invocation Protocol), Rule 22 (Lifecycle Integrity Audit), ADR-0010 (Interactive Gates Convention — exempts internal skills from the 3-option menu contract).
| Skill | Relationship | Context |
| :--- | :--- | :--- |
| x-epic-implement | caller | Phase-completion reports, epic execution plan rendering |
| x-epic-orchestrate | caller | Per-story planning report rendering |
| x-story-implement | caller | Story-level remediation / review dashboard rendering |
| x-internal-status-update | peer | Sibling x-internal-* skill; both scoped under internal/ops/; no shared runtime state |
| x-status-reconcile | consumer | Downstream reader of reports produced here; never mutates concurrently (Rule 22) |
Downstream stories that depend on this skill: story-0049-0009 (x-epic-implement consumes it), story-0049-0010 (x-epic-orchestrate consumes it), story-0049-0015 (x-story-implement consumes it).
tools
Documentation automation v2: stack-aware generation from documentation.targets.
development
Generates or updates CI/CD pipelines per project stack with actionlint validation.
tools
Generates ADRs from architecture-plan mini-ADRs with sequential numbering and index update.
development
Formats source code; first step of the pre-commit chain (format -> lint -> compile).