its-magic 0.1.2-29 → 0.1.2-32

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/README.md CHANGED
@@ -16,6 +16,8 @@ with pause/resume, decision gates, and persistent artifacts.
16
16
  - Pause/resume with checkpoints (`handoffs/resume_brief.md`).
17
17
  - Automated execute/QA loop with safety caps (optional).
18
18
  - 3-layer quality chain: AI loop → local validate-and-push → CI auto-fix.
19
+ - User-visible metadata guard for operator-facing scripts/CLI (`US-0071` / `DEC-0053`):
20
+ `python scripts/check-user-visible-metadata.py` (see `docs/engineering/runbook.md`).
19
21
  - CI/CD templates driven by `docs/engineering/runbook.md`.
20
22
  - Team-friendly local overrides (`scratchpad.local.md`).
21
23
  - Optional remote/docker execution and autonomous installs.
@@ -78,8 +80,13 @@ What upgrade does:
78
80
  - **Framework files** (commands, rules, agents, hooks, skills, CI, scripts) are
79
81
  updated to the latest version.
80
82
  - **User data** (docs, sprints, handoffs, decisions, runbook) is never touched.
81
- - **Mixed files** (`.cursor/scratchpad.md`, `README.md`) are preserved. If the
82
- template version has new content, a review notice is printed.
83
+ - **Mixed files** (`README.md`) are preserved. If the template version has new
84
+ content, a review notice is printed.
85
+ - **Scratchpad baseline (DEC-0055 / US-0073, Model B):** `.cursor/scratchpad.md`
86
+ is not copied as a manifest file; the installer **materializes** it from the
87
+ packaged template when missing and validates required merged keys (Python
88
+ required). Legacy repos that already committed `.cursor/scratchpad.md` keep it on
89
+ upgrade (not overwritten).
83
90
  - A canonical version marker is stored at `its_magic/.its-magic-version` in your repo.
84
91
  - Installer bootstrap is OS-aware + stack-aware for runbook command defaults
85
92
  (`TEST_COMMAND`, optional `LINT_COMMAND`/`TYPECHECK_COMMAND`) and preserves
@@ -177,8 +184,8 @@ your-project/
177
184
  .cursor/agents/ Subagent definitions
178
185
  .cursor/skills/ Reusable skills
179
186
  .cursor/hooks/ Automation hooks
180
- .cursor/scratchpad.md Shared configuration flags
181
- .cursor/scratchpad.local.example.md
187
+ .cursor/scratchpad.md Materialized shared defaults (Model B; not manifest-copied)
188
+ .cursor/scratchpad.local.example.md Framework default key catalog
182
189
  docs/ Engineering & product docs, runbook
183
190
  sprints/ Sprint tracking artifacts
184
191
  handoffs/ Phase handoff artifacts
@@ -191,21 +198,38 @@ your-project/
191
198
 
192
199
  ### Team mode local overrides (recommended)
193
200
 
194
- Use two layers:
201
+ Use three layers (merge precedence: **local > materialized baseline > example**,
202
+ `DEC-0055`):
195
203
 
196
- - Shared defaults: `.cursor/scratchpad.md` (committed)
197
- - Personal overrides: `.cursor/scratchpad.local.md` (gitignored)
204
+ - Framework catalog: `.cursor/scratchpad.local.example.md` (installed; refreshed on upgrade)
205
+ - Shared team baseline: `.cursor/scratchpad.md` (materialized on install when missing; commit as you prefer)
206
+ - Personal overrides: `.cursor/scratchpad.local.md` (gitignored; never overwritten by install/upgrade)
198
207
 
199
208
  Setup:
200
209
 
201
- 1. Copy `.cursor/scratchpad.local.example.md` to `.cursor/scratchpad.local.md`
202
- 2. Set personal values there (`TEAM_MEMBER`, `ACTIVE_TASK_IDS`, automation style)
203
- 3. Hook merges shared + local (local wins)
210
+ 1. Run `its-magic` — baseline is materialized and merged validation runs (requires Python on PATH for `installer.ps1` / `installer.sh`).
211
+ 2. Optionally copy `.cursor/scratchpad.local.example.md` to `.cursor/scratchpad.local.md` for personal values (`TEAM_MEMBER`, `ACTIVE_TASK_IDS`, ).
204
212
 
