@hegemonart/get-design-done 1.13.3 → 1.14.2

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.
@@ -32,6 +32,7 @@ const { spawn } = require('child_process');
32
32
  const BUDGET_PATH = path.join(process.cwd(), '.design', 'budget.json');
33
33
  const MANIFEST_PATH = path.join(process.cwd(), '.design', 'cache-manifest.json');
34
34
  const TELEMETRY_PATH = path.join(process.cwd(), '.design', 'telemetry', 'costs.jsonl');
35
+ const PHASE_TOTALS_PATH = path.join(process.cwd(), '.design', 'telemetry', 'phase-totals.json');
35
36
  const STATE_PATH = path.join(process.cwd(), '.design', 'STATE.md');
36
37
 
37
38
  // ---- budget.json loader with defaults per D-12 ----
@@ -49,8 +50,18 @@ function loadBudget() {
49
50
  catch { return defaults; }
50
51
  }
51
52
 
52
- // ---- cumulative phase spend from telemetry (D-02 per_phase_cap_usd check) ----
53
+ // ---- cumulative phase spend (WR-02) ----
54
+ // Reads from the lightweight phase-totals.json written by aggregate-agent-metrics.js
55
+ // instead of replaying the full costs.jsonl on every hook invocation.
56
+ // Falls back to 0 when the file doesn't exist yet (early in a session).
53
57
  function currentPhaseSpend(phase) {
58
+ if (fs.existsSync(PHASE_TOTALS_PATH)) {
59
+ try {
60
+ const data = JSON.parse(fs.readFileSync(PHASE_TOTALS_PATH, 'utf8'));
61
+ return Number(data.totals?.[phase] || 0);
62
+ } catch { /* fall through */ }
63
+ }
64
+ // Fallback: replay JSONL when phase-totals.json not yet written (first spawn of session).
54
65
  if (!fs.existsSync(TELEMETRY_PATH)) return 0;
55
66
  const lines = fs.readFileSync(TELEMETRY_PATH, 'utf8').split(/\r?\n/).filter(Boolean);
56
67
  let sum = 0;
@@ -118,7 +129,7 @@ function spawnAggregator() {
118
129
  cwd: process.cwd(),
119
130
  detached: true,
120
131
  stdio: 'ignore',
121
- env: process.env,
132
+ env: { PATH: process.env.PATH }, // IN-02: minimal env; aggregator needs no secrets
122
133
  });
123
134
  child.unref();
