feat(admin-config-tool): task 1.3 - nix and xce-build.sh integration

Add python3 + pycapnp/pyyaml/jsonschema/click/pytest/ruff (+pip) to the
nix dev shell, wire a new `xceadmin` case into xce-build.sh (schema
sync from src/schema/ and src/configs/schema/, pip install by profile,
pytest), package-data config so bundled schemas survive a non-editable
install, xce-build SKILL.md docs, and ruff format/check hooks matching
this repo's stdin-JSON hook convention (not brief.md's env-var sketch,
which would have silently no-op'd).

Verified live end-to-end inside `nix develop ./src` for both the
default (editable) and release (pinned) profiles; found and fixed 3
real bugs along the way (missing pip, PEP 668 externally-managed-
environment, missing package-data for release-profile bundling) —
logged as bug-1458/1459/1460.

Requirements: 5.5, 8.1, 8.2, 8.6
This commit is contained in:
Rongsong Shen 2026-07-22 03:57:43 +00:00
parent 2e4ef4e209
commit 798f19b83a
7 changed files with 92 additions and 5 deletions

View file

@ -22,6 +22,16 @@
"type": "command", "type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR/.wolf/hooks/post-write.js\"", "command": "node \"$CLAUDE_PROJECT_DIR/.wolf/hooks/post-write.js\"",
"timeout": 10 "timeout": 10
},
{
"type": "command",
"command": "FILE_PATH=$(python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('file_path') or d.get('tool_input',{}).get('path') or '')\" 2>/dev/null); [[ \"$FILE_PATH\" == *.py ]] && ruff format --quiet \"$FILE_PATH\" 2>/dev/null; true",
"timeout": 10
},
{
"type": "command",
"command": "FILE_PATH=$(python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('file_path') or d.get('tool_input',{}).get('path') or '')\" 2>/dev/null); [[ \"$FILE_PATH\" == *.py ]] && ruff check --quiet \"$FILE_PATH\" 2>/dev/null; true",
"timeout": 10
} }
] ]
} }

View file

