spritegen-cli 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.gitignore +3 -0
  2. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/PKG-INFO +1 -1
  3. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0006-centralise-configuration-and-never-cache-it.md +2 -1
  4. spritegen_cli-0.4.0/docs/adr/0016-a-workspace-is-marked-by-the-file-that-configures-it.md +63 -0
  5. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/glossary.md +3 -0
  6. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/notes.md +1 -0
  7. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/pyproject.toml +1 -1
  8. spritegen_cli-0.4.0/specs/workspace-config/design.md +98 -0
  9. spritegen_cli-0.4.0/specs/workspace-config/requirements.md +50 -0
  10. spritegen_cli-0.4.0/specs/workspace-config/tasks.md +25 -0
  11. spritegen_cli-0.4.0/src/spritegen/__init__.py +27 -0
  12. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/cli.py +17 -2
  13. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/report.py +25 -1
  14. spritegen_cli-0.4.0/src/spritegen/settings.py +420 -0
  15. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/skill/__init__.py +81 -5
  16. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/workspace.py +28 -14
  17. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/conftest.py +12 -0
  18. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_cli.py +25 -0
  19. spritegen_cli-0.4.0/tests/test_settings.py +462 -0
  20. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_skill.py +133 -0
  21. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_workspace.py +111 -1
  22. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/uv.lock +217 -195
  23. spritegen_cli-0.3.0/src/spritegen/__init__.py +0 -3
  24. spritegen_cli-0.3.0/src/spritegen/settings.py +0 -127
  25. spritegen_cli-0.3.0/tests/test_settings.py +0 -165
  26. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/agents/code-review.md +0 -0
  27. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/agents/security-review.md +0 -0
  28. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/commands/scc-adr.md +0 -0
  29. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/commands/scc-codewiki.md +0 -0
  30. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/commands/scc-glossary.md +0 -0
  31. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/commands/scc-init.md +0 -0
  32. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/commands/scc-plan-run.md +0 -0
  33. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/commands/scc-prd.md +0 -0
  34. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/commands/scc-stack.md +0 -0
  35. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/commands/scc-wiki.md +0 -0
  36. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/artifacts.md +0 -0
  37. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/autonomy.md +0 -0
  38. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/caveman.md +0 -0
  39. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/code-search.md +0 -0
  40. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/delivery.md +0 -0
  41. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/knowledge-base.md +0 -0
  42. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/methodology.md +0 -0
  43. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/notes.md +0 -0
  44. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/prior-art.md +0 -0
  45. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/project.md +0 -0
  46. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/routing.md +0 -0
  47. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/specs.md +0 -0
  48. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/tasks.md +0 -0
  49. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/rules/verification.md +0 -0
  50. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/scc-manifest.json +0 -0
  51. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/skills/adr/SKILL.md +0 -0
  52. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/skills/codewiki/SKILL.md +0 -0
  53. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/skills/glossary/SKILL.md +0 -0
  54. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/skills/init/SKILL.md +0 -0
  55. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/skills/plan-run/SKILL.md +0 -0
  56. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/skills/prd/SKILL.md +0 -0
  57. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/skills/stack/SKILL.md +0 -0
  58. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.claude/skills/wiki/SKILL.md +0 -0
  59. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.env.template +0 -0
  60. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.gitattributes +0 -0
  61. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.github/workflows/ci.yml +0 -0
  62. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.github/workflows/release.yml +0 -0
  63. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/.python-version +0 -0
  64. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/CLAUDE.md +0 -0
  65. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/README.md +0 -0
  66. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0001-asset-directory-and-no-path-arguments.md +0 -0
  67. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0002-transfer-movement-instead-of-generating-frames.md +0 -0
  68. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0003-append-only-jsonl-ledger.md +0 -0
  69. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0004-allow-list-every-downloaded-host.md +0 -0
  70. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0005-a-directory-per-artifact-kind.md +0 -0
  71. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0007-heavy-dependencies-are-optional-extras.md +0 -0
  72. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0008-local-backends-are-the-default.md +0 -0
  73. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0009-walk-the-redirect-chain-here.md +0 -0
  74. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0010-ci-on-three-operating-systems.md +0 -0
  75. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0011-keep-pixelfixer-out-of-the-distribution.md +0 -0
  76. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0012-publish-with-one-secret.md +0 -0
  77. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0013-require-python-3-13.md +0 -0
  78. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0014-every-run-is-a-version-and-the-state-names-the-chosen-one.md +0 -0
  79. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/adr/0015-record-the-call-before-the-files-it-writes.md +0 -0
  80. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/codewiki/spending-money.md +0 -0
  81. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/codewiki/the-stage-registry.md +0 -0
  82. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/codewiki/the-workspace.md +0 -0
  83. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/stack.md +0 -0
  84. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/wiki/changelog.md +0 -0
  85. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/wiki/index.md +0 -0
  86. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/wiki/pages/configuration.md +0 -0
  87. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/wiki/pages/grid-and-palette-recovery.md +0 -0
  88. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/wiki/pages/local-instead-of-paid.md +0 -0
  89. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/wiki/pages/motion-transfer.md +0 -0
  90. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/wiki/pages/paid-calls-and-the-ledger.md +0 -0
  91. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/wiki/pages/the-asset-directory.md +0 -0
  92. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/wiki/pages/the-generated-skill.md +0 -0
  93. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/docs/wiki/pages/the-pipeline.md +0 -0
  94. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/plans/code-health.md +0 -0
  95. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/plans/motion-optimisation.md +0 -0
  96. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/specs/artifact-versions/design.md +0 -0
  97. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/specs/artifact-versions/requirements.md +0 -0
  98. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/specs/artifact-versions/tasks.md +0 -0
  99. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/atlas.py +0 -0
  100. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/clip.py +0 -0
  101. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/drive.py +0 -0
  102. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/endpoints.py +0 -0
  103. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/fal.py +0 -0
  104. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/imaging.py +0 -0
  105. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/ledger.py +0 -0
  106. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/matting.py +0 -0
  107. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/migrate.py +0 -0
  108. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/prompts.py +0 -0
  109. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/rrdb.py +0 -0
  110. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/sheet.py +0 -0
  111. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/skill/files/SKILL.md +0 -0
  112. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/stages/__init__.py +0 -0
  113. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/stages/_common.py +0 -0
  114. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/stages/anchor.py +0 -0
  115. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/stages/board.py +0 -0
  116. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/stages/catalog.py +0 -0
  117. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/stages/matte.py +0 -0
  118. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/stages/motion.py +0 -0
  119. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/stages/pose.py +0 -0
  120. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/stages/registry.py +0 -0
  121. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/stages/video.py +0 -0
  122. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/src/spritegen/upscale.py +0 -0
  123. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/helpers.py +0 -0
  124. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/expected/anchor_crop.pixelart.json +0 -0
  125. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/expected/anchor_crop.pixelart.png +0 -0
  126. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/expected/synthetic_chroma.cut.json +0 -0
  127. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/expected/synthetic_chroma.cut.png +0 -0
  128. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/expected/synthetic_matted.json +0 -0
  129. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/expected/synthetic_row.png +0 -0
  130. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/expected/synthetic_video_board.json +0 -0
  131. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/expected/synthetic_video_board.png +0 -0
  132. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/expected/walk_south_row.gif +0 -0
  133. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/expected/walk_south_row.png +0 -0
  134. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/input/anchor_crop.png +0 -0
  135. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/input/synthetic_board.png +0 -0
  136. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/input/synthetic_chroma.png +0 -0
  137. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/input/synthetic_matted.png +0 -0
  138. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/input/walk_south_board.png +0 -0
  139. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/parity/manifest.json +0 -0
  140. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_anchor.py +0 -0
  141. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_atlas.py +0 -0
  142. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_board_stage.py +0 -0
  143. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_clip.py +0 -0
  144. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_drive.py +0 -0
  145. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_fal.py +0 -0
  146. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_imaging.py +0 -0
  147. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_ledger.py +0 -0
  148. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_matte.py +0 -0
  149. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_matting.py +0 -0
  150. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_migrate.py +0 -0
  151. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_motion.py +0 -0
  152. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_parity.py +0 -0
  153. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_pose.py +0 -0
  154. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_prompts.py +0 -0
  155. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_sheet.py +0 -0
  156. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_show.py +0 -0
  157. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_stages.py +0 -0
  158. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_upscale.py +0 -0
  159. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_versions.py +0 -0
  160. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/test_video.py +0 -0
  161. {spritegen_cli-0.3.0 → spritegen_cli-0.4.0}/tests/tests_fal_doubles.py +0 -0
