its-magic 0.1.2 → 0.1.3-1

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 (29) hide show
  1. package/bin/postinstall.js +72 -0
  2. package/installer.py +55 -0
  3. package/package.json +1 -1
  4. package/scripts/check_intake_template_parity.py +90 -0
  5. package/template/.cursor/agents/curator.mdc +1 -0
  6. package/template/.cursor/agents/po.mdc +1 -0
  7. package/template/.cursor/agents/release.mdc +1 -0
  8. package/template/.cursor/commands/release.md +18 -0
  9. package/template/.cursor/model-catalog.local.example.cursor-only.json +9 -0
  10. package/template/.cursor/model-catalog.local.example.json +9 -0
  11. package/template/.cursor/model-catalog.local.example.level-1-easy.json +9 -0
  12. package/template/.cursor/model-catalog.local.example.level-2-complex.json +9 -0
  13. package/template/.cursor/model-catalog.local.example.level-3-mega.json +9 -0
  14. package/template/.cursor/model-catalog.local.example.level-4-super.json +9 -0
  15. package/template/.cursor/model-catalog.local.example.role-based-balanced.json +18 -0
  16. package/template/.cursor/model-catalog.local.example.role-based-highend.json +18 -0
  17. package/template/.cursor/scratchpad.local.example.md +55 -0
  18. package/template/.cursor/scratchpad.md +62 -0
  19. package/template/CHANGELOG.md +11 -0
  20. package/template/docs/engineering/runbook.md +274 -6
  21. package/template/handoffs/releases/vX.Y.Z-release-notes.md.example +21 -0
  22. package/template/scripts/check_intake_template_parity.py +90 -0
  23. package/template/scripts/dev_environment_lib.py +138 -0
  24. package/template/scripts/model_tier_lib.py +680 -0
  25. package/template/scripts/model_tier_validate.py +368 -0
  26. package/template/scripts/release-all.sh +249 -0
  27. package/template/scripts/release_changelog_backfill.py +153 -0
  28. package/template/scripts/release_changelog_lib.py +544 -0
  29. package/template/scripts/release_changelog_validate.py +134 -0
@@ -247,40 +247,58 @@ Normative contract: `decisions/DEC-0083.md`, `docs/engineering/architecture.md`
247
247
  implementation changes — distinct from **US-0065** phase QA, **US-0086** test routing, and
248
248
  **US-0067** release hints. Default-off scratchpad gate; **no** `.env` reads (**US-0085**).
249
249
 
250
+ **Install-time bootstrap (US-0099):** on **`missing`**, **`upgrade`**, and **npm `postinstall`**, the
251
+ framework copies **`template/.cursor/dev-environment.json.example`** → resolved profile path
252
+ (**`.cursor/dev-environment.json`** by default) **only when the target file is absent** — never
253
+ overwrites operator-customized profiles. Customize **after** bootstrap (compose **`service`**, **`*Env`**
254
+ connect refs); manual copy is no longer a prerequisite to enable the gate.
255
+
256
+ | Before (US-0098) | After (US-0099) |
257
+ |------------------|-----------------|
258
+ | "Seed profile" = manual copy prerequisite | Bootstrap automatic on install/upgrade/postinstall |
259
+ | **`DEV_ENV_PROFILE_MISSING`** → manual copy first | Troubleshooting references auto-bootstrap + customize-after-bootstrap |
260
+
250
261
  ### Operator recipes
251
262
 
252
263
  | Scenario | Operator action |
253
264
  |----------|-----------------|
254
265
  | Enable dev auto-launch | Set **`DEV_AUTO_LAUNCH_PROFILE=deterministic_v1`** in scratchpad |
255
- | Seed profile | Copy **`template/.cursor/dev-environment.json.example`** **`.cursor/dev-environment.json`**; set compose **`service`** + **`*Env`** connect refs |
266
+ | Customize profile after bootstrap | Edit **`.cursor/dev-environment.json`** copied from example; set compose **`service`** + **`*Env`** connect refs |
256
267
  | Force relaunch | Send exact phrase **`refresh dev environment`** (case-sensitive whole phrase) |
257
268
  | Profile off / manual mode | Leave **`DEV_AUTO_LAUNCH_PROFILE=off`** (default) — execute step **24** zero overhead |
258
269
  | Ambiguous stack | Fix compose path or seed profile; remediate **`DEV_ENV_DETECT_AMBIGUOUS`** |
259
270
  | Remote + local both on | **US-0086** remote wins over **docker-host-local** — see precedence in **`DEC-0084`** §3 |
260
271
  | Bind-mount hot reload | Default skip on source-only docker changes; use refresh or **`restart_on_source_change=true`** |
272
+ | Global npm install (no consumer repo) | **`[DEV_ENV_BOOTSTRAP_SKIP] no consumer repository detected`** — run **`its-magic`** install into target repo |
261
273
 
262
274
  ### Troubleshooting (`DEV_ENV_*` reason codes)
263
275
 