205
- Upgrade behavior (US-0057):
206
- - `.cursor/scratchpad.local.example.md` is framework-owned and refreshed on `--mode upgrade`.
213
+ Recovery if `.cursor/scratchpad.md` is missing or merge validation fails:
214
+
215
+ ```bash
216
+ python installer.py --scratchpad-postinstall --target . --mode missing
217
+ ```
218
+
219
+ Upgrade behavior (US-0057 / DEC-0057):
220
+ - Aligns with **DEC-0039** (example vs local ownership), **DEC-0057** (example-first
221
+ ordering relative to baseline materialization), and Model B baseline rules below.
222
+
223
+ - `.cursor/scratchpad.local.example.md` is framework-owned and always refreshed from
224
+ the shipped template during post-install **before** baseline handling (`DEC-0057` **AC-1..AC-3**).
207
225
  - `.cursor/scratchpad.local.md` is user-owned and preserved on `--mode upgrade`.
208
- - Installer output includes scratchpad example refresh status and local-preserved signal.
226
+ - Existing `.cursor/scratchpad.md` is left untouched on upgrade unless missing (then
227
+ materialized) or `overwrite` / fresh materialize paths apply (Model B).
228
+ - Installer output uses `[SCRATCHPAD_LAYER]` lines to distinguish example refresh,
229
+ baseline materialize/skip, and user-local preservation (`DEC-0057` **AC-5**).
230
+ - Paired catalog parity (baseline vs `.cursor/scratchpad.local.example.md`, active and
231
+ `template/`): `python scripts/check-scratchpad-pair-parity.py --repo .` (wired into
232
+ `tests/run-tests.ps1` / `tests/run-tests.sh`; **AC-11**).
209
233
 
210
234
  Deterministic ordering behavior (US-0058):
211
235
  - Mutable artifacts follow `docs/engineering/artifact-ordering-policy.md`.
@@ -383,9 +407,15 @@ Compaction behavior:
383
407
  - Enforced rollover thresholds:
384
408
  - `STATE_HOT_MAX_LINES` (default `1200`)
385
409
  - `STATE_HOT_MAX_CHECKPOINTS` (default `80`)
386
- `/refresh-context` must archive oldest checkpoints into deterministic
387
- `state-pack-*` files when thresholds are exceeded, keeping only bounded recent
388
- checkpoints in hot surface.
410
+ - `PO_TO_TL_HOT_MAX_LINES` (default `800`)
411
+ - `PO_TO_TL_HOT_MAX_SECTIONS` (default `60`)
412
+ - `ARCH_HOT_MAX_LINES` (default `3500`)
413
+ - `ARCH_HOT_MAX_STORY_SECTIONS` (default `120`)
414
+ Triad hot surfaces (`state.md`, `handoffs/po_to_tl.md`,
415
+ `docs/engineering/architecture.md`) must stay within merged scratchpad caps.
416
+ Use `python scripts/enforce-triad-hot-surface.py --check` before completing a
417
+ phase that mutates them; use `--rollover` to archive oldest material into
418
+ deterministic packs when over cap (DEC-0054).
389
419
  Archive verification mismatch fails with
390
420
  `STATE_ARCHIVE_VERIFICATION_FAILED`.
391
421
 
@@ -619,6 +649,32 @@ Fail-closed reason codes:
619
649
  `/auto`, `/verify-work`, and `/release` must validate these tuples before
620
650
  continuation/finalization.
621
651
 
652
+ #### `/auto` phase→role enforcement (US-0069 / DEC-0051)
653
+
654
+ `/auto` uses a deterministic **phase→role matrix** plus scratchpad alternates
655
+ (`AUTO_ROLE_RESEARCH`, `AUTO_ROLE_PLAN_VERIFY`, `AUTO_ROLE_REFRESH_CONTEXT`).
656
+ Before each phase spawn it runs a **preflight capability gate**; missing
657
+ capability stops with `PHASE_ROLE_CAPABILITY_MISSING` (no unrelated-role
658
+ substitution). After each phase, isolation `role` and strict-proof `role` must
659
+ match the same expected role or the run stops with `PHASE_ROLE_MISMATCH`.
660
+ `execute` defaults to `dev`; non-`dev` requires
661
+ `AUTO_EXECUTE_ROLE_OVERRIDE=allowed_non_dev_execute` **and**
662
+ `EXECUTE_OVERRIDE_GOVERNANCE_REF` pointing to a parseable approved waiver. See
663
+ `docs/engineering/runbook.md` and `decisions/DEC-0051.md`.
664
+
665
+ #### `/auto` phase selection policy (US-0070 / DEC-0052)
666
+
667
+ `/auto` builds a **resolved phase plan** from scratchpad before spawning phases:
668
+ exactly one of `AUTO_PHASE_PLAN` (default `full`), `AUTO_PHASE_EXCLUDE`,
669
+ `AUTO_PHASE_INCLUDE`, or `AUTO_PHASE_PROFILE` applies; conflicting selectors
670
+ stop with `PHASE_POLICY_CONFLICT`. Non-skippable safety gates (`qa`,
671
+ `verify-work`, `release`) and evidence-chain closure reinstate omitted phases
672
+ with breadcrumb reasons such as `non_skippable_gate`. `start-from` and resume
673
+ anchors **intersect** with the plan (`START_FROM_PHASE_PLAN_EMPTY_INTERSECTION`
674
+ when empty). Backlog-drain, bulk execute, and team-mode runs **recompute** the
675
+ plan each boundary. See `/auto`, `docs/engineering/runbook.md`, and
676
+ `decisions/DEC-0052.md`.
677
+
622
678
  ### Lightweight interaction