@@ -223,3 +223,6 @@ assets/
223
223
 
224
224
  # ai-jail sandbox configuration, local to a checkout
225
225
  .ai-jail
226
+
227
+ # the spritegen workspace's own settings, and its key
228
+ .spritegen.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: spritegen-cli
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Sprite generation CLI — a few AI-generated images into a game-ready character sheet
5
5
  Requires-Python: >=3.13
6
6
  Requires-Dist: av>=12.0
@@ -1,5 +1,6 @@
1
1
  ---
2
- status: accepted
2
+ status: superseded
3
+ superseded-by: 0016-a-workspace-is-marked-by-the-file-that-configures-it
3
4
  ---
4
5
 
5
6
  # One settings class, read fresh, with a ceiling on the upward search
@@ -0,0 +1,63 @@
1
+ ---
2
+ status: accepted
3
+ ---
4
+
5
+ # A workspace is marked by the file that configures it
6
+
7
+ ## Context
8
+
9
+ `adr:0006-centralise-configuration-and-never-cache-it` reads settings from the
10
+ environment, the nearest `.env`, and the declared default, in that order. Where the
11
+ assets are was a separate question, answered by walking up from the working directory
12
+ looking for a directory named `spritegen/assets`.
13
+
14
+ That walk had nothing to recognise. A directory of the right name is not a claim to be
15
+ one, so the search could not tell a workspace from a coincidence — and its ceiling
16
+ admitted the home directory as a candidate. Run in `~/spritegen1/test1`, `init`
17
+ reported a workspace at `~/spritegen/assets` and adopted it: one workspace became every
18
+ project's, and a second project on the same machine was not possible without giving it
19
+ a repository first.
20
+
21
+ The two questions turned out to be one. A project that has its own assets usually wants
22
+ its own cell size, its own matte backend, and — where more than one account is in play —
23
+ its own key.
24
+
25
+ ## Decision
26
+
27
+ **A directory holding `.spritegen.json` is a workspace root.** Its assets are
28
+ `<root>/spritegen/assets`, and the nearest root at or above the working directory is
29
+ the one a command uses.
30
+
31
+ The same file carries the settings, as a JSON object whose names are the `Settings`
32
+ fields without the environment's prefix. It sits **between the environment and the
33
+ `.env`**: an exported variable still wins, which is what makes a one-off override work,
34
+ and a machine with one key in `~/.env` keeps working. A name in it that is not a setting
35
+ is refused rather than ignored.
36
+
37
+ **The home directory is a candidate only when it is where the command was typed.** A
38
+ marker there serves a command run in it and is never climbed into from below.
39
+
40
+ `init` creates the workspace where it was run, always. It writes the marker with no
41
+ credential in it, adds it to a `.gitignore` that exists, and warns when the marker is
42
+ already tracked by git.
43
+
44
+ ## Consequences
45
+
46
+ Several workspaces on one machine work, and they work without a repository: the marker
47
+ is the boundary that `.git` used to have to stand in for.
48
+
49
+ A credential can live in a file inside a project, which is why `init` writes it into
50
+ `.gitignore` and warns when git already has it. `allowed_hosts` is settable there too
51
+ (`adr:0004-allow-list-every-downloaded-host`) — the same reach a `.env` already had,
52
+ bounded by the same ceiling.
53
+
54
+ Two files now answer the same question, and that is the cost. `.env` is kept because
55
+ removing it would break every machine that has a key today; whether it eventually goes
56
+ is deliberately not decided here.
57
+
58
+ A workspace opened before this exists has no marker. It keeps working through the
59
+ unmarked fallback, which now holds the same line about the home directory — but it will
60
+ not be found from a sibling project, and `init` in its root is what gives it one.
61
+
62
+ This supersedes `adr:0006-centralise-configuration-and-never-cache-it` **on the list of
63
+ sources only**. One class, nothing cached, and the ceiling all stand.
@@ -9,6 +9,9 @@ directory `row/` to `sheet/as-is/` — retired a spelling whose word is still in
9
9
  use elsewhere, and listing it would report every correct use as a finding. Add an
