its-magic 0.1.2-29 → 0.1.2-31

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,20 +198,30 @@ 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`, ).
212
+
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
+ ```
204
218
 
205
219
  Upgrade behavior (US-0057):
220
+ - Aligns with **DEC-0039** (example vs local ownership) and Model B baseline rules below.
221
+
206
222
  - `.cursor/scratchpad.local.example.md` is framework-owned and refreshed on `--mode upgrade`.
207
223
  - `.cursor/scratchpad.local.md` is user-owned and preserved on `--mode upgrade`.
224
+ - Existing `.cursor/scratchpad.md` is left untouched on upgrade (legacy parity).
208
225
  - Installer output includes scratchpad example refresh status and local-preserved signal.
209
226
 
210
227
  Deterministic ordering behavior (US-0058):
@@ -383,9 +400,15 @@ Compaction behavior:
383
400
  - Enforced rollover thresholds:
384
401
  - `STATE_HOT_MAX_LINES` (default `1200`)
385
402
  - `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.
403
+ - `PO_TO_TL_HOT_MAX_LINES` (default `800`)
404
+ - `PO_TO_TL_HOT_MAX_SECTIONS` (default `60`)
405
+ - `ARCH_HOT_MAX_LINES` (default `3500`)
406
+ - `ARCH_HOT_MAX_STORY_SECTIONS` (default `120`)
407
+ Triad hot surfaces (`state.md`, `handoffs/po_to_tl.md`,
408
+ `docs/engineering/architecture.md`) must stay within merged scratchpad caps.
409
+ Use `python scripts/enforce-triad-hot-surface.py --check` before completing a
410
+ phase that mutates them; use `--rollover` to archive oldest material into
411
+ deterministic packs when over cap (DEC-0054).
389
412
  Archive verification mismatch fails with
390
413
  `STATE_ARCHIVE_VERIFICATION_FAILED`.
391
414
 
@@ -619,6 +642,32 @@ Fail-closed reason codes:
619
642
  `/auto`, `/verify-work`, and `/release` must validate these tuples before
620
643
  continuation/finalization.
621
644
 
645
+ #### `/auto` phase→role enforcement (US-0069 / DEC-0051)
646
+
647
+ `/auto` uses a deterministic **phase→role matrix** plus scratchpad alternates
648
+ (`AUTO_ROLE_RESEARCH`, `AUTO_ROLE_PLAN_VERIFY`, `AUTO_ROLE_REFRESH_CONTEXT`).
649
+ Before each phase spawn it runs a **preflight capability gate**; missing
650
+ capability stops with `PHASE_ROLE_CAPABILITY_MISSING` (no unrelated-role
651
+ substitution). After each phase, isolation `role` and strict-proof `role` must
652
+ match the same expected role or the run stops with `PHASE_ROLE_MISMATCH`.
653
+ `execute` defaults to `dev`; non-`dev` requires
654
+ `AUTO_EXECUTE_ROLE_OVERRIDE=allowed_non_dev_execute` **and**
655
+ `EXECUTE_OVERRIDE_GOVERNANCE_REF` pointing to a parseable approved waiver. See
656
+ `docs/engineering/runbook.md` and `decisions/DEC-0051.md`.
657
+
658
+ #### `/auto` phase selection policy (US-0070 / DEC-0052)
659
+
660
+ `/auto` builds a **resolved phase plan** from scratchpad before spawning phases:
661
+ exactly one of `AUTO_PHASE_PLAN` (default `full`), `AUTO_PHASE_EXCLUDE`,
662
+ `AUTO_PHASE_INCLUDE`, or `AUTO_PHASE_PROFILE` applies; conflicting selectors
663
+ stop with `PHASE_POLICY_CONFLICT`. Non-skippable safety gates (`qa`,
664
+ `verify-work`, `release`) and evidence-chain closure reinstate omitted phases
665
+ with breadcrumb reasons such as `non_skippable_gate`. `start-from` and resume
666
+ anchors **intersect** with the plan (`START_FROM_PHASE_PLAN_EMPTY_INTERSECTION`
667
+ when empty). Backlog-drain, bulk execute, and team-mode runs **recompute** the
668
+ plan each boundary. See `/auto`, `docs/engineering/runbook.md`, and
669
+ `decisions/DEC-0052.md`.
670
+
622
671
  ### Lightweight interaction
623
672
 
624
673
  Use `/ask` when you want to query the project without triggering the workflow:
package/bin/its-magic.js CHANGED
@@ -118,6 +118,10 @@ 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
122
+ materialized when missing; PowerShell/bash installers require Python 3
123
+ on PATH for merged scratchpad validation. Recovery:
124
+ python installer.py --scratchpad-postinstall --target <repo> --mode missing
121
125
 
122
126
  Clean options:
123
127
  --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
@@ -703,6 +721,8 @@ foreach ($rel in $files) {
703
721
  }
704
722
  }
705
723
 
724
+ Invoke-ScratchpadPostinstall -TargetRoot $targetRoot -Mode $mode
725
+
706
726
  Write-InstalledVersion $targetRoot $appVersion
707
727
  Sync-RootReadmeToItsMagic $targetRoot | Out-Null
708
728
  $runbookBootstrap = Invoke-RunbookBootstrap -TargetRoot $targetRoot
package/installer.py CHANGED
@@ -119,7 +119,143 @@ 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_baseline(target_root, source_root, mode):
219
+ """
220
+ Write stable baseline bytes from template when Model B requires it.
221
+ Never touches .cursor/scratchpad.local.md.
222
+ """
223
+ src = os.path.join(source_root, SCRATCHPAD_BASELINE_REL)
224
+ dst = os.path.join(target_root, SCRATCHPAD_BASELINE_REL)
225
+ if not os.path.isfile(src):
226
+ print(
227
+ "[SCRATCHPAD_MATERIALIZE_ERROR] TEMPLATE_BASELINE_MISSING: "
228
+ f"expected template file at {src}. Reinstall its-magic package."
229
+ )
230
+ return False
231
+ if mode == "overwrite":
232
+ ensure_parent(dst)
233
+ shutil.copy2(src, dst)
234
+ return True
235
+ if mode == "upgrade":
236
+ if not os.path.isfile(dst):
237
+ ensure_parent(dst)
238
+ shutil.copy2(src, dst)
239
+ return True
240
+ # missing, interactive
241
+ if not os.path.isfile(dst):
242
+ ensure_parent(dst)
243
+ shutil.copy2(src, dst)
244
+ return True
245
+
246
+
247
+ def run_scratchpad_postinstall(target_root, source_root, mode, print_ok=True):
248
+ if not materialize_scratchpad_baseline(target_root, source_root, mode):
249
+ return False
250
+ ok, diagnostics = validate_merged_scratchpad(target_root)
251
+ for line in diagnostics:
252
+ print(line)
253
+ if ok and print_ok:
254
+ print(
255
+ "[SCRATCHPAD_POSTINSTALL_OK] Model B: materialized baseline (if required) "
256
+ "and merged scratchpad validation passed."
257
+ )
258
+ return ok
123
259
 
124
260
 
125
261
  def classify_file(rel_path):
@@ -213,8 +349,6 @@ def package_has_script(target_root, script_name):
213
349
 
214
350
 
215
351
  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
352
  tests_sh = os.path.join(target_root, "tests", "run-tests.sh")
219
353
  has_pkg = os.path.isfile(os.path.join(target_root, "package.json"))
220
354
  has_py = any(
@@ -235,8 +369,6 @@ def detect_runbook_defaults(target_root):
235
369
  result["TEST_COMMAND"] = "go test ./..."
236
370
  elif has_py:
237
371
  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
372
  elif os.path.isfile(tests_sh):
241
373
  result["TEST_COMMAND"] = "sh tests/run-tests.sh"
242
374
 
@@ -392,6 +524,10 @@ def show_help(version):
392
524
  print(" Note: installer bootstraps runbook TEST/LINT/TYPECHECK commands")
393
525
  print(" from OS+stack detection; unresolved TEST_COMMAND fails fast with")
394
526
  print(" [RUNBOOK_BOOTSTRAP_ERROR] diagnostics.")
527
+ print(" Note: scratchpad Model B: `.cursor/scratchpad.md` is")
528
+ print(" materialized from the packaged template when missing; merged validation")
529
+ print(" requires Python 3 on PATH for installer.ps1 / installer.sh. Recovery:")
530
+ print(" python installer.py --scratchpad-postinstall --target <repo> --mode missing")
395
531
  print()
396
532
  print("Clean options:")
397
533
  print(" --clean-repo Remove all its-magic workflow artifacts from the target repo")
@@ -445,6 +581,11 @@ def main():
445
581
  parser.add_argument("--yes", action="store_true", help="Skip clean confirmation prompt")
446
582
  parser.add_argument("--help", "-h", action="store_true", help="Show help")
447
583
  parser.add_argument("--version", "-v", action="store_true", help="Show version")
584
+ parser.add_argument(
585
+ "--scratchpad-postinstall",
586
+ action="store_true",
587
+ help=argparse.SUPPRESS,
588
+ )
448
589
  args = parser.parse_args()
449
590
 
450
591
  if len(sys.argv) == 1 or args.help:
@@ -455,6 +596,24 @@ def main():
455
596
  print(f"its-magic v{version}")
456
597
  return 0
457
598
 
599
+ if args.scratchpad_postinstall:
600
+ target_root = normalize(args.target) if args.target else normalize(".")
601
+ mode = args.mode or "missing"
602
+ if mode not in ("missing", "overwrite", "interactive", "upgrade"):
603
+ print(
604
+ "[SCRATCHPAD_POSTINSTALL_ERROR] INVALID_MODE: use --mode "
605
+ "missing|overwrite|interactive|upgrade with --scratchpad-postinstall."
606
+ )
607
+ return 1
608
+ if not os.path.isdir(source_root):
609
+ print("[INSTALL_SOURCE_ERROR] template directory is missing. Reinstall its-magic package.")
610
+ return 1
611
+ if not os.path.isdir(target_root):
612
+ print(f"[SCRATCHPAD_POSTINSTALL_ERROR] TARGET_MISSING: {target_root}")
613
+ return 1
614
+ ok = run_scratchpad_postinstall(target_root, source_root, mode, print_ok=True)
615
+ return 0 if ok else 1
616
+
458
617
  if not os.path.isdir(source_root):
459
618
  print("[INSTALL_SOURCE_ERROR] template directory is missing. Reinstall its-magic package.")
460
619
  return 1
@@ -559,6 +718,9 @@ def main():
559
718
  review.append(rel)
560
719
  continue
561
720
 
721
+ if not run_scratchpad_postinstall(target_root, source_root, "upgrade", print_ok=True):
722
+ return 1
723
+
562
724
  write_installed_version(target_root, version)
563
725
  sync_root_readme_to_its_magic(target_root)
564
726
  runbook_ok, runbook_notes = bootstrap_runbook_commands(target_root)
@@ -630,6 +792,9 @@ def main():
630
792
  ensure_parent(dst)
631
793
  shutil.copy2(src, dst)
632
794
 
795
+ if not run_scratchpad_postinstall(target_root, source_root, mode, print_ok=True):
796
+ return 1
797
+
633
798
  write_installed_version(target_root, version)
634
799
  sync_root_readme_to_its_magic(target_root)
635
800
  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"
@@ -581,6 +604,8 @@ for rel in $FILES; do
581
604
  fi
582
605
  done
583
606
 
607
+ scratchpad_postinstall "$TARGET_ROOT" "$MODE"
608
+
584
609
  write_installed_version "$TARGET_ROOT" "$APP_VERSION"
585
610
  sync_root_readme_to_its_magic "$TARGET_ROOT" || true
586
611
  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-31",
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