diff --git a/README.md b/README.md
index c43ea16..280d217 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
English | [简体中文](https://github.com/LE0-Lin/AgentConfigScore/blob/main/README.simplified.md) | [繁體中文](https://github.com/LE0-Lin/AgentConfigScore/blob/main/README.traditional.md)
-
+
Catch known regressions in coding-agent instructions before merging.
@@ -23,11 +23,15 @@ reports rule-defined regressions, and uses reviewed baseline policies for CI
decisions. No model API or runtime dependencies are needed.
-
+
[Setup demo](#quick-start) · [Workflow diagram](#what-it-helps-you-review) · [Score history demo](#score-history-demo) · [Visual gallery](https://github.com/LE0-Lin/AgentConfigScore/blob/main/docs/visual-tour.md)
+The demo replays a change drawn from a merged PR. An instruction file moves to
+the root with identical text. Both scans score 100, and the comparison flags
+the original path for review. [Reproduce the reduced case](https://github.com/LE0-Lin/AgentConfigScore/blob/main/examples/instruction-move/README.md).
+
**A 100 means no active deterministic rule matched.** It does not certify
instruction usefulness, semantic correctness or safety.
[Read the score contract and limitations.](https://github.com/LE0-Lin/AgentConfigScore/blob/main/docs/limitations.md)
@@ -45,7 +49,7 @@ Existing repositories can adopt the gate without first reaching 100.
`init` enables new-error blocking and no score decrease; a passing comparison
means no regression under the selected rules and policy.
-
+
## Quick start
@@ -69,9 +73,9 @@ without fetching; use `agent-config-score diff origin/main` when you want an
explicit ref. The [user guide](https://github.com/LE0-Lin/AgentConfigScore/blob/main/docs/user-guide.md)
covers setup, exits and troubleshooting.
-
+
-*The v0.18 demo illustrates the install, initialize, and verify steps. Use the commands above for the current setup.*
+*The v0.24.0 demo shows setup and selected checks from `doctor` in a local fixture.*
## Release and source status
@@ -191,9 +195,9 @@ and [privacy-minimized feedback drafts](https://github.com/LE0-Lin/AgentConfigSc
## Score history demo
-
+
-This example shows how recorded scores and commit IDs appear together in
+The chart shows three locally recorded fixture scores read by
`agent-config-score history`. [Set up the history workflow.](https://github.com/LE0-Lin/AgentConfigScore/blob/main/docs/score-history.md)
The [visual tour](https://github.com/LE0-Lin/AgentConfigScore/blob/main/docs/visual-tour.md)
diff --git a/README.simplified.md b/README.simplified.md
index a1357f7..2f89852 100644
--- a/README.simplified.md
+++ b/README.simplified.md
@@ -4,10 +4,12 @@
在合并前发现 AI 编程助手指令中的已知退化。
-
+
[安装演示](#快速开始) · [分数历史演示](#分数历史演示) · [完整演示图集](docs/visual-tour.md)
+演示基于一条已合并的 PR。指令文件原样移到仓库根目录,前后都得 100 分,比较结果仍指出需要复核的旧路径。[复跑精简案例](examples/instruction-move/README.md)。
+
AgentConfigScore 检查 `AGENTS.md`、`CLAUDE.md`、Cursor、Copilot、Gemini 等配置,并比较改动前后的基线,回答一个具体问题:**这次改动是否引入了已知规则能检测到的风险?**
| 改动 | 审查依据 |
@@ -17,7 +19,7 @@ AgentConfigScore 检查 `AGENTS.md`、`CLAUDE.md`、Cursor、Copilot、Gemini
| PR 放宽自己的阈值或增加豁免 | 正常比较仍使用基线策略;调用方的显式覆盖需单独审核 |
| 接受已知例外 | 必须填写原因和到期日期,并保留审计记录 |
-
+
它是确定性检查器,不是 AI 评委。**A 100 只表示没有命中生效的规则,不表示指令有用、语义完美或安全无误。** 中文文档也不意味着检测器已经能够理解任意中文指令。
@@ -37,9 +39,9 @@ agent-config-score diff
`acs` 是短命令别名。v0.23.0+ 还可以使用 `python -m agent_config_score`,避免终端 PATH 没有找到命令的问题。
-
+
-*动图以 v0.18 为例展示接入步骤。当前版本请按上方命令安装。*
+*动图使用 v0.24.0 展示本地样例的接入步骤和部分 `doctor` 检查结果。*
- [简体中文使用指南](docs/simplified/user-guide.md):安装、基线、报告、退出码与排错。
- [分数含义与限制](docs/simplified/limitations.md):先了解什么能检出、什么仍可能漏掉。
@@ -155,8 +157,8 @@ jobs:
## 分数历史演示
-
+
-示例展示 `agent-config-score history` 如何把每次评分和对应提交放在一起。[历史工作流(英文)](docs/score-history.md)
+图中的三次评分由 `agent-config-score history` 读取。[历史工作流(英文)](docs/score-history.md)
项目采用 [MIT 协议](LICENSE)。[发布与升级说明](docs/releases/v0.24.0.md)目前为英文。
diff --git a/README.traditional.md b/README.traditional.md
index 7e05bd0..1f12e4c 100644
--- a/README.traditional.md
+++ b/README.traditional.md
@@ -4,10 +4,12 @@
在合併前發現 AI 程式開發助手指令中的已知退化。
-
+
[安裝示範](#快速開始) · [分數歷史示範](#分數歷史示範) · [完整示範圖集](docs/visual-tour.md)
+示範取材自一條已合併的 PR。指令檔原樣移至儲存庫根目錄,前後都得 100 分,比較結果仍指出需要審閱的舊路徑。[重現精簡範例](examples/instruction-move/README.md)。
+
AgentConfigScore 檢查 `AGENTS.md`、`CLAUDE.md`、Cursor、Copilot、Gemini 等設定,並與改動前的基準比較,回答一個具體問題:**這次改動是否引入了已知規則能偵測到的風險?**
| 改動 | 審閱依據 |
@@ -17,7 +19,7 @@ AgentConfigScore 檢查 `AGENTS.md`、`CLAUDE.md`、Cursor、Copilot、Gemini
| PR 放寬自己的門檻或增加排除設定 | 一般比較仍使用基準策略;呼叫端的明確覆寫須另行審閱 |
| 接受已知例外 | 必須填寫原因和到期日期,並保留稽核紀錄 |
-
+
它是確定性的檢查工具,不是 AI 評審。**A 100 只表示沒有命中生效的規則,不代表指令實用、語意完美或完全安全。** 提供中文文件也不表示檢查工具已經能理解任意中文指令。
@@ -37,9 +39,9 @@ agent-config-score diff
`acs` 是較短的命令別名。v0.23.0+ 也能使用 `python -m agent_config_score`,不必依賴終端機 PATH 找到主命令。
-
+
-*動畫以 v0.18 為例展示整合步驟。目前版本請依照上方命令安裝。*
+*動畫使用 v0.24.0 展示本機範例的整合步驟及部分 `doctor` 檢查結果。*
- [繁體中文使用指南](docs/traditional/user-guide.md):安裝、基準、報告、結束代碼與疑難排解。
- [分數意義與限制](docs/traditional/limitations.md):先了解能偵測哪些問題,以及可能遺漏什麼。
@@ -155,8 +157,8 @@ jobs:
## 分數歷史示範
-
+
-範例展示 `agent-config-score history` 如何將每次評分與對應提交放在一起。[歷史工作流程(英文)](docs/score-history.md)
+圖中的三次評分由 `agent-config-score history` 讀取。[歷史工作流程(英文)](docs/score-history.md)
專案採用 [MIT 授權](LICENSE)。[發布與升級說明](docs/releases/v0.24.0.md)目前為英文。
diff --git a/assets/agent-config-score-demo.gif b/assets/agent-config-score-demo.gif
index b51b68d..2fd0dda 100644
Binary files a/assets/agent-config-score-demo.gif and b/assets/agent-config-score-demo.gif differ
diff --git a/assets/agent-config-score-history.gif b/assets/agent-config-score-history.gif
index 8d6c974..f64d63a 100644
Binary files a/assets/agent-config-score-history.gif and b/assets/agent-config-score-history.gif differ
diff --git a/assets/agent-config-score-setup.gif b/assets/agent-config-score-setup.gif
index 40ec321..e3f1fc2 100644
Binary files a/assets/agent-config-score-setup.gif and b/assets/agent-config-score-setup.gif differ
diff --git a/assets/agent-config-score-social-preview.png b/assets/agent-config-score-social-preview.png
index aebd23f..f0e6b9a 100644
Binary files a/assets/agent-config-score-social-preview.png and b/assets/agent-config-score-social-preview.png differ
diff --git a/assets/agent-config-score-workflow.svg b/assets/agent-config-score-workflow.svg
index 11727cd..5cb62f3 100644
--- a/assets/agent-config-score-workflow.svg
+++ b/assets/agent-config-score-workflow.svg
@@ -1,54 +1,36 @@
- AgentConfigScore workflow
- Configure agent instructions, guard every pull request against a baseline, and retain an auditable score history.
-
-
-
-
-
-
-
-
-
-
-
-
-
- One small policy loop. Three durable outcomes.
- From local instructions to merge protection and reviewable evidence.
-
-
-
-
-
+ Review workflow
+ Use the reviewed baseline policy to check a proposed change and keep evidence about the decision.
+
+
+ AGENTCONFIGSCORE / WORKFLOW
+ Review changes against the baseline.
+
+
+
+
+
+
+
+ 01 / BASELINE
+ 02 / CHANGE
+ 03 / EVIDENCE
+
+
+ Set the policy
+ Compare the PR
+ Review the finding
+
+
+ Commit thresholds and reviewed exceptions.
+ Apply the baseline policy to the proposed change.
+ Inspect rule IDs, source paths, and the CI exit status.
+
+
+ agent-config-score init
+ agent-config-score diff
+ JSON · HTML · SARIF
+
+ Local analysis / Reviewed baseline policy / Deterministic findings
-
-
- 1
- Configure once
- agent-config-score init
- Policy + GitHub workflow
- AGENTS.md · CLAUDE.md
- Cursor · Copilot · Gemini
-
-
- 2
- Guard every PR
-
- baseline → candidate
- ✓ no regression
- ✕ unsafe change blocked
-
-
- 3
- Keep evidence
-
-
-
-
-
- JSON · HTML · badge · history
-
-
-
diff --git a/assets/agent-config-score.svg b/assets/agent-config-score.svg
index 2ed3986..4308c07 100644
--- a/assets/agent-config-score.svg
+++ b/assets/agent-config-score.svg
@@ -1,9 +1,21 @@
-
-
-
-
-
- agent config agent config
- A 100 A 100
+
+ AgentConfigScore
+ Local regression review for coding-agent instructions
+
+
+
+
+
+
+
+ CODING AGENT INSTRUCTION REVIEW
+ AgentConfigScore
+ Baseline policies, actionable findings, and local analysis.
+
+
+
+
+ BASELINE
+ CANDIDATE
diff --git a/docs/reference.md b/docs/reference.md
index d7439c1..2c28f0b 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -193,7 +193,7 @@ Without a policy file, the Action preserves its conservative compatibility defau
Optionally record a read-only score snapshot whenever your default branch changes:
-
+
```yaml
diff --git a/docs/visual-tour.md b/docs/visual-tour.md
index 0a71ecd..756c1d6 100644
--- a/docs/visual-tour.md
+++ b/docs/visual-tour.md
@@ -1,6 +1,6 @@
# Visual tour
-AgentConfigScore turns deterministic coding-agent instruction risks into something a team can see, review, and enforce. It does not certify semantic prompt quality; see the [score contract](limitations.md). These demos use representative CLI output and map directly to shipped commands.
+The gallery visualizes findings, setup checks, and recorded scores from v0.24.0 on local fixtures. The animations condense the CLI results into review cards. [Read the score contract](limitations.md).
## Shareable project card
@@ -10,11 +10,11 @@ The solid-background 1280×640 project card is optimized for GitHub link preview
## Catch a regression before merge
-
+
-`agent-config-score diff` compares the candidate with a safe baseline. A new error or disallowed score drop blocks the pull request without letting the candidate weaken its own policy.
+This `compare` run checks a byte-identical instruction moved across directories. The baseline policy gates the new finding, so the run returns exit 1 even though the scores match. [Reproduce the anonymized fixture](../examples/instruction-move/README.md).
-## Protect a repository in three commands
+## Set up a repository

@@ -22,9 +22,9 @@ The normal setup path is intentionally small: install from PyPI, run `init`, the
## Make known-risk changes visible over time
-
+
-Local history provides a human-readable trend. The reusable score-history workflow can also preserve immutable JSON, HTML, and badge artifacts for default-branch commits.
+The chart shows the stored scores for baseline, risk added, and risk resolved. The reusable score-history workflow can preserve JSON, HTML, and badge artifacts for default-branch commits.
## How the pieces fit
@@ -34,10 +34,10 @@ The regression gate answers whether a pull request introduced a detected regress
## Regenerate the gallery
-The GIFs and social preview are generated from a source-controlled Pillow renderer rather than recorded from a particular operating system or shell:
+Run the renderer in a Python 3.12 environment with Git available. It reads the installed release's CLI results, including the regression exit and selected `doctor` checks from a disposable Git repository. Font selection depends on the rendering host.
```bash
-python -m pip install pillow
+python -m pip install agent-config-score==0.24.0 pillow==12.3.0
python scripts/render_demo_gif.py --all
```
diff --git a/examples/instruction-move/README.md b/examples/instruction-move/README.md
new file mode 100644
index 0000000..d31f995
--- /dev/null
+++ b/examples/instruction-move/README.md
@@ -0,0 +1,27 @@
+# An instruction moves to a new scope
+
+A merged public PR moved `docs/AGENTS.md` to the repository root with an identical Git blob. Replaying its full before and after trees with v0.24.0 produced 100 on both sides. With new errors gated, the comparison reported `instruction-file-removed` for the former path and exited 1. The PR described the move as intentional so agents could discover the file at the root. The finding identifies the original path to review when approving the migration.
+
+This reduced example uses original sample wording and adds a repository policy so the result can be reproduced offline from a checkout. Its input captures the same path change and preserves identical instruction bytes. Project and contributor identities are omitted.
+
+## Reproduce
+
+From the AgentConfigScore checkout, install the published version in a virtual environment and run:
+
+```bash
+python -m pip install agent-config-score==0.24.0
+cd tests/fixtures/contracts/regression/instruction-move
+agent-config-score compare base head
+agent-config-score compare base base
+agent-config-score compare head head
+```
+
+| Comparison | Baseline → candidate | New rule | Exit |
+| --- | --- | --- | --- |
+| Original scope → root | 100 → 100 | `instruction-file-removed` at `docs/AGENTS.md` | 1 |
+| Original scope → itself | 100 → 100 | None | 0 |
+| Root → itself | 100 → 100 | None | 0 |
+
+The first exit is expected. Run the comparisons separately if your shell stops on an error. Add `--json` to inspect the scores, stable rule ID, and former file path.
+
+The same-directory rename heuristic deliberately requires a matching directory. This cross-directory move merits review even with byte-identical text. The decision to accept the migration depends on the client's loading rules and the intended scope.
diff --git a/scripts/render_demo_gif.py b/scripts/render_demo_gif.py
index 33f40e2..ebfeb52 100644
--- a/scripts/render_demo_gif.py
+++ b/scripts/render_demo_gif.py
@@ -1,423 +1,232 @@
#!/usr/bin/env python3
-"""Render the README terminal demo as a deterministic animated GIF.
-
-This is a maintainer tool, not a runtime dependency. Install Pillow before
-running it: ``python -m pip install pillow``.
-"""
+"""Render the gallery from the installed release and local fixtures."""
from __future__ import annotations
import argparse
+import json
from pathlib import Path
+import shutil
+import subprocess
+import sys
+from tempfile import TemporaryDirectory
from PIL import Image, ImageDraw, ImageFont
-
ROOT = Path(__file__).resolve().parents[1]
-DEFAULT_OUTPUT = ROOT / "assets" / "agent-config-score-demo.gif"
-SETUP_OUTPUT = ROOT / "assets" / "agent-config-score-setup.gif"
-HISTORY_OUTPUT = ROOT / "assets" / "agent-config-score-history.gif"
-SOCIAL_OUTPUT = ROOT / "assets" / "agent-config-score-social-preview.png"
-WIDTH = 1200
-HEIGHT = 675
-
-COLORS = {
- "page": "#07111F",
- "glow": "#102A43",
- "window": "#0D1626",
- "chrome": "#162235",
- "border": "#2C3E55",
- "text": "#E6EDF3",
- "muted": "#8FA3B8",
- "green": "#7EE787",
- "cyan": "#79C0FF",
- "red": "#FF7B72",
- "amber": "#E3B341",
-}
-
-
-def _font(size: int, *, bold: bool = False, mono: bool = False) -> ImageFont.FreeTypeFont:
+ASSETS = ROOT / "assets"
+FIXTURES = ROOT / "tests" / "fixtures" / "contracts" / "regression"
+MOVE = FIXTURES / "instruction-move"
+SHELL = FIXTURES / "unsafe-shell"
+RELEASE = "0.24.0"
+WIDTH, HEIGHT = 1200, 675
+INK = "#0B1422"
+PANEL = "#111E2D"
+EDGE = "#304457"
+WHITE = "#ECF3F8"
+SOFT = "#A9BED0"
+MINT = "#76E5C1"
+CORAL = "#FFAD9D"
+
+
+def font(size: int, *, bold: bool = False, mono: bool = False):
windows = Path("C:/Windows/Fonts")
- candidates = (
- [windows / ("consolab.ttf" if bold else "consola.ttf")]
- if mono
- else [windows / ("segoeuib.ttf" if bold else "segoeui.ttf")]
- )
- candidates.extend(
- [
- Path("/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf" if mono and bold else
- "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf" if mono else
- "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" if bold else
- "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"),
- Path("/System/Library/Fonts/SFNSMono.ttf" if mono else "/System/Library/Fonts/SFNS.ttf"),
- ]
+ win = ("consolab.ttf" if bold else "consola.ttf") if mono else ("segoeuib.ttf" if bold else "segoeui.ttf")
+ linux = "DejaVuSansMono" if mono else "DejaVuSans"
+ paths = (
+ windows / win,
+ Path("/usr/share/fonts/truetype/dejavu") / (linux + ("-Bold.ttf" if bold else ".ttf")),
+ Path("/System/Library/Fonts/SFNSMono.ttf" if mono else "/System/Library/Fonts/SFNS.ttf"),
)
- for candidate in candidates:
- if candidate.exists():
- return ImageFont.truetype(str(candidate), size=size)
+ for path in paths:
+ if path.is_file():
+ return ImageFont.truetype(str(path), size)
return ImageFont.load_default(size=size)
-TITLE = _font(34, bold=True)
-SUBTITLE = _font(18)
-BADGE = _font(14, bold=True)
-MONO = _font(22, mono=True)
-MONO_BOLD = _font(22, mono=True, bold=True)
-MONO_SMALL = _font(19, mono=True)
-FOOTER = _font(17)
-SOCIAL_TITLE = _font(58, bold=True)
-SOCIAL_TAGLINE = _font(29, bold=True)
-SOCIAL_SCORE = _font(72, bold=True)
-SOCIAL_SCORE_LABEL = _font(20, bold=True)
-
-
-OUTPUT_LINES = [
- [("AgentConfigScore regression ", "text"), ("A 100", "green"), (" → ", "muted"), ("B 82 (-18)", "amber")],
- [("New findings: 2", "text"), (" Resolved: 0 Suppressed: 0", "muted")],
- [],
- [("+ ERROR ", "red"), ("curl-pipe-shell", "text"), (" Remote script piped directly to a shell", "muted")],
- [(" .github/copilot-instructions.md:12", "muted")],
- [("+ WARNING ", "amber"), ("dead-path", "text"), (" Referenced path does not exist: src/legacy_auth.py", "muted")],
- [(" AGENTS.md:31", "muted")],
- [],
- [("POLICY RESULT: BLOCKED", "red")],
-]
-
-
-def _text_segments(draw: ImageDraw.ImageDraw, x: int, y: int, segments: list[tuple[str, str]]) -> None:
- cursor = x
- for text, color in segments:
- font = MONO_BOLD if color in {"red", "amber", "green"} else MONO
- draw.text((cursor, y), text, fill=COLORS[color], font=font)
- cursor += int(draw.textlength(text, font=font))
-
-
-def _frame(command_chars: int, visible_lines: int, *, cursor: bool = True) -> Image.Image:
- image = Image.new("RGB", (WIDTH, HEIGHT), COLORS["page"])
- draw = ImageDraw.Draw(image)
+TITLE = font(44, bold=True)
+HEADING = font(30, bold=True)
+TEXT = font(23)
+LABEL = font(17, bold=True)
+MONO = font(23, mono=True)
+SCORE = font(52, bold=True)
- draw.ellipse((740, -250, 1320, 330), fill=COLORS["glow"])
- draw.text((62, 35), "AgentConfigScore", fill=COLORS["text"], font=TITLE)
- draw.text((390, 49), "PR regression gate for coding-agent instructions", fill=COLORS["muted"], font=SUBTITLE)
-
- badge = "DETERMINISTIC • ZERO RUNTIME DEPS"
- badge_width = int(draw.textlength(badge, font=BADGE)) + 30
- draw.rounded_rectangle((WIDTH - badge_width - 60, 38, WIDTH - 60, 72), radius=17, fill="#123B32", outline="#286B57")
- draw.text((WIDTH - badge_width - 45, 47), badge, fill=COLORS["green"], font=BADGE)
-
- left, top, right, bottom = 55, 98, WIDTH - 55, 608
- draw.rounded_rectangle((left, top, right, bottom), radius=16, fill=COLORS["window"], outline=COLORS["border"], width=2)
- draw.rounded_rectangle((left, top, right, top + 52), radius=16, fill=COLORS["chrome"])
- draw.rectangle((left, top + 36, right, top + 52), fill=COLORS["chrome"])
- for index, color in enumerate(("#FF5F57", "#FEBC2E", "#28C840")):
- x = left + 25 + index * 26
- draw.ellipse((x, top + 19, x + 13, top + 32), fill=color)
- draw.text((left + 475, top + 16), "pull request check", fill=COLORS["muted"], font=SUBTITLE)
-
- command = "agent-config-score diff origin/main"
- prompt_x, prompt_y = left + 32, top + 79
- draw.text((prompt_x, prompt_y), "$", fill=COLORS["green"], font=MONO_BOLD)
- shown = command[:command_chars]
- draw.text((prompt_x + 27, prompt_y), shown, fill=COLORS["text"], font=MONO)
- if cursor:
- cursor_x = prompt_x + 27 + int(draw.textlength(shown, font=MONO)) + 2
- draw.rectangle((cursor_x, prompt_y + 4, cursor_x + 11, prompt_y + 28), fill=COLORS["cyan"])
-
- output_y = prompt_y + 54
- for index, line in enumerate(OUTPUT_LINES[:visible_lines]):
- _text_segments(draw, prompt_x, output_y + index * 34, line)
-
- if visible_lines == len(OUTPUT_LINES):
- draw.rounded_rectangle((left + 27, bottom - 58, right - 27, bottom - 18), radius=8, fill="#291B20", outline="#71333A")
- draw.text((left + 47, bottom - 48), "BASELINE POLICY STAYED IN CONTROL — THIS REGRESSION CANNOT MERGE", fill="#FFB4AC", font=FOOTER)
-
- draw.text((60, 632), "A pull request can change the instructions. It cannot weaken the policy judging itself.", fill=COLORS["muted"], font=FOOTER)
- draw.text((1055, 632), "v0.18", fill=COLORS["cyan"], font=FOOTER)
- return image
-
-
-def render(output: Path) -> None:
- frames: list[Image.Image] = []
- durations: list[int] = []
-
- frames.append(_frame(0, 0))
- durations.append(650)
- command_length = len("agent-config-score diff origin/main")
- for count in range(1, command_length + 1, 2):
- frames.append(_frame(min(count, command_length), 0))
- durations.append(65)
- frames.append(_frame(command_length, 0, cursor=False))
- durations.append(550)
-
- for visible in range(1, len(OUTPUT_LINES) + 1):
- frames.append(_frame(command_length, visible, cursor=False))
- durations.append(260 if OUTPUT_LINES[visible - 1] else 120)
- frames.append(_frame(command_length, len(OUTPUT_LINES), cursor=False))
- durations.append(3600)
- output.parent.mkdir(parents=True, exist_ok=True)
- palette_frames = [frame.quantize(colors=128, method=Image.Quantize.MEDIANCUT) for frame in frames]
- palette_frames[0].save(
- output,
- save_all=True,
- append_images=palette_frames[1:],
- duration=durations,
- loop=0,
- disposal=2,
- optimize=True,
+def cli(*args: str, cwd: Path, expected: int = 0) -> str:
+ result = subprocess.run(
+ [sys.executable, "-m", "agent_config_score", *args],
+ cwd=cwd,
+ capture_output=True,
+ encoding="utf-8",
+ errors="replace",
+ timeout=20,
)
+ if result.returncode != expected:
+ raise RuntimeError(f"Expected exit {expected} for {args}, got {result.returncode}: {result.stderr.strip()}")
+ return result.stdout
+
+
+def comparison() -> dict:
+ report = json.loads(cli("compare", "base", "head", "--json", cwd=MOVE, expected=1))
+ if [item["code"] for item in report["new_findings"]] != ["instruction-file-removed"]:
+ raise RuntimeError("Instruction-move demo output changed. Review the gallery before regenerating it.")
+ return report
+
+
+def setup_checks() -> list[tuple[str, str]]:
+ with TemporaryDirectory(prefix="acs-gallery-") as directory:
+ root = Path(directory)
+ shutil.copyfile(SHELL / "base" / "AGENTS.md", root / "AGENTS.md")
+ subprocess.run(["git", "init", "-q", "--template=", "-b", "main"], cwd=root, check=True)
+ cli("init", cwd=root)
+ subprocess.run(["git", "add", "."], cwd=root, check=True)
+ subprocess.run(
+ ["git", "-c", "user.name=Gallery Fixture", "-c", "user.email=gallery@example.invalid",
+ "-c", "core.hooksPath=.git/no-hooks", "-c", "commit.gpgsign=false",
+ "commit", "-qm", "Initialize demo repository"],
+ cwd=root, check=True,
+ )
+ subprocess.run(["git", "update-ref", "refs/remotes/origin/main", "HEAD"], cwd=root, check=True)
+ report = json.loads(cli("doctor", "--json", cwd=root))
+ selected = {"config", "instructions", "baseline", "workflow"}
+ checks = [(item["name"], item["status"]) for item in report["checks"] if item["name"] in selected]
+ if len(checks) != len(selected) or any(status != "pass" for _, status in checks):
+ raise RuntimeError(f"Unexpected doctor results: {checks}")
+ return checks
-SETUP_LINES = [
- [("$ ", "green"), ("python -m pip install agent-config-score", "text")],
- [(" Installed agent-config-score 0.18.0", "muted")],
- [],
- [("$ ", "green"), ("agent-config-score init", "text")],
- [(" CREATED ", "cyan"), (".agentconfigscore.json", "text")],
- [(" CREATED ", "cyan"), (".github/workflows/agent-config-score.yml", "text")],
- [],
- [("$ ", "green"), ("agent-config-score doctor", "text")],
- [(" PASS ", "green"), ("config", "text"), (" Policy and schema are valid", "muted")],
- [(" PASS ", "green"), ("instructions", "text"), (" 4 supported files found", "muted")],
- [(" PASS ", "green"), ("baseline", "text"), (" Automatic diff baseline: origin/main", "muted")],
- [(" PASS ", "green"), ("workflow", "text"), (" Pull-request gate is installed", "muted")],
-]
-
-
-def _shell_frame(title: str, subtitle: str, badge: str) -> tuple[Image.Image, ImageDraw.ImageDraw, tuple[int, int, int, int]]:
- image = Image.new("RGB", (WIDTH, HEIGHT), COLORS["page"])
- draw = ImageDraw.Draw(image)
- draw.ellipse((740, -250, 1320, 330), fill=COLORS["glow"])
- draw.text((62, 35), title, fill=COLORS["text"], font=TITLE)
- draw.text((390, 49), subtitle, fill=COLORS["muted"], font=SUBTITLE)
-
- badge_width = int(draw.textlength(badge, font=BADGE)) + 30
- draw.rounded_rectangle(
- (WIDTH - badge_width - 60, 38, WIDTH - 60, 72),
- radius=17,
- fill="#123B32",
- outline="#286B57",
- )
- draw.text((WIDTH - badge_width - 45, 47), badge, fill=COLORS["green"], font=BADGE)
-
- window = (55, 98, WIDTH - 55, 608)
- left, top, right, bottom = window
- draw.rounded_rectangle(window, radius=16, fill=COLORS["window"], outline=COLORS["border"], width=2)
- draw.rounded_rectangle((left, top, right, top + 52), radius=16, fill=COLORS["chrome"])
- draw.rectangle((left, top + 36, right, top + 52), fill=COLORS["chrome"])
- for index, color in enumerate(("#FF5F57", "#FEBC2E", "#28C840")):
- x = left + 25 + index * 26
- draw.ellipse((x, top + 19, x + 13, top + 32), fill=color)
- draw.text((left + 475, top + 16), "terminal", fill=COLORS["muted"], font=SUBTITLE)
- return image, draw, window
-
-
-def _setup_frame(visible_lines: int) -> Image.Image:
- image, draw, window = _shell_frame(
- "AgentConfigScore",
- "from install to protected pull requests",
- "30-SECOND SETUP",
- )
- left, top, right, bottom = window
- start_y = top + 76
- for index, line in enumerate(SETUP_LINES[:visible_lines]):
- _text_segments(draw, left + 32, start_y + index * 32, line)
-
- if visible_lines == len(SETUP_LINES):
- draw.rounded_rectangle((left + 27, bottom - 54, right - 27, bottom - 14), radius=8, fill="#123B32", outline="#286B57")
- draw.text((left + 47, bottom - 44), "READY — EVERY PULL REQUEST IS NOW PROTECTED", fill=COLORS["green"], font=FOOTER)
+def history_scores() -> list[int]:
+ from agent_config_score.history import append_snapshot
- draw.text((60, 632), "Install. Initialize. Verify. No service account and no runtime dependencies.", fill=COLORS["muted"], font=FOOTER)
- draw.text((1055, 632), "v0.18", fill=COLORS["cyan"], font=FOOTER)
- return image
+ with TemporaryDirectory(prefix="acs-history-gallery-") as directory:
+ root = Path(directory)
+ for side in ("base", "head", "base"):
+ report = json.loads(cli(str(SHELL / side), "--json", cwd=root))
+ append_snapshot(root, {"score": report["score"], "grade": report["grade"]})
+ recorded = json.loads(cli("history", "--json", cwd=root))
+ return [row["score"] for row in recorded["history"]]
-def render_setup(output: Path) -> None:
- frames = [_setup_frame(0)]
- durations = [600]
- for visible in range(1, len(SETUP_LINES) + 1):
- frames.append(_setup_frame(visible))
- durations.append(220 if SETUP_LINES[visible - 1] else 120)
- frames.append(_setup_frame(len(SETUP_LINES)))
- durations.append(3200)
- _save_gif(frames, durations, output)
-
-
-HISTORY_ROWS = [
- ("2026-08-28", 82, "B", "-", "8f13c42"),
- ("2026-08-29", 88, "B", "+6", "2b94a10"),
- ("2026-08-30", 94, "A", "+6", "a73e5cf"),
- ("2026-09-01", 100, "A", "+6", "5b283e5"),
-]
-
-
-def _history_frame(visible_rows: int) -> Image.Image:
- image = Image.new("RGB", (WIDTH, HEIGHT), COLORS["page"])
+def canvas(section: str, headline: str, detail: str) -> tuple[Image.Image, ImageDraw.ImageDraw]:
+ image = Image.new("RGB", (WIDTH, HEIGHT), INK)
draw = ImageDraw.Draw(image)
- draw.ellipse((720, -260, 1320, 340), fill=COLORS["glow"])
- draw.text((62, 35), "AgentConfigScore", fill=COLORS["text"], font=TITLE)
- draw.text((390, 49), "instruction quality, visible over time", fill=COLORS["muted"], font=SUBTITLE)
-
- badge = "AUDITABLE HISTORY"
- badge_width = int(draw.textlength(badge, font=BADGE)) + 30
- draw.rounded_rectangle((WIDTH - badge_width - 60, 38, WIDTH - 60, 72), radius=17, fill="#17344F", outline="#2B6190")
- draw.text((WIDTH - badge_width - 45, 47), badge, fill=COLORS["cyan"], font=BADGE)
-
- left, top, right, bottom = 55, 98, WIDTH - 55, 608
- draw.rounded_rectangle((left, top, right, bottom), radius=16, fill=COLORS["window"], outline=COLORS["border"], width=2)
- split = 685
- draw.line((split, top + 28, split, bottom - 28), fill=COLORS["border"], width=2)
-
- draw.text((left + 30, top + 27), "$ agent-config-score history", fill=COLORS["text"], font=MONO)
- draw.text((left + 30, top + 83), "DATE SCORE GRADE CHANGE COMMIT", fill=COLORS["muted"], font=BADGE)
- for index, (date, score, grade, change, commit) in enumerate(HISTORY_ROWS[:visible_rows]):
- y = top + 126 + index * 58
- score_color = COLORS["green"] if grade == "A" else COLORS["amber"]
- draw.text((left + 30, y), date, fill=COLORS["text"], font=MONO_SMALL)
- draw.text((left + 245, y), f"{score:>3}", fill=score_color, font=MONO_BOLD)
- draw.text((left + 340, y), grade, fill=score_color, font=MONO_BOLD)
- draw.text((left + 410, y), f"{change:>3}", fill=COLORS["green"] if change != "-" else COLORS["muted"], font=MONO_SMALL)
- draw.line((left + 485, y - 3, left + 485, y + 25), fill=COLORS["border"], width=1)
- draw.text((left + 505, y), commit, fill=COLORS["muted"], font=MONO_SMALL)
-
- chart_left, chart_top, chart_right, chart_bottom = split + 65, top + 92, right - 45, bottom - 96
- draw.text((split + 45, top + 28), "Score trend", fill=COLORS["text"], font=MONO_BOLD)
- for score in (80, 90, 100):
- y = chart_bottom - int((score - 80) / 20 * (chart_bottom - chart_top))
- draw.line((chart_left, y, chart_right, y), fill="#26384E", width=1)
- draw.text((chart_left - 40, y - 10), str(score), fill=COLORS["muted"], font=BADGE)
-
- points: list[tuple[int, int]] = []
- for index, (_, score, _, _, _) in enumerate(HISTORY_ROWS[:visible_rows]):
- x = chart_left + int(index * (chart_right - chart_left) / (len(HISTORY_ROWS) - 1))
- y = chart_bottom - int((score - 80) / 20 * (chart_bottom - chart_top))
- points.append((x, y))
- if len(points) > 1:
- draw.line(points, fill=COLORS["cyan"], width=5, joint="curve")
- for index, (x, y) in enumerate(points):
- draw.ellipse((x - 8, y - 8, x + 8, y + 8), fill=COLORS["green"], outline="#D7FFE0", width=2)
- draw.text((x - 12, chart_bottom + 20), f"#{index + 1}", fill=COLORS["muted"], font=BADGE)
-
- if visible_rows == len(HISTORY_ROWS):
- draw.rounded_rectangle((left + 27, bottom - 57, right - 27, bottom - 17), radius=8, fill="#123B32", outline="#286B57")
- draw.text((left + 47, bottom - 47), "TREND: +18 OVERALL — EVERY CHANGE HAS EVIDENCE", fill=COLORS["green"], font=FOOTER)
-
- draw.text((60, 632), "Turn instruction quality from a one-time score into a reviewable engineering signal.", fill=COLORS["muted"], font=FOOTER)
- draw.text((1055, 632), "v0.18", fill=COLORS["cyan"], font=FOOTER)
- return image
+ draw.rectangle((0, 0, WIDTH, 7), fill=MINT)
+ draw.text((58, 39), "AGENTCONFIGSCORE / " + section, font=LABEL, fill=MINT)
+ draw.text((1142, 39), "v" + RELEASE, anchor="ra", font=LABEL, fill=SOFT)
+ draw.text((56, 81), headline, font=TITLE, fill=WHITE)
+ draw.text((58, 146), detail, font=TEXT, fill=SOFT)
+ return image, draw
-def render_history(output: Path) -> None:
- frames = [_history_frame(0)]
- durations = [800]
- for visible in range(1, len(HISTORY_ROWS) + 1):
- frames.append(_history_frame(visible))
- durations.append(650)
- frames.append(_history_frame(len(HISTORY_ROWS)))
- durations.append(3600)
- _save_gif(frames, durations, output)
+def card(draw: ImageDraw.ImageDraw, bounds: tuple[int, int, int, int]) -> None:
+ draw.rounded_rectangle(bounds, radius=18, fill=PANEL, outline=EDGE, width=2)
-def _save_gif(frames: list[Image.Image], durations: list[int], output: Path) -> None:
+def save_gif(frames: list[Image.Image], durations: list[int], output: Path) -> None:
output.parent.mkdir(parents=True, exist_ok=True)
- palette_frames = [frame.quantize(colors=128, method=Image.Quantize.MEDIANCUT) for frame in frames]
- palette_frames[0].save(
- output,
- save_all=True,
- append_images=palette_frames[1:],
- duration=durations,
- loop=0,
- disposal=2,
- optimize=True,
- )
+ palette = [frame.quantize(colors=128) for frame in frames]
+ palette[0].save(output, save_all=True, append_images=palette[1:], duration=durations, loop=0, disposal=2, optimize=True)
+
+
+def render_regression(output: Path, report: dict) -> None:
+ finding = report["new_findings"][0]
+ frames = []
+ for step in range(3):
+ image, draw = canvas("REGRESSION REVIEW", "Same score. New finding.", "An instruction file moves to the repository root.")
+ for x, name, path, score in (
+ (58, "REVIEWED BASELINE", "docs/AGENTS.md", report["base"]["score"]),
+ (616, "PROPOSED CHANGE", "AGENTS.md", report["head"]["score"]),
+ ):
+ card(draw, (x, 205, x + 526, 369))
+ draw.text((x + 24, 226), name, font=LABEL, fill=SOFT)
+ draw.text((x + 24, 282), path, font=MONO, fill=WHITE)
+ if step >= 1:
+ draw.text((x + 496, 300), str(score), anchor="rm", font=SCORE, fill=MINT)
+ card(draw, (58, 396, 1142, 584))
+ draw.text((82, 416), "$ agent-config-score compare base head", font=MONO, fill=WHITE)
+ if step == 2:
+ draw.text((82, 471), finding["code"], font=HEADING, fill=CORAL)
+ draw.text((82, 524), "Former path " + finding["file"], font=TEXT, fill=SOFT)
+ draw.text((1115, 524), "REVIEW REQUIRED / EXIT 1", anchor="ra", font=LABEL, fill=CORAL)
+ draw.text((58, 620), "Reduced example from a merged PR • Identical instruction bytes", font=LABEL, fill=SOFT)
+ frames.append(image)
+ save_gif(frames, [1050, 1300, 4500], output)
-def render_social_preview(output: Path) -> None:
- width, height = 1280, 640
- image = Image.new("RGB", (width, height), COLORS["page"])
- draw = ImageDraw.Draw(image)
+def render_setup(output: Path) -> None:
+ checks = setup_checks()
+ frames = []
+ for step in range(3):
+ image, draw = canvas("QUICK START", "Install. Initialize. Verify.", "A local check confirms the files, policy, and Git baseline.")
+ card(draw, (58, 205, 1142, 586))
+ draw.text((82, 231), "$ python -m pip install agent-config-score==" + RELEASE, font=MONO, fill=WHITE)
+ if step >= 1:
+ draw.text((82, 295), "$ agent-config-score init", font=MONO, fill=WHITE)
+ draw.text((82, 337), ".agentconfigscore.json + GitHub workflow", font=TEXT, fill=MINT)
+ if step >= 2:
+ draw.text((82, 399), "$ agent-config-score doctor", font=MONO, fill=WHITE)
+ for index, (name, status) in enumerate(checks):
+ column, row = divmod(index, 2)
+ draw.text((82 + 485 * column, 461 + 47 * row), f"{status.upper()} {name}", font=MONO, fill=MINT)
+ draw.text((58, 620), "Commit the generated policy and workflow to enable the PR check.", font=LABEL, fill=SOFT)
+ frames.append(image)
+ save_gif(frames, [1200, 1450, 4200], output)
- for y in range(height):
- ratio = y / (height - 1)
- start = (7, 17, 31)
- end = (12, 31, 52)
- color = tuple(round(start[channel] * (1 - ratio) + end[channel] * ratio) for channel in range(3))
- draw.line((0, y, width, y), fill=color)
- draw.ellipse((790, -320, 1450, 340), fill="#12314B")
- draw.ellipse((990, 330, 1390, 730), fill="#0E3A35")
-
- draw.text((74, 60), "AgentConfigScore", fill=COLORS["text"], font=SOCIAL_TITLE)
- draw.text(
- (78, 137),
- "Stop AI-agent instruction regressions before they merge.",
- fill=COLORS["text"],
- font=SOCIAL_TAGLINE,
- )
- draw.text(
- (79, 184),
- "A tamper-resistant regression gate for AGENTS.md, CLAUDE.md, Cursor, Copilot, and Gemini.",
- fill=COLORS["muted"],
- font=SUBTITLE,
- )
- chips = ("AGENTS.md", "CLAUDE.md", "CURSOR", "COPILOT", "GEMINI")
- chip_x = 79
- for chip in chips:
- chip_width = int(draw.textlength(chip, font=BADGE)) + 30
- draw.rounded_rectangle(
- (chip_x, 227, chip_x + chip_width, 263),
- radius=18,
- fill="#162A40",
- outline="#31506E",
- )
- draw.text((chip_x + 15, 237), chip, fill=COLORS["cyan"], font=BADGE)
- chip_x += chip_width + 12
-
- card = (74, 310, 866, 552)
- draw.rounded_rectangle(card, radius=18, fill=COLORS["window"], outline=COLORS["border"], width=2)
- draw.rounded_rectangle((74, 310, 866, 356), radius=18, fill=COLORS["chrome"])
- draw.rectangle((74, 340, 866, 356), fill=COLORS["chrome"])
- for index, color in enumerate(("#FF5F57", "#FEBC2E", "#28C840")):
- x = 98 + index * 25
- draw.ellipse((x, 327, x + 12, 339), fill=color)
- draw.text((286, 324), "pull request check", fill=COLORS["muted"], font=BADGE)
- draw.text((105, 383), "$ agent-config-score diff origin/main", fill=COLORS["text"], font=MONO)
- draw.text((105, 430), "A 100", fill=COLORS["green"], font=MONO_BOLD)
- draw.text((175, 430), "→", fill=COLORS["muted"], font=MONO)
- draw.text((213, 430), "B 82 (-18)", fill=COLORS["amber"], font=MONO_BOLD)
- draw.rounded_rectangle((105, 478, 835, 524), radius=9, fill="#291B20", outline="#71333A")
- draw.text((128, 490), "POLICY RESULT: BLOCKED", fill=COLORS["red"], font=MONO_BOLD)
-
- score_center = (1050, 360)
- draw.ellipse((922, 232, 1178, 488), fill="#10283B", outline="#286B57", width=4)
- draw.text((score_center[0], 274), "SELF-SCAN", anchor="mm", fill=COLORS["muted"], font=SOCIAL_SCORE_LABEL)
- draw.text((score_center[0], 367), "A 100", anchor="mm", fill=COLORS["green"], font=SOCIAL_SCORE)
- draw.text((score_center[0], 429), "DETERMINISTIC", anchor="mm", fill=COLORS["cyan"], font=SOCIAL_SCORE_LABEL)
-
- draw.text((78, 586), "github.com/LE0-Lin/AgentConfigScore", fill=COLORS["cyan"], font=FOOTER)
- draw.text((931, 586), "PYTHON 3.10+ · ZERO RUNTIME DEPS", fill=COLORS["muted"], font=BADGE)
-
- output.parent.mkdir(parents=True, exist_ok=True)
+def render_history(output: Path) -> None:
+ scores = history_scores()
+ if len(scores) != 3:
+ raise RuntimeError(f"Expected three recorded scores, got {scores}")
+ frames = []
+ for visible in range(1, len(scores) + 1):
+ image, draw = canvas("SCORE HISTORY", "Follow the change over time.", "$ agent-config-score history")
+ card(draw, (58, 206, 1142, 586))
+ points = [(224 + 376 * i, 352 + (max(scores) - score) * 6) for i, score in enumerate(scores)]
+ if visible > 1:
+ draw.line(points[:visible], fill=MINT, width=4)
+ for index, (x, y) in enumerate(points[:visible]):
+ draw.ellipse((x - 8, y - 8, x + 8, y + 8), fill=MINT if index != 1 else CORAL)
+ draw.text((x, y - 66), str(scores[index]), anchor="ma", font=SCORE, fill=WHITE)
+ for index, label in enumerate(("Baseline", "Risk added", "Risk resolved")):
+ draw.text((224 + 376 * index, 526), label, anchor="ma", font=TEXT, fill=SOFT)
+ draw.text((58, 620), "Three recorded scores from the local regression fixture", font=LABEL, fill=SOFT)
+ frames.append(image)
+ save_gif(frames, [900, 1150, 4000], output)
+
+
+def render_social(output: Path, report: dict) -> None:
+ image = Image.new("RGB", (1280, 640), INK)
+ draw = ImageDraw.Draw(image)
+ draw.rectangle((0, 0, 1280, 7), fill=MINT)
+ draw.text((66, 52), "AGENT CONFIG REVIEW / LOCAL STATIC ANALYSIS", font=LABEL, fill=MINT)
+ draw.text((62, 96), "AgentConfigScore", font=font(70, bold=True), fill=WHITE)
+ draw.text((66, 200), "Same score. New review finding.", font=HEADING, fill=SOFT)
+ card(draw, (66, 280, 1214, 534))
+ draw.text((94, 310), "REVIEWED BASELINE", font=LABEL, fill=SOFT)
+ draw.text((690, 310), "PROPOSED CHANGE", font=LABEL, fill=SOFT)
+ draw.text((94, 353), f"docs/AGENTS.md {report['base']['score']}", font=MONO, fill=WHITE)
+ draw.text((690, 353), f"AGENTS.md {report['head']['score']}", font=MONO, fill=WHITE)
+ draw.line((94, 410, 1186, 410), fill=EDGE, width=2)
+ draw.text((94, 443), report["new_findings"][0]["code"], font=HEADING, fill=CORAL)
+ draw.text((1186, 455), "EXIT 1", anchor="ra", font=LABEL, fill=CORAL)
+ draw.text((66, 576), "BASELINE POLICY / SOURCE-LOCATED FINDINGS / CI REVIEW", font=LABEL, fill=SOFT)
+ draw.text((1214, 576), "v" + RELEASE, anchor="ra", font=LABEL, fill=MINT)
image.save(output, format="PNG", optimize=True)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
- parser.add_argument("--all", action="store_true", help="Render the complete visual demo gallery")
+ parser.add_argument("--output", type=Path, default=ASSETS / "agent-config-score-demo.gif")
+ parser.add_argument("--all", action="store_true", help="Render all GIFs and the share card")
args = parser.parse_args()
- outputs = [(render, args.output)]
+ if cli("--version", cwd=ROOT).strip() != f"AgentConfigScore {RELEASE}":
+ parser.error(f"Install agent-config-score=={RELEASE} in the rendering environment")
+ report = comparison()
+ render_regression(args.output, report)
if args.all:
- outputs.extend(
- [
- (render_setup, SETUP_OUTPUT),
- (render_history, HISTORY_OUTPUT),
- (render_social_preview, SOCIAL_OUTPUT),
- ]
- )
- for renderer, output in outputs:
- renderer(output)
- print(f"wrote {output} ({output.stat().st_size:,} bytes)")
+ render_setup(ASSETS / "agent-config-score-setup.gif")
+ render_history(ASSETS / "agent-config-score-history.gif")
+ render_social(ASSETS / "agent-config-score-social-preview.png", report)
if __name__ == "__main__":
diff --git a/tests/fixtures/contracts/manifest.json b/tests/fixtures/contracts/manifest.json
index c5e7bc0..62199b7 100644
--- a/tests/fixtures/contracts/manifest.json
+++ b/tests/fixtures/contracts/manifest.json
@@ -38,6 +38,13 @@
}
},
"regression_cases": {
+ "instruction-move": {
+ "base_score": 100,
+ "head_score": 100,
+ "delta": 0,
+ "new_findings": ["instruction-file-removed"],
+ "resolved_findings": []
+ },
"unsafe-shell": {
"base_score": 100,
"head_score": 82,
diff --git a/tests/fixtures/contracts/regression/instruction-move/base/.agentconfigscore.json b/tests/fixtures/contracts/regression/instruction-move/base/.agentconfigscore.json
new file mode 100644
index 0000000..ef0abc7
--- /dev/null
+++ b/tests/fixtures/contracts/regression/instruction-move/base/.agentconfigscore.json
@@ -0,0 +1,7 @@
+{
+ "version": 1,
+ "policy": {
+ "max_drop": 0,
+ "fail_on_new_errors": true
+ }
+}
diff --git a/tests/fixtures/contracts/regression/instruction-move/base/docs/AGENTS.md b/tests/fixtures/contracts/regression/instruction-move/base/docs/AGENTS.md
new file mode 100644
index 0000000..8290b49
--- /dev/null
+++ b/tests/fixtures/contracts/regression/instruction-move/base/docs/AGENTS.md
@@ -0,0 +1,4 @@
+# Repository guidance
+
+Run the unit test suite before submitting changes.
+Keep dependency changes small and reviewable.
diff --git a/tests/fixtures/contracts/regression/instruction-move/head/.agentconfigscore.json b/tests/fixtures/contracts/regression/instruction-move/head/.agentconfigscore.json
new file mode 100644
index 0000000..ef0abc7
--- /dev/null
+++ b/tests/fixtures/contracts/regression/instruction-move/head/.agentconfigscore.json
@@ -0,0 +1,7 @@
+{
+ "version": 1,
+ "policy": {
+ "max_drop": 0,
+ "fail_on_new_errors": true
+ }
+}
diff --git a/tests/fixtures/contracts/regression/instruction-move/head/AGENTS.md b/tests/fixtures/contracts/regression/instruction-move/head/AGENTS.md
new file mode 100644
index 0000000..8290b49
--- /dev/null
+++ b/tests/fixtures/contracts/regression/instruction-move/head/AGENTS.md
@@ -0,0 +1,4 @@
+# Repository guidance
+
+Run the unit test suite before submitting changes.
+Keep dependency changes small and reviewable.