10
10
  `Avoid:` the first time a genuinely dead, distinctive name turns up.
11
11
 
12
+ - **workspace** — a directory holding `.spritegen.json`, and the assets under `spritegen/assets` inside it. One machine carries several, and a command uses the nearest one at or above where it was typed.
13
+ - **workspace root** — the directory the marker is in, which is what `status` names and what `init` creates in. Not the assets directory, which sits inside it.
14
+ - **marker** — `.spritegen.json`: the file that says a directory is a workspace, and holds that workspace's settings. Avoid: config file, settings file
12
15
  - **asset** — everything belonging to one character, under `assets/<name>/`. The unit `new`, `status`, `show` and `cost` all talk about, and the reason no stage takes an input or output path.
13
16
  - **state file** — `state.json` inside an asset, the only record of progress. It holds the stages that have completed, never what should happen next.
14
17
  - **stage** — one step of the pipeline, declared in `stages.STAGES` and implemented by the module of the same name. Most spend money; `board` does not.
@@ -44,3 +44,4 @@ over this file answers with the example above as well as with the notes. -->
44
44
  - n-0004 2026-08-28 #gotcha @src/spritegen/sheet.py — the GIF preview writes one global palette plus one shared local palette whatever the frames were quantised with — Pillow unifies on save, so quantising per frame and quantising the strip produce the same file
