java/src/main/resources/targets/claude/skills/core/internal/ops/x-internal-status-update/SKILL.md
Atomic read-modify-write of execution-state.json with flock-based concurrency, schema validation, and idempotency detection. Substitutes inline Edit-based mutations in orchestrator skills (x-epic-implement, x-story-implement, x-pr-fix-epic) that previously suffered race conditions in --parallel mode. PILOT skill for the x-internal-* convention: internal visibility, non-user-invocable, subdir scoping under internal/ops/.
npx skillsauth add edercnj/claude-environment x-internal-status-updateInstall 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-story-implement, x-pr-fix-epic. Esta é a story PILOTO (story-0049-0005) da convenção
x-internal-*: frontmattervisibility: internal, subdirinternal/ops/, marker 🔒, e filtragem do menu/helpvia generator.
Perform atomic read-modify-write mutations of
plans/epic-XXXX/execution-state.json (the telemetry checkpoint file
consumed by every orchestrator skill). The operation:
flock-based advisory lock with 30s timeout.previousValue vs newValue — emits noOp=true when equal
(idempotency per RULE-002).This replaces ad-hoc Edit-tool invocations from inside orchestrators,
which historically lost updates under --parallel execution (documented
in EPIC-0042 post-mortems).
| Aspect | Value | Rationale |
| :--- | :--- | :--- |
| Path | internal/ops/x-internal-status-update/ | 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; all logic is a single shell pipeline |
| Naming | x-internal-{subject}-{action} | Mirrors Rule 04 skill taxonomy; status-update = 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-status-update in chat. All invocations
follow Rule 13 INLINE-SKILL pattern from a calling orchestrator:
Skill(skill: "x-internal-status-update",
args: "--file plans/epic-0049/execution-state.json \
--type story --id story-0049-0005 \
--field status --value MERGED")
| Parameter | Required | Default | Description |
| :--- | :--- | :--- | :--- |
| --file <path> | O | execution-state.json in cwd | Target state file |
| --type <epic\|story\|task> | M | — | Scope of the node being mutated |
| --id <id> | M | — | Epic ID (0049), story ID (story-0049-0005), or task ID (TASK-0049-0005-001) |
| --field <name> | M | — | Schema field to update (e.g., status, prNumber, commitSha) |
| --value <value> | M | — | New value; coerced to the schema-declared type |
| --initialize | O | false | Create the file with an empty schema skeleton when absent |
| --read-only | O | false | Read and return the current value without acquiring the write lock |
When successful, the skill writes a single-line JSON object to stdout:
| Field | Type | Always Present | Description |
| :--- | :--- | :--- | :--- |
| previousValue | String\|Null | yes | Value before write; null when field was absent |
| newValue | String | yes | Value after write (equal to previous on no-op) |
| fileSha | String(64) | yes | sha256 of the file after the write |
| noOp | Boolean | yes | true when previousValue == newValue |
| Code | Name | Condition | Message Format |
| :--- | :--- | :--- | :--- |
| 0 | SUCCESS | Write or no-op completed | — |
| 1 | FILE_NOT_FOUND | File missing and --initialize=false | State file not found: <path> |
| 2 | LOCK_TIMEOUT | flock timed out after 30s | Lock timeout on <path>.lock |
| 3 | INVALID_PATH | --type/--id/--field tuple does not resolve | Path '<resolved-path>' not found in schema |
| 4 | WRITE_FAILED | Atomic mv of tmp file failed | Atomic write failed: <stderr> |
Parse flags; reject unknown flags; enforce mutual exclusivity of
--read-only with any write-implying flag combination. When
--type=epic, the resolved path is <field> at document root; when
--type=story, the path is stories.<id>.<field>; when --type=task,
the path is stories.<parentStoryId>.tasks.<id>.<field>. Parent story
ID is inferred from the task ID prefix (TASK-0049-0005-001 ⇒
story-0049-0005).
lock_file="${file}.lock"
exec {fd}>"${lock_file}"
if ! flock -w 30 "${fd}"; then
echo "Lock timeout on ${lock_file}" >&2
exit 2
fi
--initialize=false: exit 1.--initialize=true: write an empty skeleton
{"version":1,"stories":{}} via the same atomic tmp+rename contract
before proceeding.Parse JSON via jq; resolve the path per Step 1. When the path does
not exist, exit 3 with the resolved path in the message.
Compute previousValue at the resolved path. When
previousValue == newValue, emit the response JSON with noOp=true
and exit 0 without touching the file. The lock is still released via
flock descriptor closure.
tmp="${file}.tmp.$$"
jq --arg v "${newValue}" "<path-expression>" "${file}" > "${tmp}"
if ! mv "${tmp}" "${file}"; then
rm -f "${tmp}"
echo "Atomic write failed: mv returned non-zero" >&2
exit 4
fi
file_sha=$(shasum -a 256 "${file}" | cut -d' ' -f1)
printf '{"previousValue":%s,"newValue":"%s","fileSha":"%s","noOp":false}\n' \
"${prev_json}" "${newValue}" "${file_sha}"
The flock descriptor is closed on process exit; no explicit unlock
is required.
--read-only short-circuitWhen --read-only=true, Steps 2, 6, and 7 are skipped. The skill
opens the file with a shared (flock -s) lock, reads the value,
emits the response with noOp=true and newValue==previousValue,
and exits 0.
Skill(skill: "x-internal-status-update",
args: "--file plans/epic-0049/execution-state.json \
--type story --id story-0049-0005 \
--field status --value MERGED")
Output:
{"previousValue":"IN_PROGRESS","newValue":"MERGED","fileSha":"a1b2...","noOp":false}
Exit: 0.
Skill(skill: "x-internal-status-update",
args: "--file plans/epic-0049/execution-state.json \
--type story --id story-0049-0005 \
--field status --value MERGED")
Output:
{"previousValue":"MERGED","newValue":"MERGED","fileSha":"a1b2...","noOp":true}
Exit: 0.
Skill(skill: "x-internal-status-update",
args: "--file plans/epic-0049/execution-state.json \
--type epic --id 0049 \
--field flowVersion --value 2 \
--initialize")
Output:
{"previousValue":null,"newValue":"2","fileSha":"c3d4...","noOp":false}
Exit: 0.
Skill(skill: "x-internal-status-update",
args: "--file plans/epic-0049/execution-state.json \
--type task --id TASK-0049-0005-003 \
--field prNumber --value 612")
Output:
{"previousValue":null,"newValue":"612","fileSha":"e5f6...","noOp":false}
Exit: 0.
Skill(skill: "x-internal-status-update",
args: "--file plans/epic-0049/execution-state.json \
--type story --id story-0049-0005 \
--field status --value UNUSED \
--read-only")
Output:
{"previousValue":"MERGED","newValue":"MERGED","fileSha":"a1b2...","noOp":true}
Exit: 0. Note: --value is required by the argument schema but is
ignored under --read-only.
Skill(skill: "x-internal-status-update",
args: "--file plans/epic-0049/execution-state.json \
--type story --id unknown-story \
--field status --value DONE")
Stderr:
Path 'stories.unknown-story.status' not found in schema
Exit: 3.
| Artifact | Path | Description |
| :--- | :--- | :--- |
| Updated state file | <--file> | JSON document mutated in place via atomic rename |
| Response envelope | stdout | Single-line JSON (previousValue / newValue / fileSha / noOp) |
| Lock file | <--file>.lock | Created empty on first invocation; retained for reuse |
| 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 lock |
| Concurrent invocation exceeds 30s wait | Exit 2 (LOCK_TIMEOUT) — caller retries with backoff |
| mv fails mid-write | Delete tmp file; exit 4 (WRITE_FAILED); state file is left untouched |
| --initialize collides with existing non-JSON file | Exit 4 — do not overwrite |
| Schema version mismatch | Log warning to stderr; proceed (non-blocking per RULE-006) |
<file>.lock file
descriptor. Exclusive (flock -x) for writes; shared (flock -s)
for --read-only.previousValue — this is the correct "last writer wins" semantic.trap-based cleanup required.Every write operation that stages and commits execution-state.json
MUST inject the canonical trailer into the commit message so that
the .githooks/commit-msg hook (story-0059-0004 surface F guard) allows
the commit to proceed.
Co-Authored-By: x-internal-status-update@<40-char-git-sha>
<sha> is the HEAD commit of the repository at the moment the
skill executes ($(git rev-parse HEAD)).Co-Authored-By: (parseable via
git interpret-trailers).When this skill issues a git commit that includes execution-state.json
in the staged files, it MUST pass the trailer:
SKILL_SHA=$(git rev-parse HEAD)
git commit -m "<subject>" \
--trailer "Co-Authored-By: x-internal-status-update@${SKILL_SHA}"
This trailer is validated by .githooks/commit-msg which checks:
git interpret-trailers --parse < "$COMMIT_MSG_FILE" \
| grep -qE '^Co-Authored-By:\s+x-internal-status-update@[0-9a-f]{40}$'
In documented recovery operations where the operator manually edits
execution-state.json (e.g., state corruption), the trailer MUST
still be present. The operator adds the trailer with an approved SHA.
The hook validates format only, not SHA authenticity (RULE-059).
The PILOT story (story-0049-0005) ships the following acceptance test
scenarios, which are the reference contract every future x-internal-*
skill MUST replicate in its own directory:
noOp=false and fileSha changes.noOp=true, file
mtime unchanged.--initialize; exit 1.Goldens under
src/test/resources/golden/internal/ops/x-internal-status-update/
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-...") invocations from other skills
resolve correctly. The invariant: user cannot see them; orchestrators
can invoke them.
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 2 (per-story status transitions) + Phase 4 (epic finalization) |
| x-story-implement | caller | Phase 2 (per-task status transitions) + Phase 3 (story finalization) |
| x-pr-fix-epic | caller | Records per-PR correction state when fanning out across an epic |
| x-status-reconcile | peer | Reads the same file to diagnose drift against markdown; never mutates concurrently with this skill (Rule 22) |
| x-parallel-eval | consumer | Reads the resulting state file to build the collision matrix |
Downstream stories that depend on this PILOT: story-0049-0013, story-0049-0018, story-0049-0019.
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).