124
135
  } catch {
@@ -6,16 +6,11 @@
6
6
  */
7
7
 
8
8
  const readline = require('readline');
9
+ const path = require('path');
10
+ const { INJECTION_PATTERNS: RAW_PATTERNS } = require(path.join(__dirname, '..', 'scripts', 'injection-patterns.cjs'));
9
11
 
10
- const INJECTION_PATTERNS = [
11
- /ignore\s+(all\s+)?(previous|prior|above)\s+instructions?/i,
12
- /disregard\s+(all\s+)?(previous|prior|above)\s+instructions?/i,
13
- /you\s+are\s+now\s+a\s+different/i,
14
- /system\s*:\s*you\s+are/i,
15
- /<\s*\/?\s*(system|assistant|human)\s*>/i,
16
- /\[INST\]/i,
17
- /###\s*instruction/i,
18
- ];
12
+ // The hook needs bare RegExp objects; extract them from the shared {name,re} entries.
13
+ const INJECTION_PATTERNS = RAW_PATTERNS.map(p => p.re);
19
14
 
20
15
  async function main() {
21
16
  const rl = readline.createInterface({ input: process.stdin });
@@ -165,6 +165,14 @@ if [ "${BASH_SOURCE[0]}" = "$0" ]; then
165
165
  BODY_EXCERPT="$(printf '%s' "${RAW}" | extract_body)"
166
166
  # Strip control chars defensively (T-13.3-03)
167
167
  BODY_EXCERPT="$(printf '%s' "${BODY_EXCERPT}" | tr -d '\000-\010\013\014\016-\037')"
168
+ # Strip double-quotes so the JSON round-trip sed read-back cannot be injected via a
169
+ # crafted release body. Body is display-only — losing quotes is acceptable.
170
+ BODY_EXCERPT="$(printf '%s' "${BODY_EXCERPT}" | tr -d '"')"
171
+ # Validate LATEST_TAG is a safe semver string before trusting it (CR-02).
172
+ if ! printf '%s' "${LATEST_TAG}" | grep -qE '^v?[0-9]+\.[0-9]+(\.[0-9]+)*$'; then
173
+ log "LATEST_TAG '${LATEST_TAG}' failed semver safety check — aborting cache write"
174
+ LATEST_TAG=""
175
+ fi
168
176
  if [ -n "${LATEST_TAG}" ]; then
169
177
  read -r DELTA_STATE DELTA_KIND <<EOF
170
178
  $(classify_delta "${DISPLAY_CURRENT}" "${LATEST_TAG}")
@@ -195,6 +203,11 @@ EOF
195
203
 
196
204
  C_LATEST="$(grep -E '"latest_tag"' "${CACHE}" 2>/dev/null | head -n1 | sed -E 's/.*"latest_tag"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')"
197
205
  C_DELTA="$(grep -E '"delta"' "${CACHE}" 2>/dev/null | head -n1 | sed -E 's/.*"delta"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')"
206
+ # Allowlist-gate C_DELTA before it reaches any shell context (WR-04).
207
+ case "${C_DELTA:-}" in
208
+ major|minor|patch|off-cadence|none) : ;;
209
+ *) C_DELTA="unknown" ;;
210
+ esac
198
211
  C_NEWER="$(grep -E '"is_newer"' "${CACHE}" 2>/dev/null | head -n1 | sed -E 's/.*"is_newer"[[:space:]]*:[[:space:]]*(true|false).*/\1/')"
199
212
  C_BODY="$(grep -E '"changelog_excerpt"' "${CACHE}" 2>/dev/null | head -n1 | sed -E 's/.*"changelog_excerpt"[[:space:]]*:[[:space:]]*"(.*)".*/\1/' | sed -E 's/\\n/\n/g')"
200
213
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hegemonart/get-design-done",
3
- "version": "1.13.3",
3
+ "version": "1.14.2",
4
4
  "description": "A Claude Code plugin for systematic design improvement",
5
5
  "author": "Hegemon",
6
6
  "homepage": "https://github.com/hegemonart/get-design-done",
@@ -25,6 +25,9 @@
25
25
  "CHANGELOG.md",
26
26
  "LICENSE"
27
27
  ],