@ -1,6 +1,6 @@
--- ---
name: xce-build name: xce-build
description: "Build XCE components with tests. Usage: /xce-build [component] [profile] [build_dir]. Components: all (default), lib, controller, scheduler, worker, apiserver, xcecli. Profiles: asan (default), debug (non-ASAN -O0 -g3 for gdb/cores), release." description: "Build XCE components with tests. Usage: /xce-build [component] [profile] [build_dir]. Components: all (default), lib, controller, scheduler, worker, apiserver, xcecli, xceadmin. Profiles: asan (default), debug (non-ASAN -O0 -g3 for gdb/cores), release."
disable-model-invocation: false disable-model-invocation: false
allowed-tools: Bash(*) allowed-tools: Bash(*)
--- ---
@ -10,12 +10,13 @@ allowed-tools: Bash(*)
Args: `[component] [profile|build_dir] [build_dir]` — passed as free text after `/xce-build`, e.g. `/xce-build controller debug`. Parse the ARGUMENTS text yourself and run the command below with the parsed values inline. Args: `[component] [profile|build_dir] [build_dir]` — passed as free text after `/xce-build`, e.g. `/xce-build controller debug`. Parse the ARGUMENTS text yourself and run the command below with the parsed values inline.
- **No `context: fork`, intentionally.** It was tried (twice) and reproducibly breaks this skill: whenever ARGUMENTS is non-empty (e.g. `controller`), the forked subagent receives no usable task — it responds as if handed only ambient project context, never runs the build, and reports nothing back. Bare `/xce-build` (no args) happens to still "succeed" under `context: fork` only because the empty-arg case is indistinguishable from no task at all failing open into some default — not because forking works. Do not re-add `context: fork` here without first confirming the harness actually threads a non-empty ARGUMENTS payload into the forked subagent's prompt. - **No `context: fork`, intentionally.** It was tried (twice) and reproducibly breaks this skill: whenever ARGUMENTS is non-empty (e.g. `controller`), the forked subagent receives no usable task — it responds as if handed only ambient project context, never runs the build, and reports nothing back. Bare `/xce-build` (no args) happens to still "succeed" under `context: fork` only because the empty-arg case is indistinguishable from no task at all failing open into some default — not because forking works. Do not re-add `context: fork` here without first confirming the harness actually threads a non-empty ARGUMENTS payload into the forked subagent's prompt.
- **component** (1st word, default `all`) — `all`, `lib`, `controller`, `scheduler`, `worker`, `apiserver`, `xcecli` - **component** (1st word, default `all`) — `all`, `lib`, `controller`, `scheduler`, `worker`, `apiserver`, `xcecli`, `xceadmin`
- **profile** (2nd word, default `asan`) — `asan` (debug + AddressSanitizer → `xce.build`), `debug` (non-ASAN `-O0 -g3 -fno-inline -fno-omit-frame-pointer` → `build.dbg`), `release` (`-O3`, no sanitizer → `build.release`). Each profile uses its own default build dir so the variants never clobber each other. If the 2nd word isn't one of these three keywords, treat it as a legacy explicit build_dir instead (3rd word, if present, always overrides the build dir). - **profile** (2nd word, default `asan`) — `asan` (debug + AddressSanitizer → `xce.build`), `debug` (non-ASAN `-O0 -g3 -fno-inline -fno-omit-frame-pointer` → `build.dbg`), `release` (`-O3`, no sanitizer → `build.release`). Each profile uses its own default build dir so the variants never clobber each other. If the 2nd word isn't one of these three keywords, treat it as a legacy explicit build_dir instead (3rd word, if present, always overrides the build dir).
- Use the **`debug`** profile to root-cause heisenbug crashes that ASAN perturbs away and that gdb-on-release turns into a hang/timeout — the non-ASAN `-O0 -g3` build reproduces them and gives a fully symbolized backtrace under gdb (how bug-1282/bug-1286 were cracked). - Use the **`debug`** profile to root-cause heisenbug crashes that ASAN perturbs away and that gdb-on-release turns into a hang/timeout — the non-ASAN `-O0 -g3` build reproduces them and gives a fully symbolized backtrace under gdb (how bug-1282/bug-1286 were cracked).
- **`controller` always builds `xce_test` too** (`meson compile … xce_controller controller_unit_test controller/xce_test`), in every profile — not just `asan`. `xce_test` is the driver `/xce-test-ha` needs; you never have to build it as a separate step after `/xce-build controller`, and the two binaries in a build dir are always the same commit/profile in lockstep. - **`controller` always builds `xce_test` too** (`meson compile … xce_controller controller_unit_test controller/xce_test`), in every profile — not just `asan`. `xce_test` is the driver `/xce-test-ha` needs; you never have to build it as a separate step after `/xce-build controller`, and the two binaries in a build dir are always the same commit/profile in lockstep.
- **`xceadmin` is Python, not meson, and ignores the ASAN/debug/release sanitizer semantics entirely** — it is dispatched by a plain shell case branch in `xce-build.sh`, deliberately not wired into `src/meson.build`/`meson.options` (see `.kiro/specs/admin-config-tool/design.md`). The `profile` arg still matters for it, but only to pick the pip install mode: default/`asan`/`debug` → `pip install -e .` (editable, for dev); `release` → `pip install .` (pinned, non-editable, for production). Every invocation — either install mode — syncs the bundled `controller.capnp`/`c.capnp` and the 7 JSON validation schemas from `src/schema/` and `src/configs/schema/` into `xcecommon`'s packaged `schema_files/`, then runs `pytest` against whichever install just happened. Only the `release` run actually exercises the packaged (non-editable) schema resolution path — an editable-only test run can't catch a `package_data` regression there. Requires the nix dev shell (`pycapnp`, `pyyaml`, `jsonschema`, `click`, `pytest`, `ruff` — all provided by `src/nixSupport/xce.nix`).
Examples: `/xce-build controller` (ASAN), `/xce-build controller debug` (gdb/core build), `/xce-build all release`. Examples: `/xce-build controller` (ASAN), `/xce-build controller debug` (gdb/core build), `/xce-build all release`, `/xce-build xceadmin` (editable install + tests), `/xce-build xceadmin release` (pinned install + tests).
Run (substituting the parsed component/profile/build_dir; omit trailing empty ones): Run (substituting the parsed component/profile/build_dir; omit trailing empty ones):