623
679
 
624
680
  Use `/ask` when you want to query the project without triggering the workflow:
package/bin/its-magic.js CHANGED
@@ -118,6 +118,11 @@ Install options:
118
118
  Note: installer bootstraps runbook TEST/LINT/TYPECHECK commands from
119
119
  OS+stack detection; unresolved TEST_COMMAND fails fast with
120
120
  [RUNBOOK_BOOTSTRAP_ERROR] diagnostics.
121
+ Note: scratchpad Model B: .cursor/scratchpad.md is materialized when missing;
122
+ post-install always refreshes .cursor/scratchpad.local.example.md from the
123
+ template before baseline handling. PowerShell/bash installers require
124
+ Python 3 on PATH for merged scratchpad validation. Recovery:
125
+ python installer.py --scratchpad-postinstall --target <repo> --mode missing
121
126
 
122
127
  Clean options:
123
128
  --clean-repo Remove all its-magic workflow artifacts from the target repo
package/installer.ps1 CHANGED
@@ -111,7 +111,7 @@ function Choose-Mode {
111
111
  function Classify-File($RelPath) {
112
112
  $normalized = $RelPath -replace '\\','/'
113
113
 
114
- $mixedFiles = @('.cursor/scratchpad.md', 'README.md')
114
+ $mixedFiles = @('README.md')
115
115
  if ($mixedFiles -contains $normalized) { return 'mixed' }
116
116
 
117
117
  $frameworkPrefixes = @(
@@ -232,7 +232,6 @@ function Get-DetectedRunbookDefaults($TargetRoot) {
232
232
  TYPECHECK_COMMAND = ""
233
233
  }
234
234
 
235
- $testsPs1 = Join-Path $TargetRoot "tests\run-tests.ps1"
236
235
  $testsSh = Join-Path $TargetRoot "tests\run-tests.sh"
237
236
  $pkgPath = Join-Path $TargetRoot "package.json"
238
237
  $goMod = Join-Path $TargetRoot "go.mod"
@@ -261,11 +260,6 @@ function Get-DetectedRunbookDefaults($TargetRoot) {
261
260
  return $defaults
262
261
  }
263
262
 
264
- if (Test-Path $testsPs1 -PathType Leaf) {
265
- $defaults.TEST_COMMAND = "powershell -ExecutionPolicy Bypass -File `"tests/run-tests.ps1`""
266
- return $defaults
267
- }
268
-
269
263
  if (Test-Path $testsSh -PathType Leaf) {
270
264
  $defaults.TEST_COMMAND = "sh tests/run-tests.sh"
271
265
  return $defaults
@@ -391,6 +385,25 @@ function Get-AppVersion($SourceRoot) {
391
385
  return "unknown"
392
386
  }
393
387
 
388
+ function Invoke-ScratchpadPostinstall {
389
+ param(
390
+ [string]$TargetRoot,
391
+ [string]$Mode
392
+ )
393
+ $installerPy = Join-Path $scriptDir "installer.py"
394
+ if (-not (Test-Path $installerPy -PathType Leaf)) {
395
+ Write-Host "[SCRATCHPAD_POSTINSTALL_ERROR] installer.py missing next to installer.ps1."
396
+ exit 1
397
+ }
398
+ $py = Get-Command python -ErrorAction SilentlyContinue
399
+ if (-not $py) {
400
+ Write-Host "[SCRATCHPAD_POSTINSTALL_ERROR] PYTHON_NOT_FOUND: Python is required for scratchpad materialization/validation (Model B). Fix: install Python 3 and re-run."
401
+ exit 1
402
+ }
403
+ & python $installerPy --scratchpad-postinstall --target $TargetRoot --mode $Mode
404
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
405
+ }
406
+
394
407
  function Show-ItsMagicBanner([switch]$IncludeInstallMessage) {
395
408
  $prev = [Console]::OutputEncoding
396
409
  [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
@@ -440,6 +453,9 @@ function Show-ItsMagicHelp($VersionString, $RepoUrl) {
440
453
  Write-Host " Note: installer bootstraps runbook TEST/LINT/TYPECHECK commands from"
441
454
  Write-Host " OS+stack detection; unresolved TEST_COMMAND fails fast with"
442
455
  Write-Host " [RUNBOOK_BOOTSTRAP_ERROR] diagnostics."
456
+ Write-Host " Note: scratchpad Model B: .cursor/scratchpad.md is"
457
+ Write-Host " materialized when missing; Python 3 on PATH is required for validation."
458
+ Write-Host " Recovery: python installer.py --scratchpad-postinstall --target <repo> --mode missing"
443
459
  Write-Host ""
444
460
  Write-Host "Clean options:"
445
461
  Write-Host " --clean-repo Remove all its-magic workflow artifacts from the target repo"
@@ -625,6 +641,8 @@ if ($mode -eq "upgrade") {
625
641
  }
626
642
  }
627
643
 
644
+ Invoke-ScratchpadPostinstall -TargetRoot $targetRoot -Mode "upgrade"
645
+
628
646
  Write-InstalledVersion $targetRoot $appVersion
629
647
  Sync-RootReadmeToItsMagic $targetRoot | Out-Null
630
648
  $runbookBootstrap = Invoke-RunbookBootstrap -TargetRoot $targetRoot
@@ -646,6 +664,7 @@ if ($mode -eq "upgrade") {
646
664
  Write-Host " Preserved (user): $preserved files"
647
665
  if ($scratchpadExampleStatus -eq 'not-seen') { $scratchpadExampleStatus = 'not-in-manifest' }
648
666
  Write-Host " Scratchpad example: $scratchpadExampleStatus (.cursor/scratchpad.local.example.md)"
667
+ Write-Host " Scratchpad layers: post-install refreshed example-first, then baseline (see [SCRATCHPAD_LAYER] lines)."
649
668
  if (Test-Path (Join-Path $targetRoot '.cursor/scratchpad.local.md') -PathType Leaf) {
650
669
  Write-Host " User local file: preserved (.cursor/scratchpad.local.md)"
651
670
  }
@@ -703,6 +722,8 @@ foreach ($rel in $files) {
703
722
  }
704
723
  }
705
724
 
725
+ Invoke-ScratchpadPostinstall -TargetRoot $targetRoot -Mode $mode
726
+
706
727
  Write-InstalledVersion $targetRoot $appVersion
707
728
  Sync-RootReadmeToItsMagic $targetRoot | Out-Null
708
729
  $runbookBootstrap = Invoke-RunbookBootstrap -TargetRoot $targetRoot
package/installer.py CHANGED
@@ -119,7 +119,187 @@ USER_DATA_PREFIXES = (
119
119
  "docs/product/", "docs/engineering/", "docs/user-guides/",
120
120
  "sprints/", "handoffs/", "decisions/",
121
121
  )
122
- MIXED_FILES = {".cursor/scratchpad.md", "README.md"}
122
+ MIXED_FILES = {"README.md"}
123
+
124
+ # Model B (DEC-0055 / US-0073): baseline bytes live in template only; installs materialize `.cursor/scratchpad.md`.
125
+ SCRATCHPAD_BASELINE_REL = os.path.join(".cursor", "scratchpad.md")
126
+ SCRATCHPAD_EXAMPLE_REL = os.path.join(".cursor", "scratchpad.local.example.md")
127
+ SCRATCHPAD_LOCAL_REL = os.path.join(".cursor", "scratchpad.local.md")
128
+
129
+ # After merge (local > baseline > example), these must be non-empty (fail closed).
130
+ REQUIRED_SCRATCHPAD_KEYS = (
131
+ "MAGIC_CONTEXT_STRICT",
132
+ "AUTO_FLOW_MODE",
133
+ "PHASE_MODE",
134
+ "PERMISSION_MODE",
135
+ "AUTO_LOOP_MAX_CYCLES",
136
+ "SYNC_POLICY_MODE",
137
+ "DONE",
138
+ "TEAM_MODE",
139
+ )
140
+
141
+
142
+ def parse_scratchpad_file(path):
143
+ """Parse KEY=value lines; empty values are retained (explicit override to empty)."""
144
+ if not os.path.isfile(path):
145
+ return {}
146
+ out = {}
147
+ with open(path, "r", encoding="utf-8") as f:
148
+ for raw in f:
149
+ line = raw.strip()
150
+ if not line or line.startswith("#") or line.startswith("- "):
151
+ continue
152
+ if "=" not in line:
153
+ continue
154
+ key, _, val = line.partition("=")
155
+ key = key.strip()
156
+ if not key:
157
+ continue
158
+ out[key] = val.strip()
159
+ return out
160
+
161
+
162
+ def merge_scratchpad_layers(target_root):
163
+ """
164
+ Model B merge precedence: local > materialized baseline > example (later wins only when key absent).
165
+ """
166
+ ex_path = os.path.join(target_root, SCRATCHPAD_EXAMPLE_REL)
167
+ base_path = os.path.join(target_root, SCRATCHPAD_BASELINE_REL)
168
+ loc_path = os.path.join(target_root, SCRATCHPAD_LOCAL_REL)
169
+ example = parse_scratchpad_file(ex_path)
170
+ baseline = parse_scratchpad_file(base_path)
171
+ local = parse_scratchpad_file(loc_path)
172
+ merged = {}
173
+ all_keys = set(example) | set(baseline) | set(local)
174
+ for key in all_keys:
175
+ if key in local:
176
+ merged[key] = local[key]
177
+ elif key in baseline:
178
+ merged[key] = baseline[key]
179
+ elif key in example:
180
+ merged[key] = example[key]
181
+ paths = {"example": ex_path, "baseline": base_path, "local": loc_path}
182
+ return merged, paths
183
+
184
+
185
+ def validate_merged_scratchpad(target_root):
186
+ """Return (ok, list of diagnostic lines)."""
187
+ merged, paths = merge_scratchpad_layers(target_root)
188
+ diagnostics = []
189
+ if not os.path.isfile(paths["example"]):
190
+ diagnostics.append(
191
+ "[SCRATCHPAD_MERGE_ERROR] EXAMPLE_LAYER_MISSING: "
192
+ f".cursor/scratchpad.local.example.md not found under {target_root}. "
193
+ "Fix: re-run its-magic install/upgrade."
194
+ )
195
+ if not os.path.isfile(paths["baseline"]):
196
+ diagnostics.append(
197
+ "[SCRATCHPAD_MERGE_ERROR] MATERIALIZED_BASELINE_MISSING: "
198
+ f".cursor/scratchpad.md not found under {target_root}. "
199
+ "Fix: run `python installer.py --scratchpad-postinstall --target <repo> --mode missing` "
200
+ "or re-run its-magic install (Model B materialization; see docs)."
201
+ )
202
+ missing = []
203
+ for key in REQUIRED_SCRATCHPAD_KEYS:
204
+ val = merged.get(key)
205
+ if val is None or str(val).strip() == "":
206
+ missing.append(key)
207
+ if missing:
208
+ diagnostics.append(
209
+ "[SCRATCHPAD_MERGE_ERROR] REQUIRED_KEY_MISSING_AFTER_MERGE: "
210
+ f"keys={','.join(missing)}. Layers consulted: local, baseline|materialized, example "
211
+ f"({paths['local']}, {paths['baseline']}, {paths['example']}). "
212
+ "Fix: set non-empty values in .cursor/scratchpad.local.md or restore materialized baseline from template."
213
+ )
214
+ ok = not diagnostics
215
+ return ok, diagnostics
216
+
217
+
218
+ def materialize_scratchpad_example(target_root, source_root, print_ok=True):
219
+ """
220
+ Always refresh framework-owned scratchpad.local.example from template first
221
+ (example-first ordering before baseline; never touches scratchpad.local.md).
222
+ """
223
+ src = os.path.join(source_root, SCRATCHPAD_EXAMPLE_REL)
224
+ dst = os.path.join(target_root, SCRATCHPAD_EXAMPLE_REL)
225
+ if not os.path.isfile(src):
226
+ print(
227
+ "[SCRATCHPAD_EXAMPLE_ERROR] TEMPLATE_EXAMPLE_MISSING: "
228
+ f"expected template file at {src}. Reinstall its-magic package."
229
+ )
230
+ return False
231
+ ensure_parent(dst)
232
+ shutil.copy2(src, dst)
233
+ if print_ok:
234
+ print(
235
+ "[SCRATCHPAD_LAYER] example_refresh: copied template "
236
+ f"{SCRATCHPAD_EXAMPLE_REL} -> target (ordering: example before baseline)."
237
+ )
238
+ return True
239
+
240
+
241
+ def materialize_scratchpad_baseline(target_root, source_root, mode, print_ok=True):
242
+ """
243
+ Write stable baseline bytes from template when Model B requires it.
244
+ Never touches .cursor/scratchpad.local.md.
245
+ """
246
+ src = os.path.join(source_root, SCRATCHPAD_BASELINE_REL)
247
+ dst = os.path.join(target_root, SCRATCHPAD_BASELINE_REL)
248
+ if not os.path.isfile(src):
249
+ print(
250
+ "[SCRATCHPAD_MATERIALIZE_ERROR] TEMPLATE_BASELINE_MISSING: "
251
+ f"expected template file at {src}. Reinstall its-magic package."
252
+ )
253
+ return False
254
+ wrote = False
255
+ if mode == "overwrite":
256
+ ensure_parent(dst)
257
+ shutil.copy2(src, dst)
258
+ wrote = True
259
+ elif mode == "upgrade":
260
+ if not os.path.isfile(dst):
261
+ ensure_parent(dst)
262
+ shutil.copy2(src, dst)
263
+ wrote = True
264
+ else:
265
+ # missing, interactive
266
+ if not os.path.isfile(dst):
267
+ ensure_parent(dst)
268
+ shutil.copy2(src, dst)
269
+ wrote = True
270
+ if wrote and print_ok:
271
+ print(
272
+ "[SCRATCHPAD_LAYER] baseline_materialize: wrote materialized "
273
+ f"{SCRATCHPAD_BASELINE_REL} from template (Model B)."
274
+ )
275
+ elif print_ok and os.path.isfile(dst):
276
+ print(
277
+ "[SCRATCHPAD_LAYER] baseline_skip: materialized baseline already present "
278
+ f"({SCRATCHPAD_BASELINE_REL}); not overwritten in this mode."
279
+ )
280
+ return True
281
+
282
+
283
+ def run_scratchpad_postinstall(target_root, source_root, mode, print_ok=True):
284
+ if not materialize_scratchpad_example(target_root, source_root, print_ok=print_ok):
285
+ return False
286
+ if not materialize_scratchpad_baseline(target_root, source_root, mode, print_ok=print_ok):
287
+ return False
288
+ ok, diagnostics = validate_merged_scratchpad(target_root)
289
+ for line in diagnostics:
290
+ print(line)
291
+ if ok and print_ok:
292
+ loc = os.path.join(target_root, SCRATCHPAD_LOCAL_REL)
293
+ if os.path.isfile(loc):
294
+ print(
295
+ "[SCRATCHPAD_LAYER] user_local: preserved "
296
+ f"{SCRATCHPAD_LOCAL_REL} (merge precedence unchanged)."
297
+ )
298
+ print(
299
+ "[SCRATCHPAD_POSTINSTALL_OK] Model B: example refreshed, baseline handled, "
300
+ "merged scratchpad validation passed."
301
+ )
302
+ return ok
123
303
 
124
304
 
125
305
  def classify_file(rel_path):
@@ -213,8 +393,6 @@ def package_has_script(target_root, script_name):
213
393
 
214
394
 
215
395
  def detect_runbook_defaults(target_root):
216
- is_windows = os.name == "nt"
217
- tests_ps1 = os.path.join(target_root, "tests", "run-tests.ps1")
218
396
  tests_sh = os.path.join(target_root, "tests", "run-tests.sh")
219
397
  has_pkg = os.path.isfile(os.path.join(target_root, "package.json"))
220
398
  has_py = any(
@@ -235,8 +413,6 @@ def detect_runbook_defaults(target_root):
235
413
  result["TEST_COMMAND"] = "go test ./..."
236
414
  elif has_py:
237
415
  result["TEST_COMMAND"] = "python -m pytest"
238
- elif is_windows and os.path.isfile(tests_ps1):
239
- result["TEST_COMMAND"] = "powershell -ExecutionPolicy Bypass -File \"tests/run-tests.ps1\""
240
416
  elif os.path.isfile(tests_sh):
241
417
  result["TEST_COMMAND"] = "sh tests/run-tests.sh"
242
418
 
@@ -392,6 +568,10 @@ def show_help(version):
392
568
  print(" Note: installer bootstraps runbook TEST/LINT/TYPECHECK commands")
393
569
  print(" from OS+stack detection; unresolved TEST_COMMAND fails fast with")
394
570
  print(" [RUNBOOK_BOOTSTRAP_ERROR] diagnostics.")
571
+ print(" Note: scratchpad Model B: `.cursor/scratchpad.md` is")
572
+ print(" materialized from the packaged template when missing; merged validation")
573
+ print(" requires Python 3 on PATH for installer.ps1 / installer.sh. Recovery:")
574
+ print(" python installer.py --scratchpad-postinstall --target <repo> --mode missing")
395
575
  print()
396
576
  print("Clean options:")
397
577
  print(" --clean-repo Remove all its-magic workflow artifacts from the target repo")
@@ -445,6 +625,11 @@ def main():
445
625
  parser.add_argument("--yes", action="store_true", help="Skip clean confirmation prompt")
446
626
  parser.add_argument("--help", "-h", action="store_true", help="Show help")
447
627
  parser.add_argument("--version", "-v", action="store_true", help="Show version")
628
+ parser.add_argument(
629
+ "--scratchpad-postinstall",
630
+ action="store_true",
631
+ help=argparse.SUPPRESS,
632
+ )
448
633
  args = parser.parse_args()
449
634
 
450
635
  if len(sys.argv) == 1 or args.help:
@@ -455,6 +640,24 @@ def main():
455
640
  print(f"its-magic v{version}")
456
641
  return 0
457
642
 
643
+ if args.scratchpad_postinstall:
644
+ target_root = normalize(args.target) if args.target else normalize(".")
645
+ mode = args.mode or "missing"
646
+ if mode not in ("missing", "overwrite", "interactive", "upgrade"):
647
+ print(
648
+ "[SCRATCHPAD_POSTINSTALL_ERROR] INVALID_MODE: use --mode "
649
+ "missing|overwrite|interactive|upgrade with --scratchpad-postinstall."
650
+ )
651
+ return 1
652
+ if not os.path.isdir(source_root):
653
+ print("[INSTALL_SOURCE_ERROR] template directory is missing. Reinstall its-magic package.")
654
+ return 1
655
+ if not os.path.isdir(target_root):
656
+ print(f"[SCRATCHPAD_POSTINSTALL_ERROR] TARGET_MISSING: {target_root}")
657
+ return 1
658
+ ok = run_scratchpad_postinstall(target_root, source_root, mode, print_ok=True)
659
+ return 0 if ok else 1
660
+
458
661
  if not os.path.isdir(source_root):
459
662
  print("[INSTALL_SOURCE_ERROR] template directory is missing. Reinstall its-magic package.")
460
663
  return 1
@@ -559,6 +762,9 @@ def main():
559
762
  review.append(rel)
560
763
  continue
561
764
 
765
+ if not run_scratchpad_postinstall(target_root, source_root, "upgrade", print_ok=True):
766
+ return 1
767
+
562
768
  write_installed_version(target_root, version)
563
769
  sync_root_readme_to_its_magic(target_root)
564
770
  runbook_ok, runbook_notes = bootstrap_runbook_commands(target_root)
@@ -587,6 +793,10 @@ def main():
587
793
  if scratchpad_example_status == "not-seen":
588
794
  scratchpad_example_status = "not-in-manifest"
589
795
  print(f" Scratchpad example: {scratchpad_example_status} (.cursor/scratchpad.local.example.md)")
796
+ print(
797
+ " Scratchpad layers: post-install refreshed example-first, then baseline "
798
+ "(see [SCRATCHPAD_LAYER] lines)."
799
+ )
590
800
  if os.path.isfile(os.path.join(target_root, ".cursor", "scratchpad.local.md")):
591
801
  print(" User local file: preserved (.cursor/scratchpad.local.md)")
592
802
  if review:
@@ -630,6 +840,9 @@ def main():
630
840
  ensure_parent(dst)
631
841
  shutil.copy2(src, dst)
632
842
 
843
+ if not run_scratchpad_postinstall(target_root, source_root, mode, print_ok=True):
844
+ return 1
845
+
633
846
  write_installed_version(target_root, version)
634
847
  sync_root_readme_to_its_magic(target_root)
635
848
  runbook_ok, runbook_notes = bootstrap_runbook_commands(target_root)
package/installer.sh CHANGED
@@ -46,7 +46,10 @@ show_help() {
46
46
  printf " --create Create the target directory if it does not exist.\n\n"
47
47
  printf " Note: installer bootstraps runbook TEST/LINT/TYPECHECK commands from\n"
48
48
  printf " OS+stack detection; unresolved TEST_COMMAND fails fast with\n"
49
- printf " [RUNBOOK_BOOTSTRAP_ERROR] diagnostics.\n\n"
49
+ printf " [RUNBOOK_BOOTSTRAP_ERROR] diagnostics.\n"
50
+ printf " Note: scratchpad Model B: .cursor/scratchpad.md is\n"
51
+ printf " materialized when missing; Python 3 on PATH is required for validation.\n"
52
+ printf " Recovery: python installer.py --scratchpad-postinstall --target <repo> --mode missing\n\n"
50
53
  printf "Clean options:\n"
51
54
  printf " --clean-repo Remove all its-magic workflow artifacts from the target repo\n"
52
55
  printf " (owned paths from installer manifest, including .cursor,\n"
@@ -130,10 +133,28 @@ choose_mode() {
130
133
  esac
131
134
  }
132
135
 
136
+ scratchpad_postinstall() {
137
+ target_root="$1"
138
+ mode="$2"
139
+ installer_py="$SCRIPT_DIR/installer.py"
140
+ if [ ! -f "$installer_py" ]; then
141
+ printf "%s\n" "[SCRATCHPAD_POSTINSTALL_ERROR] installer.py missing next to installer.sh."
142
+ exit 1
143
+ fi
144
+ if command -v python3 >/dev/null 2>&1; then
145
+ python3 "$installer_py" --scratchpad-postinstall --target "$target_root" --mode "$mode" || exit $?
146
+ elif command -v python >/dev/null 2>&1; then
147
+ python "$installer_py" --scratchpad-postinstall --target "$target_root" --mode "$mode" || exit $?
148
+ else
149
+ printf "%s\n" "[SCRATCHPAD_POSTINSTALL_ERROR] PYTHON_NOT_FOUND: Python 3 is required for scratchpad materialization/validation (Model B)."
150
+ exit 1
151
+ fi
152
+ }
153
+
133
154
  classify_file() {
134
155
  rel="$1"
135
156
  case "$rel" in
136
- .cursor/scratchpad.md|README.md) echo "mixed" ;;
157
+ README.md) echo "mixed" ;;
137
158
  .cursor/commands/*|.cursor/rules/*|.cursor/agents/*|.cursor/skills/*) echo "framework" ;;
138
159
  .cursor/hooks/*|.cursor/hooks.json|.cursor/scratchpad.local.example.md) echo "framework" ;;
139
160
  .github/workflows/*|scripts/validate-and-push*|docs/engineering/context/*|its_magic/*) echo "framework" ;;
@@ -513,6 +534,8 @@ if [ "$MODE" = "upgrade" ]; then
513
534
  fi
514
535
  done
515
536
 
537
+ scratchpad_postinstall "$TARGET_ROOT" "upgrade"
538
+
516
539
  write_installed_version "$TARGET_ROOT" "$APP_VERSION"
517
540
  sync_root_readme_to_its_magic "$TARGET_ROOT" || true
518
541
  bootstrap_runbook_commands "$TARGET_ROOT"
@@ -533,6 +556,7 @@ if [ "$MODE" = "upgrade" ]; then
533
556
  printf " Preserved (user): %s files\n" "$count_preserved"
534
557
  [ "$scratchpad_example_status" = "not-seen" ] && scratchpad_example_status="not-in-manifest"
535
558
  printf " Scratchpad example: %s (.cursor/scratchpad.local.example.md)\n" "$scratchpad_example_status"
559
+ printf " Scratchpad layers: post-install refreshed example-first, then baseline (see [SCRATCHPAD_LAYER] lines).\n"
536
560
  [ -f "$TARGET_ROOT/.cursor/scratchpad.local.md" ] && printf " User local file: preserved (.cursor/scratchpad.local.md)\n"
537
561
  if [ "$count_review" -gt 0 ]; then
538
562
  printf "\n \033[1;35mReview recommended: %s files\033[0m\n" "$count_review"
@@ -581,6 +605,8 @@ for rel in $FILES; do
581
605
  fi
582
606
  done
583
607
 
608
+ scratchpad_postinstall "$TARGET_ROOT" "$MODE"
609
+
584
610
  write_installed_version "$TARGET_ROOT" "$APP_VERSION"
585
611
  sync_root_readme_to_its_magic "$TARGET_ROOT" || true
586
612
  bootstrap_runbook_commands "$TARGET_ROOT"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "its-magic",
3
- "version": "0.1.2-29",
3
+ "version": "0.1.2-32",
4
4
  "description": "its-magic - AI dev team workflow for Cursor.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -78,6 +78,14 @@ description: "its-magic architecture: define approach, risks, and decisions."
78
78
  - If `USER_GUIDE_MODE=0`, add no required user-guide steps or blocking checks (zero overhead).
79
79
  - If `USER_GUIDE_MODE=1`, reference canonical user-guide path and schema in
80
80
  architecture/state for in-scope feature stories; see runbook user-guide section.
81
+ 9. Triad hot-surface gate (DEC-0054) when `docs/engineering/architecture.md` is
82
+ mutated:
83
+ - run `python scripts/enforce-triad-hot-surface.py --rollover` then `--check`
84
+ from repository root,
85
+ - on failure stop with `STATE_ARCHIVE_REQUIRED` or
86
+ `ARTIFACT_HOT_SURFACE_OVERSIZE`,
87
+ - preserve non-target history in archive packs only (never delete unrelated
88
+ story sections without archival evidence).
81
89
 
82
90
  ## Cross-phase ownership guard (US-0061 / DEC-0043)
83
91