45
45
  - n-0005 2026-08-28 #gotcha @src/spritegen/upscale.py — the weight cache sidecar must stay a content hash — size and mtime are forgeable with the same write access a swap needs, so a stat-based fast path silently disables the check
46
46
  - n-0006 2026-08-28 #gotcha @docs/codewiki/the-workspace.md — a codewiki citation that still resolves after a refactor is not still correct — the range shifts onto a neighbouring function and validate cannot tell, so check what each range opens on
47
+ - n-0007 2026-08-29 #gotcha @src/spritegen/__init__.py — the CLI version came from a hand-written __version__ in spritegen/__init__.py, a second copy of what pyproject.toml declares — it read 0.1.0 through three releases; it is importlib.metadata now and a test pins the two together
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "spritegen-cli"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "Sprite generation CLI — a few AI-generated images into a game-ready character sheet"
5
5
  requires-python = ">=3.13"
6
6
 
@@ -0,0 +1,98 @@
1
+ # Workspace configuration — design
2
+
3
+ ## What changes, and where
4
+
5
+ Today a workspace is found by looking for a directory that happens to be named
6
+ `spritegen/assets`, walking up from the working directory. That has no marker, so the
7
+ walk cannot tell a workspace from a coincidence — and it reaches the home directory,
8
+ where one workspace becomes every project's. Run from `~/spritegen1/test1`, `init`
9
+ reported a workspace at `~/spritegen/assets`, which belongs to something else.
10
+
11
+ The change is a marker. `.spritegen.json` at the root of a workspace says *this is
12
+ one*, the way `package.json` does for npm and `pyproject.toml` for this repository. The
13
+ walk stops at the nearest one, so a second project one directory over is a second
14
+ workspace, and nothing has to be configured for that to be true.
15
+
16
+ The same file carries the settings, which is the other half: a credential and a cell
17
+ size belong to the project they are for, not to the shell that happened to run the
18
+ command.
19
+
20
+ - `settings.py` — a source between the environment and `.env`, and the search that
21
+ finds it.
22
+ - `workspace.py` — `assets_root` resolves through the marker rather than by looking for
23
+ a directory of the right name.
24
+ - `skill/__init__.py` — `cmd_init` creates in the working directory and writes the file.
25
+
26
+ ## The order settings are read in
27
+
28
+ Highest wins:
29
+
30
+ 1. **The environment.** `SPRITEGEN_CELL=200 spritegen board …` still overrides for one
31
+ run, which is `adr:0006`'s reason and does not change.
32
+ 2. **`.spritegen.json`** at the workspace root — the new one, and where a project's own
33
+ answer belongs.
34
+ 3. **The nearest `.env`** — kept, because a machine with one `FAL_KEY` for everything is
35
+ a real setup and breaking it buys nothing.
36
+ 4. **The declared default.**
37
+
38
+ A name in the file that is not a setting is refused rather than ignored: a typo that
39
+ does nothing is worse than one that says so, and this is the file a `fal_key` is
40
+ misspelled in.
41
+
42
+ ## The file
43
+
44
+ ```json
45
+ {
46
+ "fal_key": "…",
47
+ "cell": 166,
48
+ "matte_backend": "local"
49
+ }
50
+ ```
51
+
52
+ Every field is a `Settings` field under its own name, without the `SPRITEGEN_` prefix
53
+ the environment uses — the prefix exists to keep a shared namespace apart, and a file
54
+ of this tool's own has no such namespace to share.
55
+
56
+ ## The credential, and git
57
+
58
+ `init` writes the file with no credential in it, and adds it to `.gitignore` where one
59
+ exists. It cannot add it where there is no `.gitignore`: writing one is a decision
60
+ about a repository this tool does not own.
61
+
62
+ That leaves the case the guard is for — a `.spritegen.json` somebody committed before
63
+ putting a key in it. `git ls-files --error-unmatch` answers whether it is tracked in one
64
+ call and needs no repository when there is none, so a warning is cheap. It warns and
65
+ proceeds: refusing would make the tool unusable in exactly the situation somebody is
66
+ trying to fix.
67
+
68
+ ## What a file may point at
69
+
70
+ A path out of this file is **relative to the workspace and resolved under it** — R4.4,
71
+ R4.5. The environment keeps the unrestricted form, and the difference is who decided:
72
+ exporting a variable takes a shell, while this file arrives by being cloned, extracted,
73
+ or synced onto a mounted drive, and is then read for no reason but the command having
74
+ been run in that directory.
75
+
76
+ The reachable case is worse than a redirected output directory. `Path.is_dir()` on
77
+ `\host\share` opens an SMB connection and authenticates, so `status` — free, and
78
+ touching no network — would hand a credential to whoever wrote the file. So rooted
79
+ counts, not only absolute: on Windows `/etc/x` carries no drive and is not
80
+ `is_absolute()`, and it leaves the workspace all the same.
81
+
82
+ **`allowed_hosts` becomes settable from a file inside the project**
83
+ (`adr:0004-allow-list-every-downloaded-host`). That is the same reach a `.env` already
84
+ had, and the ceiling in R1.5 is what bounds both — worth stating rather than
85
+ discovering.
86
+
87
+ ## What this supersedes
88
+
89
+ `adr:0006-centralise-configuration-and-never-cache-it` names the environment, the
90
+ nearest `.env` and the default as the three sources, in that order. It gains a fourth
91
+ between the first two, and its other decisions — one class, nothing cached, the ceiling
92
+ — stand. A new record supersedes it on the source list alone.
93
+
94
+ ## Not decided here
95
+
96
+ Whether `.env` is eventually dropped. Two files for one job is one too many, but
97
+ removing one while adding the other would break every machine that has a key today,
98
+ and nothing forces the choice now.
@@ -0,0 +1,50 @@
1
+ ---
2
+ autonomy: auto
3
+ ci: wait
4
+ branch: feat/workspace-config
5
+ delivery: in-review
6
+ pr: 24
7
+ ---
8
+
9
+ # Workspace configuration — requirements
10
+
11
+ A workspace is marked by a file it holds, so one machine can carry several and a
12
+ command run inside one never reaches into another.
13
+
14
+ ## R1 — where the workspace is
15
+
16
+ - **R1.1** The system shall treat a directory holding `.spritegen.json` as a workspace root.
17
+ - **R1.2** When a command needs a workspace, the system shall use the nearest workspace root at or above the working directory.
18
+ - **R1.3** If no workspace root is at or above the working directory, then the system shall refuse and name `spritegen init`.
19
+ - **R1.4** While a workspace root is in use, the system shall keep the assets under `<root>/spritegen/assets`.
20
+ - **R1.5** The system shall stop the upward search at the first repository boundary or at the home directory.
21
+
22
+ ## R2 — opening one
23
+
24
+ - **R2.1** When `init` runs, the system shall create the workspace in the working directory.
25
+ - **R2.2** When `init` creates a workspace, the system shall write `.spritegen.json` in that directory.
26
+ - **R2.3** Where a `.gitignore` is at the workspace root, the system shall add `.spritegen.json` to it.
27
+ - **R2.4** If a workspace root is already at or above the working directory, then the system shall name it and create the new one anyway.
28
+ - **R2.5** If `.spritegen.json` is already in the working directory, then the system shall leave its contents alone.
29
+
30
+ ## R3 — what configuration is read
31
+
32
+ - **R3.1** The system shall read its settings from `.spritegen.json` at the workspace root.
33
+ - **R3.2** Where a setting is in the environment, the environment shall win over the file.
34
+ - **R3.3** Where a setting is in neither, the system shall use the `.env` the search finds, and then the default.
35
+ - **R3.4** If `.spritegen.json` does not parse, then the system shall refuse naming the file and the reason.
36
+ - **R3.5** If `.spritegen.json` holds a name that is not a setting, then the system shall refuse naming it.
37
+ - **R3.6** The system shall build its settings fresh on every read and cache nothing.
38
+
39
+ ## R4 — the key in it
40
+
41
+ - **R4.1** Where `.spritegen.json` holds `fal_key`, the system shall use it as the credential.
42
+ - **R4.2** When `init` writes `.spritegen.json`, the system shall write no credential into it.
43
+ - **R4.3** If `.spritegen.json` is tracked by git, then the system shall warn that a credential in it is committed.
44
+ - **R4.4** (ADDED) Where `.spritegen.json` sets a path, the system shall resolve it under the workspace root.
45
+ - **R4.5** (ADDED) If `.spritegen.json` sets a path that is absolute, rooted or outside the workspace, then the system shall refuse it.
46
+
47
+ ## R5 — saying which one
48
+
49
+ - **R5.1** When `status` runs, the system shall name the workspace root it is using.
50
+ - **R5.2** When `init` finishes, the system shall name the directory it created.
@@ -0,0 +1,25 @@
1
+ # Workspace configuration — tasks
2
+
3
+ ## Tasks
4
+
5
+ - [x] 1.1 (TDD) Find the workspace root by the marker file, bounded by the ceiling — R1.1, R1.2, R1.5
6
+ - [x] 1.2 (Unit) Resolve the assets under the root the marker names, and refuse when there is none — R1.3, R1.4
7
+ _Depends 1.1_
8
+ - [x] 2.1 (TDD) Read `.spritegen.json` between the environment and `.env`, refusing a name that is not a setting — R3.1, R3.2, R3.3, R3.4, R3.5, R3.6
9
+ _Depends 1.1_
10
+ - [x] 2.2 (Unit) Take the credential from the file — R4.1
11
+ _Depends 2.1_
12
+ - [x] 3.1 (Unit) Create the workspace where `init` was run, and write the marker with no credential in it — R2.1, R2.2, R2.4, R2.5, R4.2, R5.2
13
+ _Depends 1.2_
14
+ - [x] 3.2 (Unit) Add the marker to a `.gitignore` that is there — R2.3
15
+ _Depends 3.1_
16
+ - [x] 3.3 (Unit) Warn when the marker is tracked by git — R4.3
17
+ _Depends 3.1_
18
+ - [x] 4.1 (Unit) Name the workspace root in `status` — R5.1
19
+ _Depends 1.2_
20
+ - [x] 4.2 (Unit) Record the decision as an ADR superseding `adr:0006` on its source list, and the term in the glossary — R3.1
21
+ _Depends 2.1_
22
+ - [x] 3.4 (Unit) Stop the skill search climbing into the home directory's .claude — R1.5
23
+ _Reason the same defect in the other search: the docstring says it is bounded and it is not, so init in a fresh project reported the global skill as already there and set that project up with none_
24
+ - [x] 2.3 (Unit) Bound a path the marker sets to the workspace it is in — R4.4, R4.5
25
+ _Reason security review: a hostile marker could point assets at a UNC path, and stat on a share authenticates, so status alone leaked a credential_
@@ -0,0 +1,27 @@
1
+ """Turn a handful of images into a game-ready character sheet, one stage at a time."""
2
+
3
+ _VERSION: str | None = None
4
+
5
+
6
+ def __getattr__(name: str) -> str:
7
+ """`__version__`, read from the installed metadata the first time it is asked for.
8
+
9
+ Not a constant: it was one, written out by hand, and it read 0.1.0 through two
10
+ releases that had bumped `pyproject.toml` — the declaration is the only record now.
11
+
12
+ Resolved lazily because `importlib.metadata` reads dist-info off disk, and every
13
+ command builds the parser: `--help`, `status` and `cost` would each pay some thirty
14
+ milliseconds for a string only `--version` prints.
15
+ """
16
+ global _VERSION
17
+ if name != "__version__":
18
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
19
+ if _VERSION is None:
20
+ from importlib.metadata import PackageNotFoundError
21
+ from importlib.metadata import version as installed
22
+
23
+ try:
24
+ _VERSION = installed("spritegen-cli")
25
+ except PackageNotFoundError: # pragma: no cover - a checkout nobody installed
26
+ _VERSION = "0.0.0+source"
27
+ return _VERSION
@@ -22,7 +22,22 @@ import os
22
22
  import sys