View file

@ -13,7 +13,7 @@
- Observable: raising any of these subclasses and catching the common base type recovers the message - Observable: raising any of these subclasses and catching the common base type recovers the message
- _Requirements: 7.1, 7.2, 7.3, 7.4_ - _Requirements: 7.1, 7.2, 7.3, 7.4_
- [ ] 1.3 Wire xceadmin into the project's build and dev-shell tooling - [x] 1.3 Wire xceadmin into the project's build and dev-shell tooling
- Nix dev shell provides the Python interpreter and all dependencies needed to build and test xceadmin - Nix dev shell provides the Python interpreter and all dependencies needed to build and test xceadmin
- Running the build command for xceadmin performs an editable install by default and a pinned non-editable install under the release profile, running the test suite after whichever install just happened - Running the build command for xceadmin performs an editable install by default and a pinned non-editable install under the release profile, running the test suite after whichever install just happened
- The build step keeps the packaged copies of the wire-protocol schema and the validation schemas in sync with their source locations elsewhere in the repo, and packages both correctly under either install mode - The build step keeps the packaged copies of the wire-protocol schema and the validation schemas in sync with their source locations elsewhere in the repo, and packages both correctly under either install mode
@ -140,3 +140,9 @@
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 3.1, 3.2, 3.3, 3.4, 6.1, 6.2, 6.3, 7.1, 7.2, 7.3_ - _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 3.1, 3.2, 3.3, 3.4, 6.1, 6.2, 6.3, 7.1, 7.2, 7.3_
- _Boundary: xceadmin.push, xceadmin.discovery_ - _Boundary: xceadmin.push, xceadmin.discovery_
- _Depends: 3.3_ - _Depends: 3.3_
## Implementation Notes
- **1.3**: nix's bare `python3` does not put `pip` on PATH — `python3Packages.pip` had to be added to `nativeBuildInputs` alongside the packages design.md's Technology Stack table lists; not a spec gap, just an operational dependency the table didn't need to spell out.
- **1.3**: Claude Code `PostToolUse` hooks receive tool input via stdin JSON (`tool_input.file_path`), not a `$FILE_PATH` env var — brief.md's ruff-hook sketch used the env-var form, which would have silently no-op'd. The actual hooks added to `.claude/settings.json` follow this repo's existing `.wolf/hooks/post-write.js` stdin-JSON convention instead. Any future hook work in this repo should follow the same pattern, not brief.md's original sketch.
- **1.3**: `src/xceadmin/src/xcecommon/schema_files/` (synced capnp + JSON schema copies) is a build artifact, regenerated every `xce-build.sh xceadmin` run — never hand-edit it, never commit it (`src/xceadmin/.gitignore` excludes it).

View file