276
+ **Bootstrap family (install-time; distinct from runtime profile/relaunch families):**
277
+ **`DEV_ENV_BOOTSTRAP_COPIED`**, **`DEV_ENV_BOOTSTRAP_SKIPPED_EXISTS`**, **`DEV_ENV_BOOTSTRAP_PATH_INVALID`**,
278
+ **`DEV_ENV_BOOTSTRAP_SOURCE_MISSING`**.
279
+
264
280
  **Profile family:** **`DEV_ENV_PROFILE_DISABLED`**, **`DEV_ENV_PROFILE_INVALID`**,
265
- **`DEV_ENV_PROFILE_MISSING`**, **`DEV_ENV_DETECT_AMBIGUOUS`**, **`DEV_ENV_COMPOSE_UNRESOLVED`**,
266
- **`DEV_ENV_TARGET_DISABLED`**, **`DEV_ENV_SECRET_SURFACE_VIOLATION`**.
281
+ **`DEV_ENV_PROFILE_MISSING`** (if bootstrap skipped or profile deleted — re-run install/upgrade or
282
+ **`python scripts/dev_environment_lib.py --bootstrap --target <repo>`** then customize),
283
+ **`DEV_ENV_DETECT_AMBIGUOUS`**, **`DEV_ENV_COMPOSE_UNRESOLVED`**, **`DEV_ENV_TARGET_DISABLED`**, **`DEV_ENV_SECRET_SURFACE_VIOLATION`**.
267
284
 
268
285
  **Relaunch family:** **`DEV_ENV_RELAUNCH_SKIPPED_NO_SURFACE`**, **`DEV_ENV_RELAUNCH_SKIPPED_PROFILE_OFF`**,
269
- **`DEV_ENV_RELAUNCH_FAILED`**, **`DEV_ENV_RELAUNCH_RETRY_EXHAUSTED`**, **`DEV_ENV_RELAUNCH_TIMEOUT`**,
270
- **`DEV_ENV_CONNECT_UNAVAILABLE`**.
286
+ **`DEV_ENV_RELAUNCH_FAILED`**, **`DEV_ENV_RELAUNCH_RETRY_EXHAUSTED`**, **`DEV_ENV_RELAUNCH_TIMEOUT`**, **`DEV_ENV_CONNECT_UNAVAILABLE`**.
271
287
 
272
288
  ### Commands
273
289
 
274
290
  ```bash
275
291
  python scripts/dev_environment_lib.py --self-test
276
292
  python scripts/dev_environment_lib.py --load .cursor/dev-environment.json
293
+ python scripts/dev_environment_lib.py --bootstrap --target .
277
294
  python scripts/check_intake_template_parity.py --scope=dev-environment
278
295
  pytest -k us0098 tests/auto_command_contract_test.py
296
+ pytest -k us0099 tests/auto_command_contract_test.py
279
297
  ```
280
298
 
281
299
  Implementation tranche order: **A** (schema + scratchpad) → **B** (stdlib helper) → **C** (execute step **24** + docs) → **D** (contract tests + parity + harness).
282
300
 
283
- Normative contract: `decisions/DEC-0084.md`, `docs/engineering/architecture.md` `# US-0098`.
301
+ Normative contract: `decisions/DEC-0084.md`, `docs/engineering/architecture.md` `# US-0098`, `# US-0099` (bootstrap posture).
284
302
 
285
303
  ## User-visible internal metadata guard (US-0071 / DEC-0053)
286
304
 
@@ -632,6 +650,203 @@ When **`AUTO_DELIVERY_ROUTING=backlog_then_scratchpad`**, story row may declare
632
650
 
633
651
  **Release status (S0086 / US-0096)**: **`released`** (`2026-06-13T16:00:00Z`); **`US-0096`** **DONE** in canonical backlog. Operator verify: **`handoffs/releases/S0086-release-notes.md`** **## Verify**; publish skipped while **`RELEASE_PUBLISH_MODE=confirm`**.
634
652
 