23
23
  from collections.abc import Sequence
24
24
 
25
- from . import __version__, stages
25
+ from . import stages
26
+
27
+
28
+ class _PrintVersion(argparse.Action):
29
+ """`--version`, resolved when it is asked for rather than when the parser is built.
30
+
31
+ Every command builds the parser, and reading the installed metadata costs real
32
+ milliseconds — see the module `__getattr__` this defers to.
33
+ """
34
+
35
+ def __call__(self, parser, namespace, values, option_string=None):
36
+ from . import __version__
37
+
38
+ # stdout and exit 0, which is where argparse's own version action puts it.
39
+ print(f"spritegen {__version__}")
40
+ parser.exit()
26
41
 
27
42
 
28
43
  def build_parser() -> argparse.ArgumentParser:
@@ -31,7 +46,7 @@ def build_parser() -> argparse.ArgumentParser:
31
46
  prog="spritegen",
32
47
  description="Turn a handful of images into a character sheet, one stage at a time.",
33
48
  )
34
- parser.add_argument("--version", action="version", version=f"spritegen {__version__}")
49
+ parser.add_argument("--version", action=_PrintVersion, nargs=0, help="print the version")
35
50
  sub = parser.add_subparsers(dest="command", metavar="<command>")
36
51
 
37
52
  new = sub.add_parser("new", help="open a new asset")
