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.
- package/bin/postinstall.js +72 -0
- package/installer.py +55 -0
- package/package.json +1 -1
- package/scripts/check_intake_template_parity.py +90 -0
- package/template/.cursor/agents/curator.mdc +1 -0
- package/template/.cursor/agents/po.mdc +1 -0
- package/template/.cursor/agents/release.mdc +1 -0
- package/template/.cursor/commands/release.md +18 -0
- package/template/.cursor/model-catalog.local.example.cursor-only.json +9 -0
- package/template/.cursor/model-catalog.local.example.json +9 -0
- package/template/.cursor/model-catalog.local.example.level-1-easy.json +9 -0
- package/template/.cursor/model-catalog.local.example.level-2-complex.json +9 -0
- package/template/.cursor/model-catalog.local.example.level-3-mega.json +9 -0
- package/template/.cursor/model-catalog.local.example.level-4-super.json +9 -0
- package/template/.cursor/model-catalog.local.example.role-based-balanced.json +18 -0
- package/template/.cursor/model-catalog.local.example.role-based-highend.json +18 -0
- package/template/.cursor/scratchpad.local.example.md +55 -0
- package/template/.cursor/scratchpad.md +62 -0
- package/template/CHANGELOG.md +11 -0
- package/template/docs/engineering/runbook.md +274 -6
- package/template/handoffs/releases/vX.Y.Z-release-notes.md.example +21 -0
- package/template/scripts/check_intake_template_parity.py +90 -0
- package/template/scripts/dev_environment_lib.py +138 -0
- package/template/scripts/model_tier_lib.py +680 -0
- package/template/scripts/model_tier_validate.py +368 -0
- package/template/scripts/release-all.sh +249 -0
- package/template/scripts/release_changelog_backfill.py +153 -0
- package/template/scripts/release_changelog_lib.py +544 -0
- 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
|
-
|
|
|
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
|
|
266
|
-
**`
|
|
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:
|