653
+ ## Per-phase model tier selection (US-0101 / DEC-0086)
654
+
655
+ `MODEL_TIER` selects LLM model strength (which model runs per lifecycle phase).
656
+ Three operator-facing tiers map to stable Cursor aliases — no vendor slugs in
657
+ template files.
658
+
659
+ | Key | Values | Default | Role |
660
+ |-----|--------|---------|------|
661
+ | **`MODEL_TIER_DEFAULT`** | `cheap` \| `balanced` \| `strong` | `balanced` | Fallback when phase-specific key absent |
662
+ | **`MODEL_TIER_<PHASE>`** | `cheap` \| `balanced` \| `strong` | *(per matrix)* | Per-phase tier override |
663
+ | **`MODEL_CATALOG`** | path | `.cursor/model-catalog.local.json` | Path to local slug catalog |
664
+ | **`MODEL_RESOLVE`** | `alias_only` \| `local_catalog` | `alias_only` | Resolution strategy |
665
+ | **`MODEL_FALLBACK`** | `inherit` | `inherit` | Fallback when catalog lookup fails |
666
+ | **`MODEL_PROVIDER_MODE`** | `cursor` \| `api` | `cursor` | Provider routing |
667
+
668
+ ### Default phase→tier matrix (architecture-locked)
669
+
670
+ | Tier | Cursor alias | `model:` field | Phases |
671
+ |------|-------------|----------------|--------|
672
+ | **`cheap`** | `fast` | `model: fast` | `ask`, `refresh-context`, `memory-audit`, `status-reconcile`, `pause` |
673
+ | **`balanced`** | `inherit` | `model: inherit` | `intake`, `discovery`, `research`, `release`, `plan-verify` |
674
+ | **`strong`** | *(omit)* | no `model:` field | `architecture`, `execute`, `quick`, `qa`, `verify-work`, `security-review` |
675
+ | *(inherit parent)* | — | — | `auto` (orchestrator always inherits parent model) |
676
+
677
+ ### Provider mode (MODEL_PROVIDER_MODE)
678
+
679
+ | Mode | Description |
680
+ |------|-------------|
681
+ | **`cursor`** (default) | All subagents route through Cursor-managed infrastructure; tier aliases work as designed |
682
+ | **`api`** | Operator uses BYOK via Cursor Settings → Models → API Key |
683
+
684
+ **Known limitation (confirmed 2026-06)**: Cursor subagents do **NOT** inherit
685
+ custom API keys or base URLs — they always bill against the Cursor plan. When
686
+ `MODEL_PROVIDER_MODE=api`, the local catalog is the only operator-controlled
687
+ surface; subagent `model:` aliases still resolve through Cursor infrastructure.
688
+
689
+ **Workaround recipes**:
690
+
691
+ 1. **Parent model + `inherit`** — set the parent chat model to the desired BYOK
692
+ model; subagents using `model: inherit` (balanced tier) pick it up.
693
+ 2. **Manual phase runs** — run each phase in a separate chat with the desired
694
+ model selected at the chat level (no subagent spawn).
695
+ 3. **Local catalog override** — set `MODEL_RESOLVE=local_catalog` and populate
696
+ `.cursor/model-catalog.local.json` with vendor slugs; materializer applies
697
+ slugs at template materialization time (not at subagent runtime).
698
+
699
+ ### Non-substitution paragraph
700
+
701
+ MODEL_TIER ≠ TOKEN_PROFILE ≠ DELIVERY_MODE
702
+
703
+ > **`MODEL_TIER`** selects LLM model strength (which model runs).
704
+ > **`TOKEN_PROFILE`** selects context breadth / token cost (how much context the model sees).
705
+ > **`DELIVERY_MODE`** selects lifecycle shape (standard / ultra_lean / mega_quick).
706
+ > These are **independent axes** — none substitutes for another. A `strong` tier
707
+ > with `lean` profile is valid; a `cheap` tier with `full` profile is valid.
708
+ > Setting one does not change the others. Combine freely.
709
+
710
+ ### Reason codes — `MODEL_TIER_*` / `MODEL_CATALOG_*` / `MODEL_RESOLVE_*` / `MODEL_SLUG_*`
711
+
712
+ | Code | Trigger |
713
+ |------|---------|
714
+ | **`MODEL_TIER_INVALID`** | Unknown tier value (not `cheap`/`balanced`/`strong`) |
715
+ | **`MODEL_CATALOG_INVALID`** | Malformed catalog JSON (parse error, missing `schema_version`) |
716
+ | **`MODEL_SLUG_UNKNOWN`** | Tier key missing from catalog when `MODEL_RESOLVE=local_catalog` |
717
+ | **`MODEL_RESOLVE_FALLBACK`** | Catalog lookup failed but `MODEL_FALLBACK=inherit` → reason + fallback |
718
+
719
+ ### Template agent defaults
720
+
721
+ | Agent role | Tier | `model:` field |
722
+ |-----------|------|----------------|
723
+ | `curator` | cheap | `model: fast` |
724
+ | `po` | balanced | `model: inherit` |
725
+ | `release` | balanced | `model: inherit` |
726
+ | `tech-lead` | strong | *(omit)* |
727
+ | `dev` | strong | *(omit)* |
728
+ | `qa` | strong | *(omit)* |
729
+ | `security` | strong | *(omit)* |
730
+
731
+ **Forbidden in `template/.cursor/agents/`**: hardcoded vendor slugs
732
+ (`composer-*`, `claude-*`, `gpt-*`, `opus-*`). Template files use aliases only.
733
+
734
+ ### Local catalog schema (v1)
735
+
736
+ ```json
737
+ {
738
+ "schema_version": 1,
739
+ "tiers": {
740
+ "cheap": "<slug>",
741
+ "balanced": "<slug>",
742
+ "strong": "<slug>"
743
+ },
744
+ "notes": "optional free-text"
745
+ }
746
+ ```
747
+
748
+ - Path: `.cursor/model-catalog.local.json` (gitignored)
749
+ - Example: `.cursor/model-catalog.local.example.json` (committed, placeholder values)
750
+ - All three tier keys required; values are opaque vendor slug strings
751
+
752
+ ### Resolver algorithm
753
+
754
+ 1. Read `MODEL_TIER_<PHASE>` from merged scratchpad → tier value
755
+ 2. If `MODEL_RESOLVE=alias_only` (default): use built-in mapping (table above)
756
+ 3. If `MODEL_RESOLVE=local_catalog`: load catalog JSON → lookup tier key → slug
757
+ 4. If key missing → `MODEL_SLUG_UNKNOWN` fail-closed
758
+ 5. If `MODEL_FALLBACK=inherit` and lookup fails → emit `MODEL_RESOLVE_FALLBACK` + use `inherit`
759
+ 6. Unknown tier value → `MODEL_TIER_INVALID` fail-closed
760
+ 7. Malformed catalog JSON → `MODEL_CATALOG_INVALID` fail-closed
761
+
762
+ ### Validation commands
763
+
764
+ - `python scripts/model_tier_validate.py --repo .`
765
+ - `pytest -k us0101 tests/auto_command_contract_test.py`
766
+ - `python scripts/check_intake_template_parity.py --scope=model-tier`
767
+ - `tests/run-tests.ps1` / `tests/run-tests.sh` §26Z
768
+
769
+ Normative architecture: `docs/engineering/architecture.md` (**# US-0101**).
770
+
771
+ ## Direct per-phase model slug override + role catalog (US-0102 / DEC-0087)
772
+
773
+ Composes on **US-0101** / **DEC-0086** — tier-only operators need no migration.
774
+
775
+ ### 5-step precedence chain
776
+
777
+ | Step | Source | Outcome |
778
+ |------|--------|---------|
779
+ | **1** | `MODEL_<PHASE>` | Non-empty slug → return (validate per mode) or `MODEL_OVERRIDE_SLUG_UNKNOWN` |
780
+ | **2** | `MODEL_TIER_<PHASE>` | **DEC-0086** tier→alias / `local_catalog` chain |
781
+ | **3** | `MODEL_RESOLVE=role_catalog` | Phase→logical role → catalog `roles[<key>]`; miss → `MODEL_ROLE_SLUG_UNKNOWN` → fall through |
782
+ | **4** | `MODEL_TIER_DEFAULT` | **DEC-0086** tier chain |
783
+ | **5** | Cursor alias | **DEC-0086** built-in mapping |
784
+
785
+ When `MODEL_RESOLVE` is `alias_only` or `local_catalog`, step **3** is skipped.
786
+
787
+ ### Scratchpad keys (new)
788
+
789
+ | Key | Default | Role |
790
+ |-----|---------|------|
791
+ | **`MODEL_<PHASE>`** | *(absent)* | Direct vendor slug; includes `MODEL_ASK` |
792
+ | **`MODEL_RESOLVE`** | `alias_only` | Extended: `alias_only` \| `local_catalog` \| **`role_catalog`** |
793
+
794
+ Operator slugs live in `.cursor/scratchpad.local.md` and `.cursor/model-catalog.local.json` only (gitignored).
795
+
796
+ ### Role catalog enablement recipe
797
+
798
+ 1. Copy a v2 example catalog:
799
+ - `.cursor/model-catalog.local.example.role-based-balanced.json`, or
800
+ - `.cursor/model-catalog.local.example.role-based-highend.json`
801
+ 2. Save as `.cursor/model-catalog.local.json` with real vendor slugs.
802
+ 3. Set `MODEL_RESOLVE=role_catalog` in `.cursor/scratchpad.local.md`.
803
+ 4. Optional: set `MODEL_<PHASE>=<slug>` for direct overrides (step 1 wins over role lookup).
804
+ 5. Run `python scripts/model_tier_validate.py --repo .` to validate.
805
+
806
+ On role lookup miss → `MODEL_ROLE_SLUG_UNKNOWN` emitted; resolver falls through to `MODEL_TIER_DEFAULT` then Cursor alias.
807
+
808
+ **`dev_difficult`**: no automatic phase routing — use `MODEL_EXECUTE=<slug>` direct override or tier `strong` + catalog tier slug.
809
+
810
+ Non-normative role recommendations: `ai_modell_auslegung_cursor_highend.md`.
811
+
812
+ ### Catalog schema v2 (opt-in)
813
+
814
+ ```json
815
+ {
816
+ "schema_version": 2,
817
+ "tiers": { "cheap": "<slug>", "balanced": "<slug>", "strong": "<slug>" },
818
+ "roles": {
819
+ "po": "<slug>", "sa": "<slug>", "dev": "<slug>", "dev_difficult": "<slug>",
820
+ "qa": "<slug>", "security": "<slug>", "release": "<slug>"
821
+ }
822
+ }
823
+ ```
824
+
825
+ v1 catalogs (tiers only) unchanged. Malformed v2 → `MODEL_CATALOG_SCHEMA_V2_INVALID`.
826
+
827
+ ### New reason codes
828
+
829
+ | Code | Trigger |
830
+ |------|---------|
831
+ | **`MODEL_OVERRIDE_SLUG_UNKNOWN`** | Direct slug validation failure |
832
+ | **`MODEL_ROLE_SLUG_UNKNOWN`** | Role catalog lookup miss |
833
+ | **`MODEL_CATALOG_SCHEMA_V2_INVALID`** | v2 schema validation failure |
834
+
835
+ ### Non-substitution paragraph
836
+
837
+ MODEL_TIER ≠ TOKEN_PROFILE ≠ DELIVERY_MODE — unchanged from **US-0101**; model selection is orthogonal to context breadth.
838
+
839
+ ### Validation commands
840
+
841
+ - `python scripts/model_tier_validate.py --repo .`
842
+ - `pytest -k us0102 tests/auto_command_contract_test.py`
843
+ - `python scripts/check_intake_template_parity.py --scope=model-tier-overrides`
844
+ - `tests/run-tests.ps1` / `tests/run-tests.sh` §26AA
845
+
846
+ Normative architecture: `docs/engineering/architecture.md` (**# US-0102**); decision: **`DEC-0087`**.
847
+
848
+ Binding decision: `decisions/DEC-0086.md`.
849
+
635
850
  Context compaction policy:
636
851
 
637
852
  - `docs/engineering/state.md` is a compact hot surface for current execution
@@ -792,6 +1007,59 @@ Fail-closed reason codes:
792
1007
  - `RELEASE_OPERATOR_HINTS_AMBIGUOUS`
793
1008
  - `RELEASE_OPERATOR_HINTS_SECRET_EXPOSURE`
794
1009
 
1010
+ ## Version-scoped release docs (US-0100 / DEC-0085)
1011
+
1012
+ Cumulative and per-version release documentation compose with **US-0040** sprint
1013
+ notes — they do **not** replace `handoffs/releases/Sxxxx-release-notes.md`.
1014
+
1015
+ | Artifact | Path | Role |
1016
+ |----------|------|------|
1017
+ | Cumulative changelog | `CHANGELOG.md` | Keep a Changelog 1.1.0; mandatory top `## [Unreleased]` |
1018
+ | Per-version GitHub body | `handoffs/releases/{semver}-release-notes.md` | **`gh -F` SOT** (semver stem without `v`) |
1019
+ | Sprint workflow evidence | `handoffs/releases/Sxxxx-release-notes.md` | Unchanged (**US-0040**); derivation input only |
1020
+ | Backfill manifest | `docs/engineering/context/release-version-backfill.manifest.yaml` | Tier B operator `sprint_id`→`semver` overrides |
1021
+
1022
+ ### Operator workflow (deterministic order)
1023
+
1024
+ 1. **`/release`** (local workflow): after step **9** finalization, step **19**
1025
+ derives work items, writes version docs when semver known, or appends
1026
+ `[Unreleased]` only when semver blank.
1027
+ 2. **`release-all.sh`** (npm/choco/brew + GitHub): post-`npm version`, ensure
1028
+ `handoffs/releases/${NEW_VERSION}-release-notes.md`, run
1029
+ `release_changelog_validate.py --enforce`, then `gh release create -F`.
1030
+ 3. **CI tag push** (when **US-0054** publish targets enabled): same `-F` path;
1031
+ confirmation gates unchanged.
1032
+
1033
+ ### Scratchpad keys
1034
+
1035
+ | Key | Default | Role |
1036
+ |-----|---------|------|
1037
+ | `RELEASE_CHANGELOG_ENFORCE` | `1` | Blocking validator at `/release` step **19d** + `release-all.sh` |
1038
+ | `RELEASE_CHANGELOG_ALLOW_GENERATE_NOTES` | `0` | Opt-in `gh --generate-notes` when version doc missing |
1039
+
1040
+ ### Backfill tiers (one-time / idempotent)
1041
+
1042
+ | Tier | Source | Semver |
1043
+ |------|--------|--------|
1044
+ | A | Queue `release_version` non-empty | As-is |
1045
+ | B | `release-version-backfill.manifest.yaml` | Operator map |
1046
+ | C | Remaining released rows | Synthetic `0.0.0-wf.{NNN}` (`S0089`→`0.0.0-wf.089`) |
1047
+
1048
+ Run: `python scripts/release_changelog_backfill.py --repo .` (idempotent).
1049
+ Ambiguous manifest collision → `RELEASE_CHANGELOG_BACKFILL_AMBIGUOUS`.
1050
+
1051
+ ### Troubleshooting (`RELEASE_CHANGELOG_*`)
1052
+
1053
+ | Code | Remediation |
1054
+ |------|-------------|
1055
+ | `RELEASE_CHANGELOG_VERSION_DOC_MISSING` | Run `build_version_doc` / backfill `--ensure-version` before `gh -F` |
1056
+ | `RELEASE_CHANGELOG_UNRELEASED_MISSING` | Add `## [Unreleased]` header to `CHANGELOG.md` |
1057
+ | `RELEASE_CHANGELOG_QUEUE_DRIFT` | Re-run `bind_queue_release_version` for target sprints |
1058
+ | `RELEASE_CHANGELOG_BACKFILL_AMBIGUOUS` | Fix manifest duplicate semver mapping |
1059
+
1060
+ Contract tests: `pytest -k us0100 tests/auto_command_contract_test.py`; parity:
1061
+ `python scripts/check_intake_template_parity.py --scope=release-changelog`.
1062
+
795
1063
  ## Deterministic status reconciliation mode (US-0055 / DEC-0037)
796
1064
 
797
1065
  Use the dedicated reconciliation command to normalize status drift across
@@ -0,0 +1,21 @@
1
+ # Per-version release notes — pattern example (US-0100 / DEC-0085)
2
+
3
+ > **Rename at use**: copy this file to `handoffs/releases/{semver}-release-notes.md`
4
+ > where `{semver}` is the normalized semver stem **without** a leading `v`
5
+ > (e.g. `0.1.2-41-release-notes.md`, not `v0.1.2-41-release-notes.md`).
6
+ >
7
+ > **Do not overwrite** unrelated version files — target only the semver you are publishing.
8
+ > Sprint-scoped workflow evidence stays in `handoffs/releases/Sxxxx-release-notes.md`
9
+ > (**US-0040**); never pass `Sxxxx` notes to `gh -F`.
10
+
11
+ ## Work items
12
+
13
+ - **US-xxxx** — One-line summary derived from sprint notes / backlog / queue precedence.
14
+ - **BUG-xxxx** — One-line fix summary (category **Fixed** in cumulative `CHANGELOG.md`).
15
+
16
+ ## Sprint evidence
17
+
18
+ - [`S0070`](handoffs/releases/S0070-release-notes.md)
19
+ - [`S0071`](handoffs/releases/S0071-release-notes.md)
20
+
21
+ <!-- Example coalesce: S0070 + S0071 → semver 0.1.2-41 -->
@@ -202,6 +202,90 @@ DEV_ENVIRONMENT_PAIRS: tuple[tuple[str, str], ...] = (
202
202
  ),
203
203
  )
204
204
 
205
+ RELEASE_CHANGELOG_PAIRS: tuple[tuple[str, str], ...] = (
206
+ (
207
+ "scripts/release_changelog_lib.py",
208
+ "template/scripts/release_changelog_lib.py",
209
+ ),
210
+ (
211
+ "scripts/release_changelog_validate.py",
212
+ "template/scripts/release_changelog_validate.py",
213
+ ),
214
+ (
215
+ "scripts/release_changelog_backfill.py",
216
+ "template/scripts/release_changelog_backfill.py",
217
+ ),
218
+ ("CHANGELOG.md", "template/CHANGELOG.md"),
219
+ (".cursor/commands/release.md", "template/.cursor/commands/release.md"),
220
+ ("scripts/release-all.sh", "template/scripts/release-all.sh"),
221
+ (
222
+ "template/handoffs/releases/vX.Y.Z-release-notes.md.example",
223
+ "template/handoffs/releases/vX.Y.Z-release-notes.md.example",
224
+ ),
225
+ (
226
+ "scripts/check_intake_template_parity.py",
227
+ "template/scripts/check_intake_template_parity.py",
228
+ ),
229
+ )
230
+
231
+ MODEL_TIER_PAIRS: tuple[tuple[str, str], ...] = (
232
+ (
233
+ "scripts/model_tier_lib.py",
234
+ "template/scripts/model_tier_lib.py",
235
+ ),
236
+ (
237
+ "scripts/model_tier_validate.py",
238
+ "template/scripts/model_tier_validate.py",
239
+ ),
240
+ (
241
+ "docs/engineering/runbook.md",
242
+ "template/docs/engineering/runbook.md",
243
+ ),
244
+ (
245
+ ".cursor/scratchpad.md",
246
+ "template/.cursor/scratchpad.md",
247
+ ),
248
+ (
249
+ ".cursor/scratchpad.local.example.md",
250
+ "template/.cursor/scratchpad.local.example.md",
251
+ ),
252
+ (
253
+ "scripts/check_intake_template_parity.py",
254
+ "template/scripts/check_intake_template_parity.py",
255
+ ),
256
+ )
257
+
258
+ MODEL_TIER_OVERRIDES_PAIRS: tuple[tuple[str, str], ...] = (
259
+ (
260
+ ".cursor/scratchpad.md",
261
+ "template/.cursor/scratchpad.md",
262
+ ),
263
+ (
264
+ ".cursor/scratchpad.local.example.md",
265
+ "template/.cursor/scratchpad.local.example.md",
266
+ ),
267
+ (
268
+ ".cursor/model-catalog.local.example.role-based-balanced.json",
269
+ "template/.cursor/model-catalog.local.example.role-based-balanced.json",
270
+ ),
271
+ (
272
+ ".cursor/model-catalog.local.example.role-based-highend.json",
273
+ "template/.cursor/model-catalog.local.example.role-based-highend.json",
274
+ ),
275
+ (
276
+ "scripts/model_tier_lib.py",
277
+ "template/scripts/model_tier_lib.py",
278
+ ),
279
+ (
280
+ "scripts/model_tier_validate.py",
281
+ "template/scripts/model_tier_validate.py",
282
+ ),
283
+ (
284
+ "docs/engineering/runbook.md",
285
+ "template/docs/engineering/runbook.md",
286
+ ),
287
+ )
288
+
205
289
  DOWNSTREAM_CI_GUARD_PAIRS: tuple[tuple[str, str], ...] = (
206
290
  (
207
291
  "scripts/check_downstream_ci_guard.py",
@@ -225,6 +309,9 @@ SCOPES: dict[str, tuple[tuple[str, str], ...]] = {
225
309
  "us-0096": US0096_PAIRS,
226
310
  "project-readme": PROJECT_README_PAIRS,
227
311
  "dev-environment": DEV_ENVIRONMENT_PAIRS,
312
+ "release-changelog": RELEASE_CHANGELOG_PAIRS,
313
+ "model-tier": MODEL_TIER_PAIRS,
314
+ "model-tier-overrides": MODEL_TIER_OVERRIDES_PAIRS,
228
315
  "all": (
229
316
  INTAKE_TEMPLATE_PAIRS
230
317
  + CAVEMAN_COMPRESS_PAIRS
@@ -237,6 +324,9 @@ SCOPES: dict[str, tuple[tuple[str, str], ...]] = {
237
324
  + US0096_PAIRS
238
325
  + PROJECT_README_PAIRS
239
326
  + DEV_ENVIRONMENT_PAIRS
327
+ + RELEASE_CHANGELOG_PAIRS
328
+ + MODEL_TIER_PAIRS
329
+ + MODEL_TIER_OVERRIDES_PAIRS
240
330
  ),
241
331
  }
242
332
 
@@ -9,6 +9,7 @@ import fnmatch
9
9
  import json
10
10
  import os
11
11
  import re
12
+ import shutil
12
13
  import subprocess
13
14
  import sys
14
15
  from pathlib import Path
@@ -62,6 +63,24 @@ RELAUNCH_REASON_CODES = (
62
63
  DEV_ENV_CONNECT_UNAVAILABLE,
63
64
  )
64
65
 
66
+ # DEV_ENV_BOOTSTRAP_* reason codes (DEC-0084 § bootstrap posture / US-0099)
67
+ DEV_ENV_BOOTSTRAP_COPIED = "DEV_ENV_BOOTSTRAP_COPIED"
68
+ DEV_ENV_BOOTSTRAP_SKIPPED_EXISTS = "DEV_ENV_BOOTSTRAP_SKIPPED_EXISTS"
69
+ DEV_ENV_BOOTSTRAP_PATH_INVALID = "DEV_ENV_BOOTSTRAP_PATH_INVALID"
70
+ DEV_ENV_BOOTSTRAP_SOURCE_MISSING = "DEV_ENV_BOOTSTRAP_SOURCE_MISSING"
71
+
72
+ BOOTSTRAP_REASON_CODES = (
73
+ DEV_ENV_BOOTSTRAP_COPIED,
74
+ DEV_ENV_BOOTSTRAP_SKIPPED_EXISTS,
75
+ DEV_ENV_BOOTSTRAP_PATH_INVALID,
76
+ DEV_ENV_BOOTSTRAP_SOURCE_MISSING,
77
+ )
78
+
79
+ SCRATCHPAD_EXAMPLE_REL = ".cursor/scratchpad.local.example.md"
80
+ SCRATCHPAD_BASELINE_REL = ".cursor/scratchpad.md"
81
+ SCRATCHPAD_LOCAL_REL = ".cursor/scratchpad.local.md"
82
+ DEV_ENV_EXAMPLE_REL = ".cursor/dev-environment.json.example"
83
+
65
84
  TIER_A_PATTERNS = (
66
85
  "Dockerfile",
67
86
  "Dockerfile.*",
@@ -110,6 +129,110 @@ def _normalize_path(path: str) -> str:
110
129
  return path.replace("\\", "/")
111
130
 
112
131
 
132
+ def _parse_scratchpad_file(path: Path) -> Dict[str, str]:
133
+ if not path.is_file():
134
+ return {}
135
+ out: Dict[str, str] = {}
136
+ with open(path, "r", encoding="utf-8") as f:
137
+ for raw in f:
138
+ line = raw.strip()
139
+ if not line or line.startswith("#") or line.startswith("- "):
140
+ continue
141
+ if "=" not in line:
142
+ continue
143
+ key, _, val = line.partition("=")
144
+ key = key.strip()
145
+ if key:
146
+ out[key] = val.strip()
147
+ return out
148
+
149
+
150
+ def read_merged_scratchpad(target_root: Path) -> Dict[str, str]:
151
+ """Model B merge precedence: local > materialized baseline > example."""
152
+ example = _parse_scratchpad_file(target_root / SCRATCHPAD_EXAMPLE_REL)
153
+ baseline = _parse_scratchpad_file(target_root / SCRATCHPAD_BASELINE_REL)
154
+ local = _parse_scratchpad_file(target_root / SCRATCHPAD_LOCAL_REL)
155
+ merged: Dict[str, str] = {}
156
+ for key in set(example) | set(baseline) | set(local):
157
+ if key in local:
158
+ merged[key] = local[key]
159
+ elif key in baseline:
160
+ merged[key] = baseline[key]
161
+ elif key in example:
162
+ merged[key] = example[key]
163
+ return merged
164
+
165
+
166
+ def resolve_profile_path(
167
+ target_root: Path | str,
168
+ scratchpad: Optional[Dict[str, str]] = None,
169
+ ) -> Tuple[Optional[Path], Optional[str]]:
170
+ """Resolve profile path from scratchpad override or default; fail closed on invalid override."""
171
+ root = Path(target_root).resolve()
172
+ pad = scratchpad or {}
173
+ override = (pad.get("DEV_ENVIRONMENT_CONFIG") or "").strip()
174
+ rel = override if override else DEFAULT_PROFILE_PATH
175
+ norm = _normalize_path(rel)
176
+
177
+ if os.path.isabs(rel) or (len(rel) > 1 and rel[1] == ":"):
178
+ return None, DEV_ENV_BOOTSTRAP_PATH_INVALID
179
+ if ".." in norm.split("/"):
180
+ return None, DEV_ENV_BOOTSTRAP_PATH_INVALID
181
+ if not norm.endswith(".json"):
182
+ return None, DEV_ENV_BOOTSTRAP_PATH_INVALID
183
+
184
+ resolved = (root / norm).resolve()
185
+ try:
186
+ resolved.relative_to(root)
187
+ except ValueError:
188
+ return None, DEV_ENV_BOOTSTRAP_PATH_INVALID
189
+ return resolved, None
190
+
191
+
192
+ def bootstrap_dev_environment_profile(
193
+ target_root: Path | str,
194
+ source_root: Optional[Path | str] = None,
195
+ scratchpad: Optional[Dict[str, str]] = None,
196
+ ) -> Tuple[str, str]:
197
+ """
198
+ Copy example profile when target absent (non-destructive).
199
+ Returns (reason_code, log_channel) where log_channel is stdout or stderr.
200
+ """
201
+ root = Path(target_root).resolve()
202
+ if source_root is None:
203
+ src_root = Path(__file__).resolve().parent.parent / "template"
204
+ else:
205
+ src_root = Path(source_root).resolve()
206
+
207
+ profile_path, path_err = resolve_profile_path(root, scratchpad)
208
+ if path_err:
209
+ print(f"[DEV_ENV_BOOTSTRAP_ERROR] {path_err}", file=sys.stderr)
210
+ return path_err, "stderr"
211
+
212
+ source_path = src_root / DEV_ENV_EXAMPLE_REL
213
+ if not source_path.is_file():
214
+ print(f"[DEV_ENV_BOOTSTRAP_ERROR] {DEV_ENV_BOOTSTRAP_SOURCE_MISSING}", file=sys.stderr)
215
+ return DEV_ENV_BOOTSTRAP_SOURCE_MISSING, "stderr"
216
+
217
+ rel_target = profile_path.relative_to(root).as_posix()
218
+ if profile_path.is_file():
219
+ print(f"[DEV_ENV_BOOTSTRAP_OK] skipped: profile exists at {rel_target}")
220
+ return DEV_ENV_BOOTSTRAP_SKIPPED_EXISTS, "stdout"
221
+
222
+ profile_path.parent.mkdir(parents=True, exist_ok=True)
223
+ shutil.copy2(source_path, profile_path)
224
+ print(f"[DEV_ENV_BOOTSTRAP_OK] copied: {rel_target}")
225
+ return DEV_ENV_BOOTSTRAP_COPIED, "stdout"
226
+
227
+
228
+ def run_bootstrap_cli(target: Path, source_root: Path) -> int:
229
+ scratchpad = read_merged_scratchpad(target)
230
+ reason, _channel = bootstrap_dev_environment_profile(target, source_root, scratchpad)
231
+ if reason in (DEV_ENV_BOOTSTRAP_PATH_INVALID, DEV_ENV_BOOTSTRAP_SOURCE_MISSING):
232
+ return 1
233
+ return 0
234
+
235
+
113
236
  def _check_secret_literals(obj: Any, path: str = "") -> Optional[str]:
114
237
  if isinstance(obj, dict):
115
238
  for key, value in obj.items():
@@ -442,11 +565,26 @@ def main() -> int:
442
565
  p = argparse.ArgumentParser(description="Dev environment profile helper (US-0098).")
443
566
  p.add_argument("--self-test", action="store_true", help="Run built-in checks.")
444
567
  p.add_argument("--load", metavar="PATH", help="Load and validate profile path.")
568
+ p.add_argument("--bootstrap", action="store_true", help="Bootstrap dev-environment profile (US-0099).")
569
+ p.add_argument("--target", metavar="PATH", help="Consumer repository root (default: cwd).")
570
+ p.add_argument(
571
+ "--source-root",
572
+ metavar="PATH",
573
+ help="Packaged template root (default: <pkg>/template).",
574
+ )
445
575
  args = p.parse_args()
446
576
 
447
577
  if args.self_test:
448
578
  return run_self_test()
449
579
 
580
+ if args.bootstrap:
581
+ target = Path(args.target or ".").resolve()
582
+ if args.source_root:
583
+ source_root = Path(args.source_root).resolve()
584
+ else:
585
+ source_root = Path(__file__).resolve().parent.parent / "template"
586
+ return run_bootstrap_cli(target, source_root)
587
+
450
588
  if args.load:
451
589
  _, err = load_profile(args.load)
452
590
  if err: