From 45fdf0eb5b79501dadd6e83338b2346cb274e0ae Mon Sep 17 00:00:00 2001 From: "Xingdi (Eric) Yuan" <4028684+xingdi-eric-yuan@users.noreply.github.com> Date: Thu, 18 Jun 2026 17:58:17 -0400 Subject: [PATCH 1/8] Release ShadowFrog codebase Publish the ShadowFrog skills package, installer, hook templates, examples, evaluation documentation, tests, and release validation scripts to microsoft/ShadowFrog. Use hook-templates/ as the source package directory for bundled hook configs/scripts to satisfy public-repo path rules while preserving installed .github/hooks/ and .claude/hooks/ behavior. Excluded internal repository policy/compliance files and public-repo workflow files, and preserved the public Microsoft SECURITY.md template already present in the target repository. Validation: no internal reference/secrets scan matches; JSON and SKILL frontmatter parse; full pytest 1063 passed; direct Copilot/Claude install smoke tests matched hook templates byte-for-byte; hook fail-open guard passes; shellcheck passes; git diff --check passes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitattributes | 2 + .gitignore | 456 +- CHANGELOG.md | 977 ++++ LICENSE | 21 + README.md | 434 ++ RESPONSIBLE_AI.md | 103 + agent-context.md | 22 + claude.md | 272 ++ eval/README.md | 845 ++++ eval/results_dashboard.html | 4321 +++++++++++++++++ examples/coupon-demo/.shadow/.shadowignore | 35 + .../coupon-case-normalization-mismatch.md | 11 + .../global-coupon-cache-side-effects.md | 12 + .../mutation-through-discount-pipeline.md | 11 + .../manifest.json | 21 + .../patch.diff | 53 + .../report.md | 67 + .../manifest.json | 20 + .../patch.diff | 50 + .../report.md | 64 + .../manifest.json | 49 + .../patch.diff | 75 + .../report.md | 79 + .../coupon-demo/.shadow/_dreams/_index.md | 7 + examples/coupon-demo/.shadow/_index.md | 10 + examples/coupon-demo/.shadow/_meta/state.json | 11 + examples/coupon-demo/.shadow/_prefs.md | 3 + examples/coupon-demo/.shadow/cart.py.md | 65 + examples/coupon-demo/.shadow/inventory.py.md | 46 + examples/coupon-demo/.shadow/test_cart.py.md | 43 + examples/coupon-demo/README.md | 76 + examples/coupon-demo/cart.py | 28 + examples/coupon-demo/inventory.py | 15 + examples/coupon-demo/test_cart.py | 31 + froggy_logo.png | Bin 0 -> 467488 bytes hook-templates/claude-settings.json | 27 + .../scripts/shadow-frog-check-init.sh | 145 + .../scripts/shadow-frog-pre-tool.sh | 242 + hook-templates/shadow-frog-hooks.json | 20 + install.sh | 260 + pytest.ini | 6 + requirements-dev.txt | 7 + scripts/check-hook-failopen.py | 276 ++ shadow_repo.png | Bin 0 -> 2412350 bytes skills/shadow-frog-dream/SKILL.md | 1203 +++++ skills/shadow-frog-dream/_worktree_safety.py | 207 + skills/shadow-frog-dream/dream-cleanup.sh | 166 + skills/shadow-frog-dream/dream-coverage.py | 262 + skills/shadow-frog-dream/dream-gc.sh | 333 ++ skills/shadow-frog-dream/dream-reconcile.py | 1783 +++++++ skills/shadow-frog-dream/dream-setup.sh | 347 ++ skills/shadow-frog-dream/dream-validate.py | 355 ++ skills/shadow-frog-init/SKILL.md | 267 + skills/shadow-frog-init/shadow-init.py | 1513 ++++++ skills/shadow-frog-meditate/SKILL.md | 328 ++ .../shadow-frog-meditate/meditate-repair.py | 371 ++ skills/shadow-frog-update/SKILL.md | 201 + skills/shadow-frog-viewer/SKILL.md | 162 + skills/shadow-frog-viewer/dream-lineage.py | 737 +++ skills/shadow-frog-viewer/shadow-viewer.py | 1393 ++++++ skills/shadow-frog/SKILL.md | 373 ++ tests/__init__.py | 0 tests/conftest.py | 131 + tests/hooks/__init__.py | 0 tests/hooks/test_check_hook_failopen.py | 277 ++ tests/hooks/test_check_init_sh.py | 267 + tests/hooks/test_hook_fault_injection.py | 585 +++ tests/hooks/test_pre_tool_sh.py | 664 +++ tests/skills/__init__.py | 0 tests/skills/shadow_frog_dream/__init__.py | 0 .../test_dream_cleanup_sh.py | 360 ++ .../shadow_frog_dream/test_dream_coverage.py | 332 ++ .../shadow_frog_dream/test_dream_gc_sh.py | 741 +++ .../shadow_frog_dream/test_dream_reconcile.py | 3277 +++++++++++++ .../shadow_frog_dream/test_dream_setup_sh.py | 557 +++ .../shadow_frog_dream/test_dream_validate.py | 582 +++ .../shadow_frog_dream/test_worktree_safety.py | 305 ++ tests/skills/shadow_frog_init/__init__.py | 0 .../shadow_frog_init/test_shadow_init.py | 1604 ++++++ tests/skills/shadow_frog_meditate/__init__.py | 0 .../test_meditate_repair.py | 381 ++ tests/skills/shadow_frog_viewer/__init__.py | 0 .../shadow_frog_viewer/test_dream_lineage.py | 992 ++++ .../shadow_frog_viewer/test_shadow_viewer.py | 2184 +++++++++ tests/test_install_sh.py | 251 + tests/test_smoke.py | 60 + 86 files changed, 32445 insertions(+), 424 deletions(-) create mode 100644 .gitattributes create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 RESPONSIBLE_AI.md create mode 100644 agent-context.md create mode 100644 claude.md create mode 100644 eval/README.md create mode 100644 eval/results_dashboard.html create mode 100644 examples/coupon-demo/.shadow/.shadowignore create mode 100644 examples/coupon-demo/.shadow/_cross/coupon-case-normalization-mismatch.md create mode 100644 examples/coupon-demo/.shadow/_cross/global-coupon-cache-side-effects.md create mode 100644 examples/coupon-demo/.shadow/_cross/mutation-through-discount-pipeline.md create mode 100644 examples/coupon-demo/.shadow/_dreams/20260420-140000Z-cache-poison-sequence/manifest.json create mode 100644 examples/coupon-demo/.shadow/_dreams/20260420-140000Z-cache-poison-sequence/patch.diff create mode 100644 examples/coupon-demo/.shadow/_dreams/20260420-140000Z-cache-poison-sequence/report.md create mode 100644 examples/coupon-demo/.shadow/_dreams/20260420-141000Z-bulk-min-total-interaction/manifest.json create mode 100644 examples/coupon-demo/.shadow/_dreams/20260420-141000Z-bulk-min-total-interaction/patch.diff create mode 100644 examples/coupon-demo/.shadow/_dreams/20260420-141000Z-bulk-min-total-interaction/report.md create mode 100644 examples/coupon-demo/.shadow/_dreams/20260420-142000Z-adversarial-inputs/manifest.json create mode 100644 examples/coupon-demo/.shadow/_dreams/20260420-142000Z-adversarial-inputs/patch.diff create mode 100644 examples/coupon-demo/.shadow/_dreams/20260420-142000Z-adversarial-inputs/report.md create mode 100644 examples/coupon-demo/.shadow/_dreams/_index.md create mode 100644 examples/coupon-demo/.shadow/_index.md create mode 100644 examples/coupon-demo/.shadow/_meta/state.json create mode 100644 examples/coupon-demo/.shadow/_prefs.md create mode 100644 examples/coupon-demo/.shadow/cart.py.md create mode 100644 examples/coupon-demo/.shadow/inventory.py.md create mode 100644 examples/coupon-demo/.shadow/test_cart.py.md create mode 100644 examples/coupon-demo/README.md create mode 100644 examples/coupon-demo/cart.py create mode 100644 examples/coupon-demo/inventory.py create mode 100644 examples/coupon-demo/test_cart.py create mode 100644 froggy_logo.png create mode 100644 hook-templates/claude-settings.json create mode 100755 hook-templates/scripts/shadow-frog-check-init.sh create mode 100755 hook-templates/scripts/shadow-frog-pre-tool.sh create mode 100644 hook-templates/shadow-frog-hooks.json create mode 100755 install.sh create mode 100644 pytest.ini create mode 100644 requirements-dev.txt create mode 100755 scripts/check-hook-failopen.py create mode 100644 shadow_repo.png create mode 100644 skills/shadow-frog-dream/SKILL.md create mode 100644 skills/shadow-frog-dream/_worktree_safety.py create mode 100755 skills/shadow-frog-dream/dream-cleanup.sh create mode 100644 skills/shadow-frog-dream/dream-coverage.py create mode 100755 skills/shadow-frog-dream/dream-gc.sh create mode 100755 skills/shadow-frog-dream/dream-reconcile.py create mode 100755 skills/shadow-frog-dream/dream-setup.sh create mode 100644 skills/shadow-frog-dream/dream-validate.py create mode 100644 skills/shadow-frog-init/SKILL.md create mode 100755 skills/shadow-frog-init/shadow-init.py create mode 100644 skills/shadow-frog-meditate/SKILL.md create mode 100755 skills/shadow-frog-meditate/meditate-repair.py create mode 100644 skills/shadow-frog-update/SKILL.md create mode 100644 skills/shadow-frog-viewer/SKILL.md create mode 100644 skills/shadow-frog-viewer/dream-lineage.py create mode 100755 skills/shadow-frog-viewer/shadow-viewer.py create mode 100644 skills/shadow-frog/SKILL.md create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/hooks/__init__.py create mode 100644 tests/hooks/test_check_hook_failopen.py create mode 100644 tests/hooks/test_check_init_sh.py create mode 100644 tests/hooks/test_hook_fault_injection.py create mode 100644 tests/hooks/test_pre_tool_sh.py create mode 100644 tests/skills/__init__.py create mode 100644 tests/skills/shadow_frog_dream/__init__.py create mode 100644 tests/skills/shadow_frog_dream/test_dream_cleanup_sh.py create mode 100644 tests/skills/shadow_frog_dream/test_dream_coverage.py create mode 100644 tests/skills/shadow_frog_dream/test_dream_gc_sh.py create mode 100644 tests/skills/shadow_frog_dream/test_dream_reconcile.py create mode 100644 tests/skills/shadow_frog_dream/test_dream_setup_sh.py create mode 100644 tests/skills/shadow_frog_dream/test_dream_validate.py create mode 100644 tests/skills/shadow_frog_dream/test_worktree_safety.py create mode 100644 tests/skills/shadow_frog_init/__init__.py create mode 100644 tests/skills/shadow_frog_init/test_shadow_init.py create mode 100644 tests/skills/shadow_frog_meditate/__init__.py create mode 100644 tests/skills/shadow_frog_meditate/test_meditate_repair.py create mode 100644 tests/skills/shadow_frog_viewer/__init__.py create mode 100644 tests/skills/shadow_frog_viewer/test_dream_lineage.py create mode 100644 tests/skills/shadow_frog_viewer/test_shadow_viewer.py create mode 100644 tests/test_install_sh.py create mode 100644 tests/test_smoke.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore index d5a18de..86db3e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,429 +1,37 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates -*.env - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ - -[Dd]ebug/x64/ -[Dd]ebugPublic/x64/ -[Rr]elease/x64/ -[Rr]eleases/x64/ -bin/x64/ -obj/x64/ - -[Dd]ebug/x86/ -[Dd]ebugPublic/x86/ -[Rr]elease/x86/ -[Rr]eleases/x86/ -bin/x86/ -obj/x86/ - -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -[Aa][Rr][Mm]64[Ee][Cc]/ -bld/ -[Oo]bj/ -[Oo]ut/ -[Ll]og/ -[Ll]ogs/ - -# Build results on 'Bin' directories -**/[Bb]in/* -# Uncomment if you have tasks that rely on *.refresh files to move binaries -# (https://github.com/github/gitignore/pull/3736) -#!**/[Bb]in/*.refresh - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* -*.trx - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Approval Tests result files -*.received.* - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ -.artifacts/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.idb -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -# but not Directory.Build.rsp, as it configures directory-level build defaults -!Directory.Build.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.tlog -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* +# OS +.DS_Store +Thumbs.db + +# IDE +.idea/ +.vscode/ +*.swp +*.swo *~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl -# Microsoft Fakes -FakesAssemblies/ +# Python (for any helper scripts) +__pycache__/ +*.py[cod] +.mypy_cache/ +.pytest_cache/ -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat +# Node node_modules/ -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio 6 workspace and project file (working project files containing files to include in project) -*.dsw -*.dsp - -# Visual Studio 6 technical files -*.ncb -*.aps - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -**/.paket/paket.exe -paket-files/ - -# FAKE - F# Make -**/.fake/ - -# CodeRush personal settings -**/.cr/personal - -# Python Tools for Visual Studio (PTVS) -**/__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -#tools/** -#!tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog -MSBuild_Logs/ - -# AWS SAM Build and Temporary Artifacts folder -.aws-sam - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -**/.mfractor/ - -# Local History for Visual Studio -**/.localhistory/ - -# Visual Studio History (VSHistory) files -.vshistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -**/.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd - -# VS Code files for those working on multiple tools -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -!.vscode/*.code-snippets - -# Local History for Visual Studio Code -.history/ - -# Built Visual Studio Code Extensions -*.vsix - -# Windows Installer files from build outputs -*.cab -*.msi -*.msix -*.msm -*.msp +# Generated hooks (installed per-project by skenv) +.github/hooks/ + +# Transient flag file (created by sessionStart hook) +.shadow-frog-needs-init +.shadow/ +!examples/**/.shadow/ +eval-results/ +eval/results_dashboard.png +__pycache__/ +.agent_workdirs/ +eval/swebench-fix/reports/legacy/ +eval/feature-ideation/logs/ + +# Coverage +.coverage +.coverage.* diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7768bdb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,977 @@ +# Changelog + +All notable changes to ShadowFrog are documented here. + +ShadowFrog is a suite of AI coding agent skills that build and maintain +shadow knowledge bases for any codebase. + +--- + +## 2026-06-03 + +### Changed +- **`preToolUse.matcher` now filters at the CLI layer.** The hook used to + fire on every tool call (Read, Bash, glob, …) and filter mutations + internally in bash. With the matcher fix in Copilot CLI 1.0.36, the CLI + itself can now skip the hook for non-mutating tools, eliminating the + per-call subprocess overhead for the majority of tool invocations. + Matcher pattern covers both Copilot CLI lowercase + (`edit`/`create`/`str_replace`/`write`/`multiedit`/`notebookedit`) and + Claude Code PascalCase (`Edit`/`Write`/`MultiEdit`/`NotebookEdit`). + Backwards-compatible: on Copilot CLI < 1.0.36 the matcher field is + ignored and the hook fires on every call (same as before). + +--- + +## 2026-06-02 + +### Added (additional hook hardening) +Follow-up coverage pass before release surfaced sixteen further fixes +across the hook scripts, CI guard, and test matrix. Categorized as 1 +BLOCKING, 6 HIGH, 9 MEDIUM: + +- **BLOCKING — Per-test dedup isolation.** The 70-cell fault matrix shared + a single PPID+lstart-keyed dedup directory across all parametrized cells. + The first cell to touch `a.py` marked it `.injected`; every subsequent + cell skipped the entire viewer subprocess (pre-tool.sh line 95 guard). + Result: the viewer-branch `git rev-parse` + viewer invocation were + exercised exactly ONCE per pytest run — the very code paths the matrix + was added to defend. Reproduced empirically: replacing the bounded + `_git(['rev-parse',...], 0.5)` with an unbounded call passed `70 passed` + even though production would hang for 31s. Fix: `_run()` injects a + unique `SHADOWFROG_TMP_DIR=/_sf_dedup` per test. +- **HIGH — Behavioral SIGTERM coverage.** `subprocess.run(timeout=)` sends + SIGKILL, not SIGTERM, so the matrix had zero behavioral coverage of the + TERM trap. Removing `trap 'exit 0' TERM` was invisible to the entire + test suite. Added `TestPreToolSigterm` using `Popen` + `os.kill(SIGTERM)` + at varied delays, including a "SIGTERM during hung subprocess" case + proving bounded `subprocess.run(timeout=)` ensures the queued signal is + delivered within the 5s deny threshold. +- **HIGH — PascalCase tool name coverage.** Claude Code emits + `Edit`/`Write`/`MultiEdit`/`NotebookEdit`; Copilot CLI emits lowercase. + Removing `.lower()` from TOOL_NAME normalization silently downgraded all + Claude Code mutations to the base reminder — undetected. Added + `TestPreToolPascalCaseToolNames` with 11 spellings asserting the + file-specific actionable branch fires for each. +- **HIGH — CI guard evasion patterns.** The `FORBIDDEN_SET_RE` anchor + `^\s*set\s+` was bypassed by `[[ X ]] && set -e`, `eval 'set -e'`, + `; set -e`, and `set \\\n -e` (line continuation). Substring scanning + + pre-joining line continuations now catch all four. Signal aliases + `SIGTERM` and numeric `15` are normalized to TERM-equivalent. Added 8 + evasion regression tests and 3 alias acceptance tests. +- **HIGH — Strict production wall-clock budget.** The matrix's 7s + wall-clock cap tolerates CI cold-start variance, but happy-path runs + must clear Copilot's strict 5s deny threshold. Added + `test_happy_path_meets_strict_production_budget` (best-of-3 < 5s). +- **HIGH — PATH-empty / python-crash stderr leaks.** With `PATH=""`, + bash itself emits `cat: No such file or directory` to stderr before any + hook code runs. Defensive PATH append + `cat 2>/dev/null` + stderr + redirects on final emitters close the leaks. +- **HIGH — Raw bash-level `python3` script invocations.** CI guard now + flags `python3 path.py` / `python3 -m module` at bash level (same bug + class as raw `git`). Three separate `python3 -c` calls in both hooks + consolidated into single invocations to reduce blast radius. + +Plus 9 MEDIUM cleanups: dedup tmp-dir TTL GC at sessionStart; file_path +precedence over path (was inverted); oversized path cap (1KB); +SIGPIPE in trap pyramid; final emitters silenced; state.json edge-case +coverage (symlink-to-/dev/null, directory-shaped, huge file, future +schema, NUL bytes, deep nesting); readonly-tmpdir test now uses chmod +0500 instead of `/proc` so macOS exercises it; matrix-setup regression +guard that asserts the viewer-branch is actually reachable. + +Test count: **908 passed** (+46 from 862 baseline). New tests include 11 +PascalCase tool variants, 2 SIGTERM behavioral, 6 state.json edge cases, +17 CI guard evasion/alias/raw-python detection, file_path precedence, +strict 5s budget, regression-guard, and cross-platform readonly-tmpdir. + +### Added +- **Multi-layer fail-open defense for advisory hooks** — the original + `trap 'exit 0' EXIT` fix was necessary but insufficient. Three + additional layers are now in place: + 1. **Trap pyramid**: separate `trap 'exit 0' EXIT` and `trap 'exit 0' TERM + HUP INT` traps. EXIT alone returns 143/-15 under SIGTERM (empirically + verified on bash 3.2 macOS and bash 5+ Linux), which the runner's + timeout-kill triggers; the TERM trap converts that to exit 0. + 2. **Every external call bounded**: the previously-unbounded + `git rev-parse --show-toplevel` in the pre-tool viewer-discovery branch + was hanging for 30+ seconds on locked/NFS/fsmonitor-corrupted repos + (reproduced as a 31s hang in Opus-4.8's review), causing runner + SIGTERM-kill → tool denial. All git and viewer subprocesses now run + inside a single consolidated Python block with per-call + `subprocess.run(timeout=...)` wrappers. Total bounded work budget is + ~3.5s, leaving >=1.5s headroom under the hook's 5s `timeoutSec`. + 3. **Static structural enforcement**: `scripts/check-hook-failopen.py` + blocks PRs that re-introduce ANY of the regression vectors the panel + identified — short-form `set -e`/`-u`, long-form `set -o + errexit|nounset|pipefail|errtrace`, `source`/`.` of external files, + missing EXIT or TERM trap, comment-masquerading-as-trap, or unbounded + `git`/external calls at the bash level. +- **Fault-injection test matrix** (`tests/hooks/test_hook_fault_injection.py`) + — parametrized cells that systematically perturb the hooks across four + axes (stubbed binary failures, malformed/malicious JSON payloads, + filesystem & git state, environment). Now also seeds `.shadow/.md` + in setup so the pre-tool viewer-discovery branch is actually exercised + under fault injection — closing the matrix blind spot that let the + unbounded `git rev-parse` bug ship in the first place. Every cell now + asserts wall-clock < 4.5s (matching the production 5s `timeoutSec`). +- **Shellcheck / release automation** — runs shellcheck (info-level on hook + scripts, warning-level on installer/skill scripts) and invokes the + fail-open contract checker described above. +- **Unit tests for the fail-open checker** + (`tests/hooks/test_check_hook_failopen.py`) — 17 adversarial cases + asserting the checker correctly flags every regression vector and accepts + every legitimate pattern (including `git` inside Python heredocs and the + combined `trap '...' EXIT TERM HUP INT` form). + +### Fixed +- **`preToolUse` hook denied tool calls under Copilot CLI ≥ 1.0.57.** As of + v1.0.57, a `preToolUse` command hook that exits non-zero now **denies** the + tool call (previously such errors were silently ignored). Both hook scripts + ran under `set -euo pipefail`, so any failing sub-step — most commonly the + staleness check's `git diff … | wc -l | tr` pipeline when the shadow was + behind HEAD and `git diff` returned non-zero — exited the script non-zero and + surfaced as *"Denied by preToolUse hook (hook errored)"*. The advisory hooks + are now **fail-open** via the multi-layer defense described above. Affects + `hooks/scripts/shadow-frog-pre-tool.sh` and + `hooks/scripts/shadow-frog-check-init.sh`. +- **Unbounded `git rev-parse --show-toplevel` in viewer-discovery branch** + (pre-tool.sh:94, pre-fix). On any system where this git call hangs (NFS, + `.git/index.lock` held by `gitk`/`vscode`/`gh`, fsmonitor/Watchman, large + monorepos), the hook exceeded the 5s `timeoutSec` and the runner SIGTERM- + killed it → tool deny. Now consolidated into one Python subprocess with + `timeout=0.5`, covering the same fallback resolution paths. +- **stderr leak on missing `state.json`.** `state.json` was read via a shell + `< redirect`, which printed `No such file or directory` to stderr (before + `2>/dev/null` applied) when the file was absent. The file is now opened + inside Python so a missing file is a caught exception — no stderr noise. + +### Changed +- Tightened all bounded subprocess timeouts to leave ≥1.5s of headroom under + the 5s `timeoutSec` budget: viewer 1.5s → 1.0s; staleness rev-parse 1.0s → + 0.5s × 2; staleness diff 1.5s → 1.0s. Sum was exactly 5.0s with zero + headroom; now 3.5s. +- Removed redundant `signal.alarm(2)` from the viewer subprocess wrapper — + `subprocess.run(timeout=1.0)` already handles cancellation, and the layered + alarm could leave orphaned grandchildren under load (per Opus-4.7-xhigh + review). +- `_init_minimal_shadow` test helper now seeds `.shadow/.md` by + default so the viewer branch is reachable in tests. Pass `seed_target=None` + to opt out. +- Replaced hardcoded `/tmp/PWNED` injection-test sentinels with `tmp_path`- + scoped paths to eliminate flakiness from stale files across runs. +- Repurposed the `shadow-read-only` cwd-fault cell to make the dedup tmpdir + read-only instead of `state.json` (the latter is read-only by design and + the hook never writes to it, so the original cell was a no-op). +- Fixed `SC2295` quoting bugs (`${VAR#"$PREFIX"}`) in + `hooks/scripts/shadow-frog-pre-tool.sh` and `install.sh` — the unquoted + form treats `$PREFIX` as a glob pattern, which could mangle paths + containing `*`, `?`, or `[` characters. Surfaced by shellcheck CI. + +### Notes +- **`additionalContext` on `preToolUse` is undocumented but functional on + Copilot CLI.** The 2026 hooks reference documents only `permissionDecision`/ + `permissionDecisionReason`/`modifiedArgs` for `preToolUse` output, but the + copilot-cli v1.0.24 changelog explicitly notes that `preToolUse` hooks + "respect modifiedArgs/updatedInput, and additionalContext fields." The + current dual-shape output (top-level `additionalContext` for Copilot, + nested `hookSpecificOutput.additionalContext` for Claude Code) works on + both platforms. If Copilot ever removes this undocumented support, the + shadow-aware reminder would silently no-op on Copilot — the `sessionStart` + reminder would remain. + +--- + +## 2026-05-30 + +### Removed +- **Personal (global) install mode** — `install.sh` no longer symlinks skills + into `~/.copilot/skills/` or `~/.claude/skills/`. ShadowFrog now installs + **only into a specific repository** via the now-required `--project ` + flag. A global install would auto-engage the shadow-edit hooks across every + repository the developer touches, firing unexpected shadow writes (and a + potential information-leakage risk) in projects that never opted in. Per-repo + install also keeps skills committed to the fork, which is what fork-based + dreaming needs. Updated `install.sh` (required `--project`, dropped the + personal block + help text), `README.md` (single "Install into your repo" + section), `claude.md`, the `pre-tool` hook's viewer-resolution loop, every + SKILL.md path-resolution loop and usage example (project paths only), and the + install test suite. + +--- + +Pre-release hardening for the public MIT release: a five-model independent +audit (Opus 4.6 / 4.7 / 4.8, GPT-5.5) drove correctness fixes across the dream +pipeline, the reconciler, and the viewer, plus licensing/transparency docs and +a round of helper-script slimming. + +### Added +- **LICENSE (MIT)** and **`RESPONSIBLE_AI.md`** — standard Microsoft MIT + license plus a transparency note covering intended uses, out-of-scope uses, + evaluation results, limitations, and best practices. README links to both. +- **gitignored-`.shadow/` guard for dream** — when `shadow-frog-init`'s + "local only" option gitignores `.shadow/`, the dream workflow's + `git add -A` silently skipped the shadow content, so nothing reached the + remote and every discovery was lost without warning. `dream-setup.sh` now + fails fast via `git check-ignore .shadow` with a clear fix message; + `shadow-frog-init` step 9 documents the committed-vs-gitignored trade-off; + the dream SKILL adds a "`.shadow/` must be git-tracked" prerequisite; and the + README's Dream section carries a one-line callout. +- **dirty-tree guard on branch cleanup (B16)** — `dream-reconcile.py + --cleanup-branches` merges discoveries into the working tree without + committing, so the old ancestor check passed against a stale HEAD and could + delete dream branches (the only durable copy) before the merge was + persisted. Cleanup now also refuses when `git status --porcelain -- .shadow/` + is non-empty, enforcing reconcile → commit → push → cleanup. + +### Fixed +- **Pre-release audit bug sweep** — a broad set of correctness fixes surfaced + by the 5-model audit: `dream-validate` normalizes bare-string/non-dict + discoveries and tolerates BOM/CRLF in frontmatter; `dream-reconcile` emits + canonical per-file shadow headers, unions refs into existing `_cross` slugs + instead of dropping them, stops truncating the tip SHA, and mirrors + manifest/patch even when a report is corrupt; `dream-coverage` filters to the + shadowed source set; `shadow-viewer` preserves dotfile paths and scopes + cross-ref parsing to the `**Refs**` block; `meditate-repair` recovers + category from frontmatter; `shadow-init` prunes only `EXCLUDE_DIRS` + `.git`; + `install.sh` / `dream-setup.sh` validate args and pass values via quoted + heredocs to prevent shell injection. README drops nonexistent viewer flags; + several SKILL docs corrected. Added 28 regression tests. +- **Discovery metadata loss on duplicate merge (B15)** — on an exact-text + duplicate the reconciler returned early and discarded the new discovery's + metadata, so a later dream recording the same claim with stronger evidence, + higher-trust source, or extra labels lost the upgrade. Exact matches now + merge metadata (union labels, upgrade source by trust, promote + uncertain → verified, never touch `refuted`). Fuzzy near-duplicates still + skip. +- **`_`-prefixed source dirs dropped from totals (B19)** — `update_state` and + `rebuild_top_index` pruned every `_*` directory at every depth, which also + excluded mirrored shadows under `_`-prefixed *source* dirs (e.g. + `src/_internal/foo.py.md`). Internal dirs only live at the top level, so the + `_*` prune now applies only at `.shadow/`'s root. +- **`_index.md` parent column convention (D9)** — meditate's `repair_parent` + wrote a resolved `dream_id` while the reconciler and the branch-keyed lineage + reader used branch names, orphaning nodes after a meditate pass. Standardized + on branch names everywhere. +- **manifest-vs-shadow source-of-truth contradiction** — the dream SKILL + contradicted itself and misdescribed reconcile (the reconciler merges + discoveries from `manifest.json`, not by replaying the branch shadow). + Reworded to match the code: the manifest is authoritative for propagation, + per-file shadows are the human-readable copy and a required validate gate, + and every discovery must be written into both. + +### Removed +- **dream-lineage Graph tab** — the interactive radial force-graph tab in + `dream-lineage.py`'s HTML output (~326 lines: graph data prep, JS + `CAT_COLORS`, constellation CSS, the tab button/container, and `initGraph()`). + The documented Chains / Fresh / Full Tree tabs are unchanged. (`dream-lineage.py` + 1076 → 750 lines.) +- **`_dreams/_coverage.json`** — the exploration coverage map written by + `dream-reconcile.py`'s `rebuild_coverage`. Nothing read it: `dream-coverage.py` + recomputes coverage live on every invocation. Removed the function and its + reconcile call (reconcile steps renumbered 1–9). The shared discovery-counting + helper it used is retained for `rebuild_top_index`. +- **Dead constants and legacy fossils** — unused `VALID_CATEGORIES` / + `VALID_VERDICTS` in `dream-reconcile.py`; the obsolete `_None yet._` + placeholder heal (only the canonical `_No cross-cutting discoveries yet._` is + emitted); and the undocumented `**Parent**:` / `**Chain**:` report-body + scrapers in `dream-lineage.py` (the `builds_on` frontmatter parse is the + supported lineage source). + +--- + +## 2026-05-19 + +Round-2 multi-reviewer audit (Opus 4.7 xhigh + high, Opus 4.6, GPT-5.5): +correctness, security, and documentation fixes across the dream pipeline, +the preToolUse hook, and the meditate index repair. A follow-on Tier 3 +bug sweep landed seven additional correctness fixes in the same area. + +### Fixed (Tier 3 bug sweep) +- **preToolUse hook dedup collision** — `DEDUP_DIR` was keyed on `$PPID` + alone, so distinct Claude Code sessions sharing `PPID=1` under a + process manager, transient shells reusing a PPID, or PID-wrap on + long-running systems would share a dedup bucket and silently suppress + each other's discovery injection. Now keys on `$PPID` plus + `ps -p $PPID -o lstart=` (parent process start time), falling back to + PPID-only if `ps` is unavailable inside sandboxed containers. +- **shadow-init last_commit empty-string sentinel** — `build_state_json` + and the main init path defaulted `last_commit` to `""` when + `git rev-parse` failed. The preToolUse hook reads + `state.get('last_commit', 'none')`, but `.get()`'s default only fires + on missing keys, not empty values — so `git rev-parse --verify ""` + failed and every subsequent staleness comparison misfired with a + false warning. Both call sites now default to `"none"`, which the + hook already handles as the non-git sentinel. +- **shadow-viewer parse_discovery "Dream report:" leakage** — the + continuation-line catch-all branch in `parse_discovery` swept + `Dream report: \`_dreams//\`` markers into the discovery body, + so `--top` and `--summary` output rendered them concatenated to the + behavioral statement. Added an explicit `Dream report:` branch that + extracts the slug into `meta["dream_report"]` instead. +- **dream-reconcile back-pointer idempotency false positive** — the + "is the back-pointer already present?" check used substring + `f'_cross/{slug}.md' in line`, which false-matched any discovery + body mentioning the same slug (e.g. `Also involves: \`_cross/foo.md\``). + That silently swallowed legitimate back-pointer adds on subsequent + dreams. Now matches the actual markdown link target + `]({prefix}_cross/{slug}.md)` using the same depth-aware prefix as + the write path. +- **dream-reconcile case-insensitive heading reads** — most cross-ref + read sites already lowercased the comparison, but + `find_cross_references_heading` used exact match. A meditate or user + rewrite that lowercased `## cross-references` would slip past the + finder, causing `_ensure_cross_references_section` to append a + duplicate section and back-pointer dedup to miss entirely. All + read sites are now consistently case-insensitive; writes still + emit the canonical `## Cross-References`. +- **dream-reconcile top-level `_index.md` not refreshed** — the + reconciler updated `state.json`, per-file shadows, and cross-cutting + files, but `.shadow/_index.md` (the human-readable manifest with + symbol lists and discovery counts) was never regenerated after + reconciliation. It stayed frozen at init values until a manual + meditate or re-init. Added `rebuild_top_index` as Step 8 (runs after + `update_state` so totals reflect the new dream cycle), with + extracted `_count_discoveries` and `_shadow_symbol_names` helpers + shared between coverage and index rebuilds. +- **shadow-frog-dream SKILL anchor mismatch** — the per-validation + error message pointed reviewers to "see Critical Invariants" for + the artifact-format requirement, but the Critical Invariants section + covers paths, branches, RUN_PREFIX, and reconciliation rules — + artifact format is a sub-section. Updated the cross-reference to + "see Critical Invariants → Artifact Format" so reviewers land at + the correct sub-section. + +### Fixed +- **dream-reconcile / dream-coverage count inflation** — `rebuild_coverage` + and `update_state` counted `- ` bullets inside `## Cross-References` + (which are back-pointer links, not discoveries) and counted + `## Cross-References` / `## File-Level` as symbols. Every reconcile + silently corrupted `state.json` totals and `_coverage.json`. Both + loops (plus `dream-coverage.py`'s `check_coverage`) now use an + `in_xref` state machine that matches `--check-invariants` semantics. +- **dream-reconcile back-pointer paths** — subdir shadows (e.g. + `.shadow/src/foo.py.md`) wrote relative links as `_cross/slug.md` + instead of `../_cross/slug.md`, breaking markdown rendering and + `--check-invariants` on any non-flat repo. Now computes depth and + prepends `../` per level. +- **dream-reconcile canonical layout** — bootstrap for never-before-seen + shadow files used `## ` + filename as the heading (treating a filename + as a symbol) and `_None yet._` for the cross-ref placeholder. Now + uses `## File-Level` and `_No cross-cutting discoveries yet._` to + match `shadow-init.py`. Placeholder-detection accepts both forms so + meditate runs heal older shadows. +- **dream-validate first-dream mirror gate** — `git status --porcelain` + rolled untracked subtrees up to a single `?? .shadow/` line, so + first-dream cases where the entire `.shadow/src/` subtree is untracked + false-failed the discovery-mirror gate. Added `--untracked-files=all`. +- **dream-validate mirror-gate error message** — claimed manifest entries + were "LOST at merge time", but `dream-reconcile.py` reads + `manifest.discoveries` directly. Gate kept (PR reviewers still need + shadows in sync with the branch), but the message now accurately + describes the workflow contract being enforced. +- **preToolUse shell→Python injection** — the discovery-inlining heredoc + used an unquoted `<` marker. Also: Phase 7 said "seven invariants" but + listed five — clarified as "five core (full 7-invariant set in + `/shadow-frog`)". +- **`dream-reconcile.py --all` references** — three places documented + an `--all` flag that was never implemented. Rewrote + parallel-batch instructions to use positional branch arguments. +- **`--check-invariants` surfacing** — added missing row to the + shadow-frog-viewer/SKILL.md Available Views table and an example + invocation. Previously only documented in shadow-frog/SKILL.md. +- **Duplicate `# 9.` numbering** in `dream-validate.py` — two checks + were both labelled step 9 in source comments and docstring. + Renumbered consistently (op validator → 9, mirror gate → 10, + label triage → 11). + +### Changed +- **Discovery format spec dedup** — claude.md and + shadow-frog-update/SKILL.md trimmed their duplicated discovery-format + sections (~31% reduction across both) and now point at + shadow-frog/SKILL.md as the canonical source. shadow-frog-init was + audited and left alone (no actual discovery-writing spec there). + +### Fixed (code-quality audit follow-on) + +Systematic 5-script code-quality audit (Opus 4.7 xhigh × 2, Opus 4.7 +high, Opus 4.6 × 2) of `shadow-init.py`, `shadow-viewer.py`, +`dream-reconcile.py`, `dream-lineage.py`, and the seven smaller scripts +(`meditate-repair`, `dream-validate`, `dream-coverage`, `dream-setup.sh`, +both hooks, `install.sh`). Two real bugs surfaced; rest was inline +tidying. Risky refactors flagged for future review. + +- **dream-reconcile prefix-substring data loss in cleanup_branches** — + Safety check 2 used `if dream_id not in index_content:` raw substring + against `.shadow/_dreams/_index.md`. When our dream_id is a prefix of + any indexed ID (e.g. branch `…1400-foo` plus indexed + `…1400-foo-extended`), the check falsely passed, allowing cleanup + to DELETE the unreconciled branch — irreversible loss of whatever + discoveries were only on it. Same false-pass shape in the + descendant-detection check at line 1097. Both now use + `_read_indexed_dream_ids` for parsed-ID set membership. +- **dream-reconcile prefix-substring false-pass in verify_reconciliation** — + Same `if dream_id not in f.read():` substring against the index. + Less severe (verify only reports failures; no data mutation), but + still silently swallowed real index-mismatch bugs. Same fix. +- **dream-lineage md_to_html emitted invalid HTML** — `- item` lines + became bare `
  • ` tags with no `