@@ -39,16 +39,39 @@ def cmd_status(args: argparse.Namespace) -> int:
39
39
  console this tool cannot encode into drops the line rather than raising where
40
40
  anyone would see it.
41
41
  """
42
+ from . import settings
43
+
42
44
  root = assets_root()
43
45
  as_json = getattr(args, "json", False)
46
+ # Which workspace answered, by name — R5.1. A machine carries several now, and
47
+ # "no assets here" is a different sentence from "the workspace you meant is
48
+ # elsewhere": the second one is what a command run in the wrong directory needs.
49
+ declared = settings.workspace_root()
44
50
 
45
51
  if not root.is_dir():
46
52
  if as_json:
47
- print(json.dumps({"root": str(root), "workspace": False, "assets": []}, indent=2))
53
+ print(
54
+ json.dumps(
55
+ {
56
+ "root": str(root),
57
+ "workspace": False,
58
+ "declared_at": str(declared) if declared else None,
59
+ "assets": [],
60
+ },
61
+ indent=2,
62
+ )
63
+ )
48
64
  return 0
49
65
  print(f"no spritegen workspace at {root}; run `spritegen init` to make one")
50
66
  return 0
51
67
 
68
+ if not as_json:
69
+ # Only where a marker actually said so. `root.parent.parent` guessed the shape
70
+ # `<root>/spritegen/assets`, which is wrong for SPRITEGEN_ASSETS pointing
71
+ # anywhere and for the one-level legacy `assets/` — and a wrong directory printed
72
+ # under this heading reads as authoritative.
73
+ print(f"workspace {declared}" if declared else f"assets {root}")
74
+
52
75
  # Resolved once, at the top of the command, and passed down from here: every asset,
53
76
  # and every version inside each one, would otherwise rebuild it from the environment.
54
77
  names = known(root)
@@ -58,6 +81,7 @@ def cmd_status(args: argparse.Namespace) -> int:
58
81
  {
59
82
  "root": str(root),
60
83
  "workspace": True,
84
+ "declared_at": str(declared) if declared else None,
61
85
  "assets": [inspect(name, root) for name in names],
62
86
  },
63
87
  indent=2,