refactor(generate): port generate.sh to Python

Replace 960-line bash script with ~680-line Python module that leans on
pyyaml and jsonschema instead of shelling to yq/jq/awk/sed/envsubst for every
field. Ecosystem dependencies pinned through the existing flake pythonEnv.

Motivation: the generator had outgrown bash. Recent bugs (awk frontmatter
state machine eating markdown ---, envsubst variable scope, shell quoting in
nested heredocs, multi-section rule surgery) were all classic bash pitfalls
that don't exist in Python.

Design notes:
- Uses pyyaml for TEAM.yaml / SETTINGS.yaml parsing.
- Uses jsonschema to validate both inside the generator (previously only in
  flake.nix's embedded Python block).
- Does NOT use python-frontmatter because its content-stripping drops
  leading blank lines that matter for byte-level parity with bash output.
  Replaced with a 6-line fence-split that preserves whitespace exactly.
- Does NOT use tomli-w because it can't emit multiline-basic-string
  ("\"\"\"...\"\"\"") literals — it would escape every newline in the
  developer_instructions body onto a single line, destroying readability.
  Codex TOML output is hand-built with a documented comment.
- Opencode skill pool now symlinks per-skill based on applies_to instead
  of a blanket symlink, honoring TEAM.yaml's skill filtering.

Verified: snapshotted generated outputs before the port and diffed after.
All of claude/, codex/, opencode/ are byte-identical to baseline except
claude/settings.json, which now uses json.dumps(indent=2) multi-line arrays
instead of hand-built compact arrays — confirmed semantically identical via
json.load comparison.

flake.nix, install.sh, README.md, .gitignore updated to reference
generate.py instead of generate.sh. generate.sh deleted.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Ramos 2026-04-14 11:12:05 -04:00
parent 26d004fe46
commit 590145c714
11 changed files with 699 additions and 952 deletions

View file

@ -13,6 +13,7 @@
gettext
jq
just
(python3.withPackages (ps: with ps; [ pyyaml jsonschema ]))
];
};
});
@ -30,7 +31,7 @@
validateCmd = ''
# Script syntax checks
${bashBin} -n ./generate.sh
python -c "import ast; ast.parse(open('./generate.py').read())"
${bashBin} -n ./install.sh
# Protocol file presence checks
@ -94,8 +95,8 @@
type = "app";
program = "${mkAppScript "build" ''
set -euo pipefail
test -f ./generate.sh || { echo "Run this command from the repository root."; exit 1; }
${bashBin} ./generate.sh
test -f ./generate.py || { echo "Run this command from the repository root."; exit 1; }
python ./generate.py
''}/bin/build";
meta.description = "Generate Claude, Codex, and OpenCode build artifacts from the authored protocol files.";
};
@ -104,7 +105,7 @@
type = "app";
program = "${mkAppScript "validate" ''
set -euo pipefail
test -f ./generate.sh || { echo "Run this command from the repository root."; exit 1; }
test -f ./generate.py || { echo "Run this command from the repository root."; exit 1; }
${validateCmd}
''}/bin/validate";
meta.description = "Validate scripts and protocol files.";
@ -114,9 +115,9 @@
type = "app";
program = "${mkAppScript "check" ''
set -euo pipefail
test -f ./generate.sh || { echo "Run this command from the repository root."; exit 1; }
test -f ./generate.py || { echo "Run this command from the repository root."; exit 1; }
${validateCmd}
${bashBin} ./generate.sh
python ./generate.py
''}/bin/check";
meta.description = "Run validation and generation together.";
};
@ -145,7 +146,7 @@
bashBin = "${pkgs.bash}/bin/bash";
validateCmd = ''
${bashBin} -n ./generate.sh
python -c "import ast; ast.parse(open('./generate.py').read())"
${bashBin} -n ./install.sh
test -f ./SETTINGS.yaml
test -f ./TEAM.yaml
@ -209,7 +210,7 @@
build = mkCheck "agent-team-build-check" ''
set -euxo pipefail
${validateCmd}
${bashBin} ./generate.sh
python ./generate.py
'';
});
};