its-magic 0.1.2 → 0.1.3-0
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 +28 -0
- package/template/.cursor/commands/release.md +18 -0
- package/template/CHANGELOG.md +11 -0
- package/template/docs/engineering/runbook.md +77 -6
- package/template/handoffs/releases/vX.Y.Z-release-notes.md.example +21 -0
- package/template/scripts/check_intake_template_parity.py +28 -0
- package/template/scripts/dev_environment_lib.py +138 -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
package/bin/postinstall.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { spawnSync } = require("child_process");
|
|
5
|
+
|
|
2
6
|
const M = "\x1b[1;35m";
|
|
3
7
|
const C = "\x1b[1;36m";
|
|
4
8
|
const Y = "\x1b[1;33m";
|
|
@@ -19,3 +23,71 @@ console.log(`${G} Installation complete!${R}`);
|
|
|
19
23
|
console.log("");
|
|
20
24
|
console.log(`${W} Run: its-magic --help${R}`);
|
|
21
25
|
console.log("");
|
|
26
|
+
|
|
27
|
+
const pkgRoot = path.resolve(__dirname, "..");
|
|
28
|
+
const templateRoot = path.join(pkgRoot, "template");
|
|
29
|
+
const bootstrapScript = path.join(pkgRoot, "scripts", "dev_environment_lib.py");
|
|
30
|
+
|
|
31
|
+
function detectConsumerRepoRoot() {
|
|
32
|
+
let dir = process.cwd();
|
|
33
|
+
for (let depth = 0; depth <= 6; depth += 1) {
|
|
34
|
+
const scratchpad = path.join(dir, ".cursor", "scratchpad.md");
|
|
35
|
+
const versionSentinel = path.join(dir, "its_magic", ".its-magic-version");
|
|
36
|
+
if (fs.existsSync(scratchpad) || fs.existsSync(versionSentinel)) {
|
|
37
|
+
return dir;
|
|
38
|
+
}
|
|
39
|
+
const parent = path.dirname(dir);
|
|
40
|
+
if (parent === dir) {
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
dir = parent;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolvePythonCommand() {
|
|
49
|
+
if (process.platform === "win32") {
|
|
50
|
+
return "python";
|
|
51
|
+
}
|
|
52
|
+
return "python3";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function runDevEnvBootstrap() {
|
|
56
|
+
const repoRoot = detectConsumerRepoRoot();
|
|
57
|
+
if (!repoRoot) {
|
|
58
|
+
console.log("[DEV_ENV_BOOTSTRAP_SKIP] no consumer repository detected");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (!fs.existsSync(bootstrapScript)) {
|
|
62
|
+
console.log(
|
|
63
|
+
"[DEV_ENV_BOOTSTRAP_ERROR] bootstrap helper missing; copy template/.cursor/dev-environment.json.example manually"
|
|
64
|
+
);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const py = resolvePythonCommand();
|
|
68
|
+
const res = spawnSync(
|
|
69
|
+
py,
|
|
70
|
+
[
|
|
71
|
+
bootstrapScript,
|
|
72
|
+
"--bootstrap",
|
|
73
|
+
"--target",
|
|
74
|
+
repoRoot,
|
|
75
|
+
"--source-root",
|
|
76
|
+
templateRoot,
|
|
77
|
+
],
|
|
78
|
+
{ encoding: "utf-8" }
|
|
79
|
+
);
|
|
80
|
+
if (res.stdout) {
|
|
81
|
+
process.stdout.write(res.stdout);
|
|
82
|
+
}
|
|
83
|
+
if (res.stderr) {
|
|
84
|
+
process.stderr.write(res.stderr);
|
|
85
|
+
}
|
|
86
|
+
if (res.status === 1) {
|
|
87
|
+
console.log(
|
|
88
|
+
"[DEV_ENV_BOOTSTRAP_ERROR] bootstrap failed; copy template/.cursor/dev-environment.json.example to .cursor/dev-environment.json"
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
runDevEnvBootstrap();
|
package/installer.py
CHANGED
|
@@ -345,6 +345,57 @@ def _load_doc_profile_lib():
|
|
|
345
345
|
return mod
|
|
346
346
|
|
|
347
347
|
|
|
348
|
+
def _load_dev_environment_lib():
|
|
349
|
+
"""Load dev_environment_lib from scripts/ adjacent to this installer."""
|
|
350
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
351
|
+
path = os.path.join(here, "scripts", "dev_environment_lib.py")
|
|
352
|
+
if not os.path.isfile(path):
|
|
353
|
+
raise RuntimeError(
|
|
354
|
+
"[DEV_ENVIRONMENT_LIB_MISSING] Expected dev environment library at "
|
|
355
|
+
f"{path} (same directory as installer.py). "
|
|
356
|
+
f"Reinstall or upgrade its-magic ({REPO_URL})."
|
|
357
|
+
)
|
|
358
|
+
spec = importlib.util.spec_from_file_location("dev_environment_lib", path)
|
|
359
|
+
if spec is None or spec.loader is None:
|
|
360
|
+
raise RuntimeError(
|
|
361
|
+
"[DEV_ENVIRONMENT_LIB_LOAD_ERROR] Could not create import spec for "
|
|
362
|
+
f"{path}. Reinstall its-magic ({REPO_URL})."
|
|
363
|
+
)
|
|
364
|
+
mod = importlib.util.module_from_spec(spec)
|
|
365
|
+
sys.modules["dev_environment_lib"] = mod
|
|
366
|
+
try:
|
|
367
|
+
spec.loader.exec_module(mod)
|
|
368
|
+
except Exception as e:
|
|
369
|
+
sys.modules.pop("dev_environment_lib", None)
|
|
370
|
+
raise RuntimeError(
|
|
371
|
+
"[DEV_ENVIRONMENT_LIB_LOAD_ERROR] dev_environment_lib failed to load "
|
|
372
|
+
f"({e!r}). Reinstall its-magic ({REPO_URL})."
|
|
373
|
+
) from e
|
|
374
|
+
return mod
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def bootstrap_dev_environment_profile_installer_hook(target_root, source_root):
|
|
378
|
+
"""
|
|
379
|
+
Non-destructive dev-environment profile bootstrap.
|
|
380
|
+
Fail-closed only on PATH_INVALID / SOURCE_MISSING.
|
|
381
|
+
"""
|
|
382
|
+
try:
|
|
383
|
+
lib = _load_dev_environment_lib()
|
|
384
|
+
except RuntimeError as exc:
|
|
385
|
+
print(str(exc))
|
|
386
|
+
return False
|
|
387
|
+
merged, _paths = merge_scratchpad_layers(target_root)
|
|
388
|
+
reason, _channel = lib.bootstrap_dev_environment_profile(
|
|
389
|
+
target_root, source_root, merged
|
|
390
|
+
)
|
|
391
|
+
if reason in (
|
|
392
|
+
lib.DEV_ENV_BOOTSTRAP_PATH_INVALID,
|
|
393
|
+
lib.DEV_ENV_BOOTSTRAP_SOURCE_MISSING,
|
|
394
|
+
):
|
|
395
|
+
return False
|
|
396
|
+
return True
|
|
397
|
+
|
|
398
|
+
|
|
348
399
|
def _doc_profile_sync(target_root, merged, print_ok=True):
|
|
349
400
|
"""Append missing normative README/developer doc sections from merged profile (non-destructive)."""
|
|
350
401
|
doc_profile_lib = _load_doc_profile_lib()
|
|
@@ -878,6 +929,8 @@ def main():
|
|
|
878
929
|
|
|
879
930
|
if not run_scratchpad_postinstall(target_root, source_root, "upgrade", print_ok=True):
|
|
880
931
|
return 1
|
|
932
|
+
if not bootstrap_dev_environment_profile_installer_hook(target_root, source_root):
|
|
933
|
+
return 1
|
|
881
934
|
if not validate_install_completeness(target_root, source_root, required_script_paths, manifest_path):
|
|
882
935
|
return 1
|
|
883
936
|
|
|
@@ -958,6 +1011,8 @@ def main():
|
|
|
958
1011
|
|
|
959
1012
|
if not run_scratchpad_postinstall(target_root, source_root, mode, print_ok=True):
|
|
960
1013
|
return 1
|
|
1014
|
+
if not bootstrap_dev_environment_profile_installer_hook(target_root, source_root):
|
|
1015
|
+
return 1
|
|
961
1016
|
if not validate_install_completeness(target_root, source_root, required_script_paths, manifest_path):
|
|
962
1017
|
return 1
|
|
963
1018
|
|
package/package.json
CHANGED
|
@@ -202,6 +202,32 @@ 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
|
+
|
|
205
231
|
DOWNSTREAM_CI_GUARD_PAIRS: tuple[tuple[str, str], ...] = (
|
|
206
232
|
(
|
|
207
233
|
"scripts/check_downstream_ci_guard.py",
|
|
@@ -225,6 +251,7 @@ SCOPES: dict[str, tuple[tuple[str, str], ...]] = {
|
|
|
225
251
|
"us-0096": US0096_PAIRS,
|
|
226
252
|
"project-readme": PROJECT_README_PAIRS,
|
|
227
253
|
"dev-environment": DEV_ENVIRONMENT_PAIRS,
|
|
254
|
+
"release-changelog": RELEASE_CHANGELOG_PAIRS,
|
|
228
255
|
"all": (
|
|
229
256
|
INTAKE_TEMPLATE_PAIRS
|
|
230
257
|
+ CAVEMAN_COMPRESS_PAIRS
|
|
@@ -237,6 +264,7 @@ SCOPES: dict[str, tuple[tuple[str, str], ...]] = {
|
|
|
237
264
|
+ US0096_PAIRS
|
|
238
265
|
+ PROJECT_README_PAIRS
|
|
239
266
|
+ DEV_ENVIRONMENT_PAIRS
|
|
267
|
+
+ RELEASE_CHANGELOG_PAIRS
|
|
240
268
|
),
|
|
241
269
|
}
|
|
242
270
|
|
|
@@ -425,6 +425,24 @@ Guardrails:
|
|
|
425
425
|
- `RELEASE_OPERATOR_HINTS_SECRET_EXPOSURE`
|
|
426
426
|
- Remediation: populate required fields in canonical sprint notes with
|
|
427
427
|
sanitized env-ref-only credential guidance, then rerun `/release`.
|
|
428
|
+
19. Version changelog derivation (US-0100 / DEC-0085):
|
|
429
|
+
- Runs **only after** step **9** successful finalization (`unreleased → released`)
|
|
430
|
+
and step **18** operator hints (**US-0067**). Doc writes are **not** publish
|
|
431
|
+
execution — **`RELEASE_PUBLISH_MODE=disabled`** remains valid (**US-0054**).
|
|
432
|
+
- **19a — Resolve semver**: read target queue row **`release_version`**; when
|
|
433
|
+
blank, workflow-only release → **`[Unreleased]`** path only (no per-version file).
|
|
434
|
+
- **19b — Derive work items**: `derive_work_items` for target sprint + coalesce
|
|
435
|
+
peer **`released`** rows sharing normalized semver when semver known
|
|
436
|
+
(`coalesce_sprints_by_semver`).
|
|
437
|
+
- **19c — Write docs**: when semver known → `build_version_doc` +
|
|
438
|
+
`promote_unreleased` + `bind_queue_release_version`; else `append_unreleased`
|
|
439
|
+
only. Per-version SOT = `handoffs/releases/{semver}-release-notes.md` (stem
|
|
440
|
+
without leading **`v`**). Never pass `Sxxxx-release-notes.md` to **`gh -F`**.
|
|
441
|
+
- **19d — Validate (optional enforce)**: when scratchpad
|
|
442
|
+
**`RELEASE_CHANGELOG_ENFORCE=1`** (default **`1`** post-bootstrap), run
|
|
443
|
+
`python scripts/release_changelog_validate.py --repo . --enforce`; record
|
|
444
|
+
outcome in `sprints/Sxxxx/release-findings.md` § version-doc gates. When
|
|
445
|
+
**`0`**, report `skipped` evidence only.
|
|
428
446
|
|
|
429
447
|
## Fail-safe reason codes and remediation guidance
|
|
430
448
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
<!-- semver-sections-newest-first -->
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
@@ -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
|
|
|
@@ -792,6 +810,59 @@ Fail-closed reason codes:
|
|
|
792
810
|
- `RELEASE_OPERATOR_HINTS_AMBIGUOUS`
|
|
793
811
|
- `RELEASE_OPERATOR_HINTS_SECRET_EXPOSURE`
|
|
794
812
|
|
|
813
|
+
## Version-scoped release docs (US-0100 / DEC-0085)
|
|
814
|
+
|
|
815
|
+
Cumulative and per-version release documentation compose with **US-0040** sprint
|
|
816
|
+
notes — they do **not** replace `handoffs/releases/Sxxxx-release-notes.md`.
|
|
817
|
+
|
|
818
|
+
| Artifact | Path | Role |
|
|
819
|
+
|----------|------|------|
|
|
820
|
+
| Cumulative changelog | `CHANGELOG.md` | Keep a Changelog 1.1.0; mandatory top `## [Unreleased]` |
|
|
821
|
+
| Per-version GitHub body | `handoffs/releases/{semver}-release-notes.md` | **`gh -F` SOT** (semver stem without `v`) |
|
|
822
|
+
| Sprint workflow evidence | `handoffs/releases/Sxxxx-release-notes.md` | Unchanged (**US-0040**); derivation input only |
|
|
823
|
+
| Backfill manifest | `docs/engineering/context/release-version-backfill.manifest.yaml` | Tier B operator `sprint_id`→`semver` overrides |
|
|
824
|
+
|
|
825
|
+
### Operator workflow (deterministic order)
|
|
826
|
+
|
|
827
|
+
1. **`/release`** (local workflow): after step **9** finalization, step **19**
|
|
828
|
+
derives work items, writes version docs when semver known, or appends
|
|
829
|
+
`[Unreleased]` only when semver blank.
|
|
830
|
+
2. **`release-all.sh`** (npm/choco/brew + GitHub): post-`npm version`, ensure
|
|
831
|
+
`handoffs/releases/${NEW_VERSION}-release-notes.md`, run
|
|
832
|
+
`release_changelog_validate.py --enforce`, then `gh release create -F`.
|
|
833
|
+
3. **CI tag push** (when **US-0054** publish targets enabled): same `-F` path;
|
|
834
|
+
confirmation gates unchanged.
|
|
835
|
+
|
|
836
|
+
### Scratchpad keys
|
|
837
|
+
|
|
838
|
+
| Key | Default | Role |
|
|
839
|
+
|-----|---------|------|
|
|
840
|
+
| `RELEASE_CHANGELOG_ENFORCE` | `1` | Blocking validator at `/release` step **19d** + `release-all.sh` |
|
|
841
|
+
| `RELEASE_CHANGELOG_ALLOW_GENERATE_NOTES` | `0` | Opt-in `gh --generate-notes` when version doc missing |
|
|
842
|
+
|
|
843
|
+
### Backfill tiers (one-time / idempotent)
|
|
844
|
+
|
|
845
|
+
| Tier | Source | Semver |
|
|
846
|
+
|------|--------|--------|
|
|
847
|
+
| A | Queue `release_version` non-empty | As-is |
|
|
848
|
+
| B | `release-version-backfill.manifest.yaml` | Operator map |
|
|
849
|
+
| C | Remaining released rows | Synthetic `0.0.0-wf.{NNN}` (`S0089`→`0.0.0-wf.089`) |
|
|
850
|
+
|
|
851
|
+
Run: `python scripts/release_changelog_backfill.py --repo .` (idempotent).
|
|
852
|
+
Ambiguous manifest collision → `RELEASE_CHANGELOG_BACKFILL_AMBIGUOUS`.
|
|
853
|
+
|
|
854
|
+
### Troubleshooting (`RELEASE_CHANGELOG_*`)
|
|
855
|
+
|
|
856
|
+
| Code | Remediation |
|
|
857
|
+
|------|-------------|
|
|
858
|
+
| `RELEASE_CHANGELOG_VERSION_DOC_MISSING` | Run `build_version_doc` / backfill `--ensure-version` before `gh -F` |
|
|
859
|
+
| `RELEASE_CHANGELOG_UNRELEASED_MISSING` | Add `## [Unreleased]` header to `CHANGELOG.md` |
|
|
860
|
+
| `RELEASE_CHANGELOG_QUEUE_DRIFT` | Re-run `bind_queue_release_version` for target sprints |
|
|
861
|
+
| `RELEASE_CHANGELOG_BACKFILL_AMBIGUOUS` | Fix manifest duplicate semver mapping |
|
|
862
|
+
|
|
863
|
+
Contract tests: `pytest -k us0100 tests/auto_command_contract_test.py`; parity:
|
|
864
|
+
`python scripts/check_intake_template_parity.py --scope=release-changelog`.
|
|
865
|
+
|
|
795
866
|
## Deterministic status reconciliation mode (US-0055 / DEC-0037)
|
|
796
867
|
|
|
797
868
|
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,32 @@ 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
|
+
|
|
205
231
|
DOWNSTREAM_CI_GUARD_PAIRS: tuple[tuple[str, str], ...] = (
|
|
206
232
|
(
|
|
207
233
|
"scripts/check_downstream_ci_guard.py",
|
|
@@ -225,6 +251,7 @@ SCOPES: dict[str, tuple[tuple[str, str], ...]] = {
|
|
|
225
251
|
"us-0096": US0096_PAIRS,
|
|
226
252
|
"project-readme": PROJECT_README_PAIRS,
|
|
227
253
|
"dev-environment": DEV_ENVIRONMENT_PAIRS,
|
|
254
|
+
"release-changelog": RELEASE_CHANGELOG_PAIRS,
|
|
228
255
|
"all": (
|
|
229
256
|
INTAKE_TEMPLATE_PAIRS
|
|
230
257
|
+ CAVEMAN_COMPRESS_PAIRS
|
|
@@ -237,6 +264,7 @@ SCOPES: dict[str, tuple[tuple[str, str], ...]] = {
|
|
|
237
264
|
+ US0096_PAIRS
|
|
238
265
|
+ PROJECT_README_PAIRS
|
|
239
266
|
+ DEV_ENVIRONMENT_PAIRS
|
|
267
|
+
+ RELEASE_CHANGELOG_PAIRS
|
|
240
268
|
),
|
|
241
269
|
}
|
|
242
270
|
|
|
@@ -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:
|