28
+ "bin": {
29
+ "get-design-done": "./scripts/install.cjs"
30
+ },
28
31
  "publishConfig": {
29
32
  "access": "public",
30
33
  "provenance": true
@@ -0,0 +1,102 @@
1
+ # AI-Native Design Tool Interface — Capability Contract
2
+
3
+ This file defines the capability-based contract that AI-native design tools must implement to integrate with the get-design-done pipeline. Two sub-categories are defined: **canvas** and **component-generator**. Future tools implement one sub-category and plug in via the same probe/read/write or probe/generate/adopt surface.
4
+
5
+ ---
6
+
7
+ ## Sub-Categories
8
+
9
+ ### Canvas Tools
10
+
11
+ Canvas tools treat the design canvas as both source AND destination. They expose a bidirectional read+write surface.
12
+
13
+ **Contract:**
14
+
15
+ ```
16
+ probe() → { available | unavailable | not_configured }
17
+
18
+ read(selection) → {
19
+ jsx: string, // React JSX of component tree
20
+ styles: object, // computed CSS styles
21
+ screenshot: base64_png, // visual snapshot
22
+ metadata: object // component name, bounds, id
23
+ }
24
+
25
+ write(proposal) → { confirmed | rejected }
26
+ ```
27
+
28
+ **Implementations:**
29
+ - `connections/paper-design.md` — MCP-based; 24-tool server; budget: 100 calls/week (free)
30
+ - `connections/pencil-dev.md` — file-based; `.pen` YAML spec files; git-tracked; no MCP
31
+
32
+ **Pipeline stages:** `explore` (read) + `verify` (screenshot) + `design` (write via writer agent)
33
+
34
+ ---
35
+
36
+ ### Component Generators
37
+
38
+ Component generators produce UI component code from a natural-language description and an optional design-system target. They expose a generative one-way (or roundtrip) surface.
39
+
40
+ **Contract:**
41
+
42
+ ```
43
+ probe() → { available | unavailable | not_configured }
44
+
45
+ generate(description: string, ds: "shadcn"|"tailwind"|"mantine"|"chakra") → {
46
+ code: string, // component source code
47
+ preview_url: string, // hosted preview URL
48
+ variants: array, // multiple generated variations
49
+ component_id: string // for adopt/annotate operations
50
+ }
51
+
52
+ adopt(variant: object) → { confirmed | rejected }
53
+ ```
54
+
55
+ **Implementations:**
56
+ - `connections/21st-dev.md` — Magic MCP; `npx @21st-dev/magic@latest init`; marketplace prior-art gate
57
+ - `connections/magic-patterns.md` — Claude connector (`mcp__magic_patterns*`) + API key fallback; DS-aware generation
58
+
59
+ **Pipeline stages:** `explore` (prior-art gate for 21st.dev) + `design` (generate + adopt)
60
+
61
+ ---
62
+
63
+ ## Shared Probe Pattern
64
+
65
+ All AI-native tools use the three-value status schema from `connections/connections.md`:
66
+
67
+ | Status | Meaning |
68
+ |--------|---------|
69
+ | `available` | Tool confirmed present and responsive |
70
+ | `unavailable` | Tool present but errored (rate-limited, auth failure) |
71
+ | `not_configured` | ToolSearch returned empty or no .pen files found |
72
+
73
+ STATE.md format: `<tool-name>: <status>` in the `<connections>` block.
74
+
75
+ ---
76
+
77
+ ## Extending with Future Tools
78
+
79
+ To add a new AI-native design tool:
80
+
81
+ 1. Determine sub-category: **canvas** (bidirectional design source) or **component-generator** (code generator).
82
+ 2. Create `connections/<tool-name>.md` following the frozen template in `connections/figma.md`.
83
+ 3. Implement `probe()` using ToolSearch or file-based check. Write status to STATE.md.
84
+ 4. For **canvas**: expose `read()` and `write()` surfaces via the corresponding agent.
85
+ 5. For **component-generator**: implement `generate()` and `adopt()` in `agents/design-component-generator.md` as a new `<!-- impl: <tool> -->` section.
86
+ 6. Add a row to `connections/connections.md` capability matrix with `canvas` or `generator` column marked.
87
+ 7. Append to `test-fixture/baselines/current/connection-list.txt` in sorted order.
88
+
89
+ ---
90
+
91
+ ## Candidate Tools (Backlog)
92
+
93
+ | Tool | Sub-category | Priority | Notes |
94
+ |------|-------------|----------|-------|
95
+ | Subframe | canvas | high | MCP-based; production-ready components; check for `mcp__subframe*` |
96
+ | v0.dev | generator | high | Vercel product; generates shadcn/tailwind; check for `mcp__v0*` |
97
+ | Galileo AI | generator | medium | Enterprise DS generation; API key required |
98
+ | Builder.io Visual Copilot | canvas + generator | medium | Figma plugin + code export; check for `mcp__builder*` |
99
+ | Locofy | generator | low | Figma→React/Next.js; Figma plugin-based |
100
+ | Anima | canvas | low | Figma→React; Figma plugin-based |
101
+ | Plasmic | generator | medium | Headless CMS + visual builder; `mcp__plasmic*` |
102
+ | TeleportHQ | generator | low | Code export + collaboration |
@@ -25,6 +25,7 @@ const os = require('os');
25
25
  const CWD = process.cwd();
26
26
  const TELEMETRY_PATH = path.join(CWD, '.design', 'telemetry', 'costs.jsonl');
27
27
  const METRICS_PATH = path.join(CWD, '.design', 'agent-metrics.json');
28
+ const PHASE_TOTALS_PATH = path.join(CWD, '.design', 'telemetry', 'phase-totals.json');
28
29
  const AGENTS_DIR = path.join(CWD, 'agents');
29
30
 
30
31
  // ---- frontmatter reader (no YAML dep) ----
@@ -124,6 +125,18 @@ function writeAtomic(filePath, content) {
124
125
  fs.renameSync(tmp, filePath);
125
126
  }
126
127
 
128
+ // ---- phase totals aggregator (WR-02: avoids full JSONL replay in budget enforcer) ----
129
+ function aggregateByPhase(rows) {
130
+ const byPhase = {};
131
+ for (const r of rows) {
132
+ const phase = r.phase || 'unknown';
133
+ byPhase[phase] = (byPhase[phase] || 0) + Number(r.est_cost_usd || 0);
134
+ }
135
+ // Round to 6dp to match per-agent precision
136
+ for (const k of Object.keys(byPhase)) byPhase[k] = Number(byPhase[k].toFixed(6));
137
+ return byPhase;
138
+ }
139
+
127
140
  // ---- main ----
128
141
  function main() {
129
142
  const rows = readTelemetryRows();
@@ -133,6 +146,13 @@ function main() {
133
146
  agents,
134
147
  };
135
148
  writeAtomic(METRICS_PATH, JSON.stringify(payload, null, 2) + '\n');
149
+ // Write lightweight phase-totals.json so budget-enforcer can read phase spend
150
+ // in O(1) without replaying the full JSONL on every agent spawn (WR-02).
151
+ const phaseTotals = {
152
+ generated_at: new Date().toISOString(),
153
+ totals: aggregateByPhase(rows),
154
+ };
155
+ writeAtomic(PHASE_TOTALS_PATH, JSON.stringify(phaseTotals, null, 2) + '\n');
136
156
  }
137
157
 
138
158
  try {
@@ -17,7 +17,7 @@
17
17
 
18
18
  const fs = require('fs');
19
19
  const path = require('path');
20
- const { execSync } = require('child_process');
20
+ const { spawnSync } = require('child_process');
21
21
 
22
22
  const ROOT = process.cwd();
23
23
  const INTEL_DIR = path.join(ROOT, '.design', 'intel');
@@ -46,15 +46,23 @@ function writeSlice(name, data) {
46
46
 
47
47
  function gitHash(filePath) {
48
48
  try {
49
- return execSync(`git log -1 --format=%h -- "${filePath}"`, { stdio: ['pipe', 'pipe', 'ignore'] })
50
- .toString().trim() || 'untracked';
49
+ const r = spawnSync('git', ['log', '-1', '--format=%h', '--', filePath], {
50
+ stdio: ['pipe', 'pipe', 'ignore'],
51
+ encoding: 'utf8',
52
+ timeout: 5000,
53
+ });
54
+ return r.stdout.trim() || 'untracked';
51
55
  } catch { return 'untracked'; }
52
56
  }
53
57
 
54
58
  function headHash() {
55
59
  try {
56
- return execSync('git rev-parse --short HEAD', { stdio: ['pipe', 'pipe', 'ignore'] })
57
- .toString().trim();
60
+ const r = spawnSync('git', ['rev-parse', '--short', 'HEAD'], {
61
+ stdio: ['pipe', 'pipe', 'ignore'],
62
+ encoding: 'utf8',
63
+ timeout: 5000,
64
+ });
65
+ return r.stdout.trim() || 'unknown';
58
66
  } catch { return 'unknown'; }
59
67
  }
60
68
 
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+ // Shared prompt-injection patterns — single source of truth for both
3
+ // hooks/gdd-read-injection-scanner.js (runtime hook) and
4
+ // scripts/run-injection-scanner-ci.cjs (CI scanner).
5
+ // Add new patterns here; both consumers pick them up automatically.
6
+
7
+ const INJECTION_PATTERNS = [
8
+ { name: 'ignore previous', re: /ignore\s+(all\s+)?(previous|prior|above)\s+instructions?/i },
9
+ { name: 'disregard previous', re: /disregard\s+(all\s+)?(previous|prior|above)\s+instructions?/i },
10
+ { name: 'you are now a different', re: /you\s+are\s+now\s+a\s+different/i },
11
+ { name: 'system: you are', re: /system\s*:\s*you\s+are/i },
12
+ { name: 'role tag injection', re: /<\s*\/?\s*(system|assistant|human)\s*>/i },
13
+ { name: '[INST] fragment', re: /\[INST\]/i },
14
+ { name: '### instruction fragment',re: /###\s*instruction/i },
15
+ ];
16
+
17
+ module.exports = { INJECTION_PATTERNS };
@@ -13,16 +13,7 @@ const path = require('path');
13
13
 
14
14
  const REPO_ROOT = path.resolve(__dirname, '..');
15
15
 
16
- // Patterns mirror hooks/gdd-read-injection-scanner.js.
17
- const INJECTION_PATTERNS = [
18
- { name: 'ignore previous', re: /ignore\s+(all\s+)?(previous|prior|above)\s+instructions?/i },
19
- { name: 'disregard previous', re: /disregard\s+(all\s+)?(previous|prior|above)\s+instructions?/i },
20
- { name: 'you are now a different', re: /you\s+are\s+now\s+a\s+different/i },
21
- { name: 'system: you are', re: /system\s*:\s*you\s+are/i },
22
- { name: 'role tag injection', re: /<\s*\/?\s*(system|assistant|human)\s*>/i },
23
- { name: '[INST] fragment', re: /\[INST\]/i },
24
- { name: '### instruction fragment', re: /###\s*instruction/i },
25
- ];
16
+ const { INJECTION_PATTERNS } = require('./injection-patterns.cjs');
26
17
 
27
18
  function walkMd(dir, out) {
28
19
  if (!fs.existsSync(dir)) return;
@@ -35,7 +35,11 @@ fi
35
35
 
36
36
  # Count actual fixture files (should be 4 frozen feeds + 1 README; we only
37
37
  # care that at least one XML/JSON fixture is present).
38
- FIXTURE_COUNT=$(find "$FIXTURE_DIR" -maxdepth 1 -type f \( -name '*.atom' -o -name '*.rss' -o -name '*.json' \) | wc -l | tr -d ' ')
38
+ # Use null-delimited find to handle filenames with spaces/newlines (WR-05).
39
+ FIXTURE_COUNT=0
40
+ while IFS= read -r -d '' _f; do
41
+ FIXTURE_COUNT=$((FIXTURE_COUNT + 1))
42
+ done < <(find "$FIXTURE_DIR" -maxdepth 1 -type f \( -name '*.atom' -o -name '*.rss' -o -name '*.json' \) -print0)
39
43
  if [ "$FIXTURE_COUNT" -lt 1 ]; then
40
44
  echo "FAIL: $FIXTURE_DIR contains no feed fixtures (.atom/.rss/.json)." >&2
41
45
  exit 1
@@ -35,6 +35,8 @@ Flags can be combined: `--refresh --prompt` is valid (re-fetch, then enrich). `-
35
35
  - Print: `No cache. Network may be unreachable or the hook has not run yet. Try /gdd:check-update --refresh.`
36
36
  - Exit.
37
37
 
38
+ <!-- markdownlint-disable MD025 -->
39
+
38
40
  4. **Dismiss path** (if `--dismiss` in flags):
39
41
  Compute new config contents and write atomically. The python heredoc receives CONFIG_PATH and LATEST_TAG via the ENVIRONMENT (env-prefix form — `KEY=VALUE python3 <<PY`), NOT via trailing argv. Passing `python3 -c '...' KEY=VALUE` makes Python treat the assignments as `sys.argv`, which the old draft did incorrectly; env-prefix form is the portable fix.
40
42
 
@@ -31,7 +31,59 @@ Empty → refero: not_configured
31
31
  Non-empty → refero: available
32
32
  ```
33
33
 
34
- Write results to STATE.md `<connections>`.
34
+ **C 21st.dev probe:**
35
+ ```
36
+ ToolSearch({ query: "mcp__21st", max_results: 5 })
37
+ Empty → 21st-dev: not_configured
38
+ Non-empty → 21st-dev: available
39
+ ```
40
+
41
+ **D — Magic Patterns probe:**
42
+ ```
43
+ ToolSearch({ query: "mcp__magic_patterns", max_results: 5 })
44
+ Empty → magic-patterns: not_configured
45
+ Non-empty → magic-patterns: available
46
+ ```
47
+
48
+ **E — paper.design probe:**
49
+ ```
50
+ ToolSearch({ query: "mcp__paper", max_results: 5 })
51
+ Empty → paper-design: not_configured
52
+ Non-empty → call mcp__paper-design__get_selection; success → available; error → unavailable
53
+ ```
54
+
55
+ **F — pencil.dev probe (file-based):**
56
+ ```bash
57
+ find . -name "*.pen" -not -path "*/node_modules/*" 2>/dev/null | head -1
58
+ Empty → pencil-dev: not_configured
59
+ Found → pencil-dev: available
60
+ ```
61
+
62
+ Write all results to STATE.md `<connections>`.
63
+
64
+ ## Step 1.5 — 21st.dev Prior-Art Check (when 21st-dev: available)
65
+
66
+ If `21st-dev: not_configured` in STATE.md: skip this step entirely.
67
+
68
+ When the explore stage identifies any greenfield component in scope (component name from BRIEF.md or user request that does not yet have an implementation file):
69
+
70
+ 1. `21st_magic_component_search(component_name, limit: 3)`
71
+ 2. Evaluate top result:
72
+ - **fit ≥ 80%**: add `<prior-art>` block to DESIGN.md:
73
+ ```xml
74
+ <prior-art source="21st.dev" component="<name>" fit="<score>%" id="<component_id>">
75
+ Recommendation: adopt — do not build custom. Confirm with design-executor.
76
+ </prior-art>
77
+ ```
78
+ - **fit < 80%**: note top candidate in DESIGN.md as a reference, proceed with custom build:
79
+ ```xml
80
+ <prior-art source="21st.dev" component="<name>" fit="<score>%" id="<component_id>">
81
+ Low fit — noted for reference. Building custom component.
82
+ </prior-art>
83
+ ```
84
+ 3. If `svgl_get_brand_logo` is available and explore scope includes brand logo assets: call `svgl_get_brand_logo(brand_name)` for each required brand asset; add SVG results to `.design/assets/` and note in DESIGN.md.
85
+
86
+ If no greenfield components in scope: skip this step.
35
87
 
36
88
  ## Step 2 — Inventory scan (unless `--skip-scan`)
37
89
 
@@ -1,65 +0,0 @@
1
- # Branch Protection — Two-Phase Rollout
2
-
3
- Per D-16 / D-17: branch protection on `main` is rolled out in two phases. The
4
- repo admin applies each phase manually via `scripts/apply-branch-protection.sh`
5
- (no CI automation — avoids leaking repo admin credentials).
6
-
7
- ## Phase A — Advisory (initial state)
8
-
9
- Status checks **run** on every push, but they are **not required to merge**.
10
- This is the default posture while CI stabilizes and baselines are established.
11
-
12
- Apply:
13
-
14
- ```bash
15
- bash scripts/apply-branch-protection.sh --advisory
16
- ```
17
-
18
- Effects:
19
-
20
- - Required status checks: **none** (checks run but don't block)
21
- - `main` accepts direct pushes (the solo maintainer's existing workflow)
22
- - Force-push: allowed by admins
23
-
24
- ## Phase B — Enforcing (after first clean release)
25
-
26
- After the first full release cycle ships clean (plan 13-06 / 13-07 smoke test
27
- passes on a real tag + GitHub Release), tighten to enforcing.
28
-
29
- Apply:
30
-
31
- ```bash
32
- bash scripts/apply-branch-protection.sh --enforcing
33
- ```
34
-
35
- Effects:
36
-
37
- - Required status checks (all must pass to merge):
38
- - `Lint (markdown + frontmatter + stale-refs)`
39
- - `Validate (schemas + plugin + shellcheck)`
40
- - `Test (Node 22 / ubuntu-latest)`
41
- - `Test (Node 22 / macos-latest)`
42
- - `Test (Node 22 / windows-latest)`
43
- - `Test (Node 24 / ubuntu-latest)`
44
- - `Test (Node 24 / macos-latest)`
45
- - `Test (Node 24 / windows-latest)`
46
- - `Security (secrets + injection scan)`
47
- - `Size budget (blocking)`
48
- - Require linear history (no merge commits)
49
- - Disallow force-push
50
- - Admins still bypass for emergency fixes (logged)
51
-
52
- ## When to promote Phase A → Phase B
53
-
54
- - [ ] Wave A + B of Phase 13 merged to main
55
- - [ ] `release.yml` (plan 13-06) merged and triggered at least once successfully
56
- - [ ] Release smoke test (plan 13-07) passed on a real tag
57
- - [ ] Baseline lock (plan 13-08) committed
58
-
59
- When all four are true, run `apply-branch-protection.sh --enforcing`.
60
-
61
- ## Rollback
62
-
63
- To revert either phase: `apply-branch-protection.sh --disable` removes all
64
- protection rules. Use only if a protection misconfiguration is blocking a
65
- legitimate merge.
@@ -1,75 +0,0 @@
1
- #!/usr/bin/env bash
2
- # apply-branch-protection.sh — manually apply branch protection to `main`.
3
- # Per D-16: this script is run by the repo admin locally, NOT from CI.
4
- # Usage:
5
- # bash scripts/apply-branch-protection.sh --advisory
6
- # bash scripts/apply-branch-protection.sh --enforcing
7
- # bash scripts/apply-branch-protection.sh --disable
8
-
9
- set -euo pipefail
10
-
11
- MODE="${1:-}"
12
- REPO="${GITHUB_REPOSITORY:-hegemonart/get-design-done}"
13
-
14
- if ! command -v gh >/dev/null 2>&1; then
15
- echo "ERROR: gh CLI not found. Install from https://cli.github.com/"
16
- exit 1
17
- fi
18
-
19
- case "$MODE" in
20
- --advisory)
21
- echo "Applying ADVISORY branch protection to $REPO main..."
22
- gh api -X PUT "repos/${REPO}/branches/main/protection" \
23
- -H "Accept: application/vnd.github+json" \
24
- -f "required_status_checks=null" \
25
- -F "enforce_admins=false" \
26
- -f "required_pull_request_reviews=null" \
27
- -F "restrictions=null" \
28
- -F "required_linear_history=false" \
29
- -F "allow_force_pushes=true" \
30
- -F "allow_deletions=false"
31
- echo "Advisory mode applied. CI checks will run but not block merges."
32
- ;;
33
- --enforcing)
34
- echo "Applying ENFORCING branch protection to $REPO main..."
35
- # Status check names must match the `name:` field of each job exactly.
36
- # See reference/BRANCH-PROTECTION.md §Phase B for the authoritative list.
37
- gh api -X PUT "repos/${REPO}/branches/main/protection" \
38
- -H "Accept: application/vnd.github+json" \
39
- --input - <<'JSON'
40
- {
41
- "required_status_checks": {
42
- "strict": true,
43
- "contexts": [
44
- "Lint (markdown + frontmatter + stale-refs)",
45
- "Validate (schemas + plugin + shellcheck)",
46
- "Test (Node 22 / ubuntu-latest)",
47
- "Test (Node 22 / macos-latest)",
48
- "Test (Node 22 / windows-latest)",
49
- "Test (Node 24 / ubuntu-latest)",
50
- "Test (Node 24 / macos-latest)",
51
- "Test (Node 24 / windows-latest)",
52
- "Security (secrets + injection scan)",
53
- "Size budget (blocking)"
54
- ]
55
- },
56
- "enforce_admins": false,
57
- "required_pull_request_reviews": null,
58
- "restrictions": null,
59
- "required_linear_history": true,
60
- "allow_force_pushes": false,
61
- "allow_deletions": false
62
- }
63
- JSON
64
- echo "Enforcing mode applied. CI must pass before merge; linear history required."
65
- ;;
66
- --disable)
67
- echo "Removing branch protection from $REPO main..."
68
- gh api -X DELETE "repos/${REPO}/branches/main/protection" || true
69
- echo "Protection removed."
70
- ;;
71
- *)
72
- echo "Usage: $0 --advisory | --enforcing | --disable"
73
- exit 1
74
- ;;
75
- esac