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 +72 -16
- package/bin/its-magic.js +5 -0
- package/installer.ps1 +28 -7
- package/installer.py +218 -5
- package/installer.sh +28 -2
- package/package.json +1 -1
- package/template/.cursor/commands/architecture.md +8 -0
- package/template/.cursor/commands/auto.md +266 -11
- package/template/.cursor/commands/discovery.md +11 -0
- package/template/.cursor/commands/execute.md +23 -0
- package/template/.cursor/commands/intake.md +8 -0
- package/template/.cursor/commands/qa.md +6 -0
- package/template/.cursor/commands/refresh-context.md +18 -8
- package/template/.cursor/commands/release.md +9 -1
- package/template/.cursor/rules/quality.mdc +7 -0
- package/template/.cursor/scratchpad.local.example.md +50 -13
- package/template/.cursor/scratchpad.md +47 -10
- package/template/README.md +74 -20
- package/template/docs/engineering/context/installer-owned-paths.manifest +5 -1
- package/template/docs/engineering/phase-context.md +16 -0
- package/template/docs/engineering/runbook.md +234 -14
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** (
|
|
82
|
-
|
|
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
|
|
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
|
|
201
|
+
Use three layers (merge precedence: **local > materialized baseline > example**,
|
|
202
|
+
`DEC-0055`):
|
|
195
203
|
|
|
196
|
-
-
|
|
197
|
-
-
|
|
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.
|
|
202
|
-
2.
|
|
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
|
-
|
|
206
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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 = @('
|
|
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 = {"
|
|
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
|
|
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
|
-
|
|
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
|
@@ -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
|
|