@ -61,6 +61,18 @@ stdenv.mkDerivation {
})) }))
packcc packcc
cacert cacert
# Python — for xceadmin (admin config tool, src/xceadmin/). pycapnp links
# against the capnproto C++ runtime above; nix's python3Packages.pycapnp
# handles that link correctly, unlike a plain `pip install pycapnp`.
# See .kiro/specs/admin-config-tool/brief.md "Nix Environment".
python3
python3Packages.pip
python3Packages.pycapnp
python3Packages.pyyaml
python3Packages.jsonschema
python3Packages.click
python3Packages.pytest
python3Packages.ruff
] ++ (if withDebug then [ ] ++ (if withDebug then [
(librtti_debug.overrideAttrs (oldattrs: { (librtti_debug.overrideAttrs (oldattrs: {
separateDebugInfo = true; separateDebugInfo = true;

View file

@ -49,7 +49,7 @@ cd "$SRC_DIR"
usage() { usage() {
echo "Usage: $0 [component] [profile|build_dir] [build_dir]" echo "Usage: $0 [component] [profile|build_dir] [build_dir]"
echo "Components: all (default), lib, controller, scheduler, worker, apiserver, xcecli" echo "Components: all (default), lib, controller, scheduler, worker, apiserver, xcecli, xceadmin"
echo "Profiles: asan (default), debug (non-ASAN -O0 -g3, for gdb/cores), release" echo "Profiles: asan (default), debug (non-ASAN -O0 -g3, for gdb/cores), release"
exit 1 exit 1
} }
@ -112,6 +112,43 @@ case "$COMPONENT" in
cd frontend/xcecli && CGO_ENABLED=0 go test ./... -count=1 cd frontend/xcecli && CGO_ENABLED=0 go test ./... -count=1
fi fi
;; ;;
xceadmin)
# Python — no meson, dispatched by this case statement alone (design
# review correction: xceadmin is deliberately NOT wired into
# src/meson.build/meson.options, see .kiro/specs/admin-config-tool/
# design.md, File Structure Plan). Ignores $BUILD_DIR/$MESON_OPTS;
# $PROFILE only selects editable vs. pinned pip install below.
XCEADMIN_DIR="$SRC_DIR/xceadmin"
SCHEMA_DST="$XCEADMIN_DIR/src/xcecommon/schema_files"
echo "=== Syncing xceadmin bundled schemas ==="
mkdir -p "$SCHEMA_DST/json"
cp "$SRC_DIR/schema/controller.capnp" "$SCHEMA_DST/controller.capnp"
cp "$SRC_DIR/schema/c.capnp" "$SCHEMA_DST/c.capnp"
cp "$SRC_DIR/configs/schema/"*.schema.json "$SCHEMA_DST/json/"
cd "$XCEADMIN_DIR"
# nix's python3 site-packages lives under the read-only /nix/store,
# so `pip install` (editable or pinned) can't target it directly —
# it refuses with "externally-managed-environment" (PEP 668). Use a
# throwaway venv that inherits the nix-provided pycapnp/pyyaml/
# jsonschema/click/pytest (--system-site-packages) so pip only ever
# has to install xceadmin itself, never rebuild those from PyPI.
# Recreated on every invocation for a clean, reproducible install —
# same philosophy as setup_build's `rm -rf "$BUILD_DIR"` above.
VENV_DIR="$XCEADMIN_DIR/.venv"
rm -rf "$VENV_DIR"
python3 -m venv --system-site-packages "$VENV_DIR"
if [ "$PROFILE" = "release" ]; then
echo "=== Building xceadmin Python package (pinned, non-editable) ==="
"$VENV_DIR/bin/pip" install . --quiet
else
echo "=== Building xceadmin Python package (editable) ==="
"$VENV_DIR/bin/pip" install -e . --quiet
fi
echo "=== Running xceadmin tests ==="
"$VENV_DIR/bin/python" -m pytest tests/ -v
echo "=== Done: xceadmin (profile=$PROFILE) ==="
exit 0
;;
all) all)
setup_build setup_build
echo "=== Building all components ===" echo "=== Building all components ==="

12
src/xceadmin/.gitignore vendored Normal file
View file

@ -0,0 +1,12 @@
# Build artifacts
build/
*.egg-info/
__pycache__/
*.pyc
.pytest_cache/
.venv/
.ruff_cache/
# Synced package data — regenerated from src/schema/ and src/configs/schema/
# by `xce-build.sh xceadmin`'s schema-sync step; never hand-edited, never committed.
src/xcecommon/schema_files/

View file

@ -27,3 +27,12 @@ include = ["xceadmin*", "xcecommon*"]
[tool.setuptools.package-dir] [tool.setuptools.package-dir]
"" = "src" "" = "src"
# Bundled copies of controller.capnp/c.capnp and the 7 JSON validation
# schemas (synced from src/schema/ and src/configs/schema/ by
# src/tools/xce-build.sh's `xceadmin)` case, see Requirement 5.5). Without
# this, a non-editable `pip install .` builds a wheel containing only *.py
# files and silently drops schema_files/ — an editable install would never
# catch this since it resolves straight into the checked-out source tree.
[tool.setuptools.package-data]
xcecommon = ["schema_files/*.capnp", "schema_files/json/*.schema.json"]