@runchr/gstack-antigravity 0.1.0 → 0.1.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.

Potentially problematic release.


This version of @runchr/gstack-antigravity might be problematic. Click here for more details.

Files changed (231) hide show
  1. package/.agents/skills/gstack/.agents/skills/gstack/SKILL.md +651 -0
  2. package/.agents/skills/gstack/.agents/skills/gstack-autoplan/SKILL.md +678 -0
  3. package/.agents/skills/gstack/.agents/skills/gstack-benchmark/SKILL.md +482 -0
  4. package/.agents/skills/gstack/.agents/skills/gstack-browse/SKILL.md +511 -0
  5. package/.agents/skills/gstack/.agents/skills/gstack-canary/SKILL.md +486 -0
  6. package/.agents/skills/gstack/.agents/skills/gstack-careful/SKILL.md +50 -0
  7. package/.agents/skills/gstack/.agents/skills/gstack-cso/SKILL.md +607 -0
  8. package/.agents/skills/gstack/.agents/skills/gstack-design-consultation/SKILL.md +615 -0
  9. package/.agents/skills/gstack/.agents/skills/gstack-design-review/SKILL.md +988 -0
  10. package/.agents/skills/gstack/.agents/skills/gstack-document-release/SKILL.md +604 -0
  11. package/.agents/skills/gstack/.agents/skills/gstack-freeze/SKILL.md +67 -0
  12. package/.agents/skills/gstack/.agents/skills/gstack-guard/SKILL.md +62 -0
  13. package/.agents/skills/gstack/.agents/skills/gstack-investigate/SKILL.md +415 -0
  14. package/.agents/skills/gstack/.agents/skills/gstack-land-and-deploy/SKILL.md +873 -0
  15. package/.agents/skills/gstack/.agents/skills/gstack-office-hours/SKILL.md +986 -0
  16. package/.agents/skills/gstack/.agents/skills/gstack-plan-ceo-review/SKILL.md +1268 -0
  17. package/.agents/skills/gstack/.agents/skills/gstack-plan-design-review/SKILL.md +668 -0
  18. package/.agents/skills/gstack/.agents/skills/gstack-plan-eng-review/SKILL.md +826 -0
  19. package/.agents/skills/gstack/.agents/skills/gstack-qa/SKILL.md +1006 -0
  20. package/.agents/skills/gstack/.agents/skills/gstack-qa-only/SKILL.md +626 -0
  21. package/.agents/skills/gstack/.agents/skills/gstack-retro/SKILL.md +1065 -0
  22. package/.agents/skills/gstack/.agents/skills/gstack-review/SKILL.md +704 -0
  23. package/.agents/skills/gstack/.agents/skills/gstack-setup-browser-cookies/SKILL.md +325 -0
  24. package/.agents/skills/gstack/.agents/skills/gstack-setup-deploy/SKILL.md +450 -0
  25. package/.agents/skills/gstack/.agents/skills/gstack-ship/SKILL.md +1312 -0
  26. package/.agents/skills/gstack/.agents/skills/gstack-unfreeze/SKILL.md +36 -0
  27. package/.agents/skills/gstack/.agents/skills/gstack-upgrade/SKILL.md +220 -0
  28. package/.agents/skills/gstack/.env.example +5 -0
  29. package/.agents/skills/gstack/.github/workflows/skill-docs.yml +17 -0
  30. package/.agents/skills/gstack/AGENTS.md +49 -0
  31. package/.agents/skills/gstack/ARCHITECTURE.md +359 -0
  32. package/.agents/skills/gstack/BROWSER.md +271 -0
  33. package/.agents/skills/gstack/CHANGELOG.md +800 -0
  34. package/.agents/skills/gstack/CLAUDE.md +284 -0
  35. package/.agents/skills/gstack/CONTRIBUTING.md +370 -0
  36. package/.agents/skills/gstack/ETHOS.md +129 -0
  37. package/.agents/skills/gstack/LICENSE +21 -0
  38. package/.agents/skills/gstack/README.md +228 -0
  39. package/.agents/skills/gstack/SKILL.md +657 -0
  40. package/.agents/skills/gstack/SKILL.md.tmpl +281 -0
  41. package/.agents/skills/gstack/TODOS.md +564 -0
  42. package/.agents/skills/gstack/VERSION +1 -0
  43. package/.agents/skills/gstack/autoplan/SKILL.md +689 -0
  44. package/.agents/skills/gstack/autoplan/SKILL.md.tmpl +416 -0
  45. package/.agents/skills/gstack/benchmark/SKILL.md +489 -0
  46. package/.agents/skills/gstack/benchmark/SKILL.md.tmpl +233 -0
  47. package/.agents/skills/gstack/bin/dev-setup +68 -0
  48. package/.agents/skills/gstack/bin/dev-teardown +56 -0
  49. package/.agents/skills/gstack/bin/gstack-analytics +191 -0
  50. package/.agents/skills/gstack/bin/gstack-community-dashboard +113 -0
  51. package/.agents/skills/gstack/bin/gstack-config +38 -0
  52. package/.agents/skills/gstack/bin/gstack-diff-scope +71 -0
  53. package/.agents/skills/gstack/bin/gstack-global-discover.ts +591 -0
  54. package/.agents/skills/gstack/bin/gstack-repo-mode +93 -0
  55. package/.agents/skills/gstack/bin/gstack-review-log +9 -0
  56. package/.agents/skills/gstack/bin/gstack-review-read +12 -0
  57. package/.agents/skills/gstack/bin/gstack-slug +15 -0
  58. package/.agents/skills/gstack/bin/gstack-telemetry-log +158 -0
  59. package/.agents/skills/gstack/bin/gstack-telemetry-sync +127 -0
  60. package/.agents/skills/gstack/bin/gstack-update-check +196 -0
  61. package/.agents/skills/gstack/browse/SKILL.md +517 -0
  62. package/.agents/skills/gstack/browse/SKILL.md.tmpl +141 -0
  63. package/.agents/skills/gstack/browse/bin/find-browse +21 -0
  64. package/.agents/skills/gstack/browse/bin/remote-slug +14 -0
  65. package/.agents/skills/gstack/browse/scripts/build-node-server.sh +48 -0
  66. package/.agents/skills/gstack/browse/src/browser-manager.ts +634 -0
  67. package/.agents/skills/gstack/browse/src/buffers.ts +137 -0
  68. package/.agents/skills/gstack/browse/src/bun-polyfill.cjs +109 -0
  69. package/.agents/skills/gstack/browse/src/cli.ts +420 -0
  70. package/.agents/skills/gstack/browse/src/commands.ts +111 -0
  71. package/.agents/skills/gstack/browse/src/config.ts +150 -0
  72. package/.agents/skills/gstack/browse/src/cookie-import-browser.ts +417 -0
  73. package/.agents/skills/gstack/browse/src/cookie-picker-routes.ts +207 -0
  74. package/.agents/skills/gstack/browse/src/cookie-picker-ui.ts +541 -0
  75. package/.agents/skills/gstack/browse/src/find-browse.ts +61 -0
  76. package/.agents/skills/gstack/browse/src/meta-commands.ts +269 -0
  77. package/.agents/skills/gstack/browse/src/platform.ts +17 -0
  78. package/.agents/skills/gstack/browse/src/read-commands.ts +335 -0
  79. package/.agents/skills/gstack/browse/src/server.ts +369 -0
  80. package/.agents/skills/gstack/browse/src/snapshot.ts +398 -0
  81. package/.agents/skills/gstack/browse/src/url-validation.ts +91 -0
  82. package/.agents/skills/gstack/browse/src/write-commands.ts +352 -0
  83. package/.agents/skills/gstack/browse/test/bun-polyfill.test.ts +72 -0
  84. package/.agents/skills/gstack/browse/test/commands.test.ts +1836 -0
  85. package/.agents/skills/gstack/browse/test/config.test.ts +250 -0
  86. package/.agents/skills/gstack/browse/test/cookie-import-browser.test.ts +397 -0
  87. package/.agents/skills/gstack/browse/test/cookie-picker-routes.test.ts +205 -0
  88. package/.agents/skills/gstack/browse/test/find-browse.test.ts +50 -0
  89. package/.agents/skills/gstack/browse/test/fixtures/basic.html +33 -0
  90. package/.agents/skills/gstack/browse/test/fixtures/cursor-interactive.html +22 -0
  91. package/.agents/skills/gstack/browse/test/fixtures/dialog.html +15 -0
  92. package/.agents/skills/gstack/browse/test/fixtures/empty.html +2 -0
  93. package/.agents/skills/gstack/browse/test/fixtures/forms.html +55 -0
  94. package/.agents/skills/gstack/browse/test/fixtures/qa-eval-checkout.html +108 -0
  95. package/.agents/skills/gstack/browse/test/fixtures/qa-eval-spa.html +98 -0
  96. package/.agents/skills/gstack/browse/test/fixtures/qa-eval.html +51 -0
  97. package/.agents/skills/gstack/browse/test/fixtures/responsive.html +49 -0
  98. package/.agents/skills/gstack/browse/test/fixtures/snapshot.html +55 -0
  99. package/.agents/skills/gstack/browse/test/fixtures/spa.html +24 -0
  100. package/.agents/skills/gstack/browse/test/fixtures/states.html +17 -0
  101. package/.agents/skills/gstack/browse/test/fixtures/upload.html +25 -0
  102. package/.agents/skills/gstack/browse/test/gstack-config.test.ts +125 -0
  103. package/.agents/skills/gstack/browse/test/gstack-update-check.test.ts +467 -0
  104. package/.agents/skills/gstack/browse/test/handoff.test.ts +235 -0
  105. package/.agents/skills/gstack/browse/test/path-validation.test.ts +63 -0
  106. package/.agents/skills/gstack/browse/test/platform.test.ts +37 -0
  107. package/.agents/skills/gstack/browse/test/snapshot.test.ts +467 -0
  108. package/.agents/skills/gstack/browse/test/test-server.ts +57 -0
  109. package/.agents/skills/gstack/browse/test/url-validation.test.ts +72 -0
  110. package/.agents/skills/gstack/canary/SKILL.md +493 -0
  111. package/.agents/skills/gstack/canary/SKILL.md.tmpl +220 -0
  112. package/.agents/skills/gstack/careful/SKILL.md +59 -0
  113. package/.agents/skills/gstack/careful/SKILL.md.tmpl +57 -0
  114. package/.agents/skills/gstack/careful/bin/check-careful.sh +112 -0
  115. package/.agents/skills/gstack/codex/SKILL.md +677 -0
  116. package/.agents/skills/gstack/codex/SKILL.md.tmpl +356 -0
  117. package/.agents/skills/gstack/conductor.json +6 -0
  118. package/.agents/skills/gstack/cso/SKILL.md +615 -0
  119. package/.agents/skills/gstack/cso/SKILL.md.tmpl +376 -0
  120. package/.agents/skills/gstack/design-consultation/SKILL.md +625 -0
  121. package/.agents/skills/gstack/design-consultation/SKILL.md.tmpl +369 -0
  122. package/.agents/skills/gstack/design-review/SKILL.md +998 -0
  123. package/.agents/skills/gstack/design-review/SKILL.md.tmpl +262 -0
  124. package/.agents/skills/gstack/docs/images/github-2013.png +0 -0
  125. package/.agents/skills/gstack/docs/images/github-2026.png +0 -0
  126. package/.agents/skills/gstack/docs/skills.md +877 -0
  127. package/.agents/skills/gstack/document-release/SKILL.md +613 -0
  128. package/.agents/skills/gstack/document-release/SKILL.md.tmpl +357 -0
  129. package/.agents/skills/gstack/freeze/SKILL.md +82 -0
  130. package/.agents/skills/gstack/freeze/SKILL.md.tmpl +80 -0
  131. package/.agents/skills/gstack/freeze/bin/check-freeze.sh +68 -0
  132. package/.agents/skills/gstack/gstack-upgrade/SKILL.md +226 -0
  133. package/.agents/skills/gstack/gstack-upgrade/SKILL.md.tmpl +224 -0
  134. package/.agents/skills/gstack/guard/SKILL.md +82 -0
  135. package/.agents/skills/gstack/guard/SKILL.md.tmpl +80 -0
  136. package/.agents/skills/gstack/investigate/SKILL.md +435 -0
  137. package/.agents/skills/gstack/investigate/SKILL.md.tmpl +196 -0
  138. package/.agents/skills/gstack/land-and-deploy/SKILL.md +880 -0
  139. package/.agents/skills/gstack/land-and-deploy/SKILL.md.tmpl +575 -0
  140. package/.agents/skills/gstack/office-hours/SKILL.md +996 -0
  141. package/.agents/skills/gstack/office-hours/SKILL.md.tmpl +624 -0
  142. package/.agents/skills/gstack/package.json +55 -0
  143. package/.agents/skills/gstack/plan-ceo-review/SKILL.md +1277 -0
  144. package/.agents/skills/gstack/plan-ceo-review/SKILL.md.tmpl +838 -0
  145. package/.agents/skills/gstack/plan-design-review/SKILL.md +676 -0
  146. package/.agents/skills/gstack/plan-design-review/SKILL.md.tmpl +314 -0
  147. package/.agents/skills/gstack/plan-eng-review/SKILL.md +836 -0
  148. package/.agents/skills/gstack/plan-eng-review/SKILL.md.tmpl +279 -0
  149. package/.agents/skills/gstack/qa/SKILL.md +1016 -0
  150. package/.agents/skills/gstack/qa/SKILL.md.tmpl +316 -0
  151. package/.agents/skills/gstack/qa/references/issue-taxonomy.md +85 -0
  152. package/.agents/skills/gstack/qa/templates/qa-report-template.md +126 -0
  153. package/.agents/skills/gstack/qa-only/SKILL.md +633 -0
  154. package/.agents/skills/gstack/qa-only/SKILL.md.tmpl +101 -0
  155. package/.agents/skills/gstack/retro/SKILL.md +1072 -0
  156. package/.agents/skills/gstack/retro/SKILL.md.tmpl +833 -0
  157. package/.agents/skills/gstack/review/SKILL.md +849 -0
  158. package/.agents/skills/gstack/review/SKILL.md.tmpl +259 -0
  159. package/.agents/skills/gstack/review/TODOS-format.md +62 -0
  160. package/.agents/skills/gstack/review/checklist.md +190 -0
  161. package/.agents/skills/gstack/review/design-checklist.md +132 -0
  162. package/.agents/skills/gstack/review/greptile-triage.md +220 -0
  163. package/.agents/skills/gstack/scripts/analytics.ts +190 -0
  164. package/.agents/skills/gstack/scripts/dev-skill.ts +82 -0
  165. package/.agents/skills/gstack/scripts/eval-compare.ts +96 -0
  166. package/.agents/skills/gstack/scripts/eval-list.ts +116 -0
  167. package/.agents/skills/gstack/scripts/eval-select.ts +86 -0
  168. package/.agents/skills/gstack/scripts/eval-summary.ts +187 -0
  169. package/.agents/skills/gstack/scripts/eval-watch.ts +172 -0
  170. package/.agents/skills/gstack/scripts/gen-skill-docs.ts +2414 -0
  171. package/.agents/skills/gstack/scripts/skill-check.ts +167 -0
  172. package/.agents/skills/gstack/setup +269 -0
  173. package/.agents/skills/gstack/setup-browser-cookies/SKILL.md +330 -0
  174. package/.agents/skills/gstack/setup-browser-cookies/SKILL.md.tmpl +74 -0
  175. package/.agents/skills/gstack/setup-deploy/SKILL.md +459 -0
  176. package/.agents/skills/gstack/setup-deploy/SKILL.md.tmpl +220 -0
  177. package/.agents/skills/gstack/ship/SKILL.md +1457 -0
  178. package/.agents/skills/gstack/ship/SKILL.md.tmpl +528 -0
  179. package/.agents/skills/gstack/supabase/config.sh +10 -0
  180. package/.agents/skills/gstack/supabase/functions/community-pulse/index.ts +59 -0
  181. package/.agents/skills/gstack/supabase/functions/telemetry-ingest/index.ts +135 -0
  182. package/.agents/skills/gstack/supabase/functions/update-check/index.ts +37 -0
  183. package/.agents/skills/gstack/supabase/migrations/001_telemetry.sql +89 -0
  184. package/.agents/skills/gstack/test/analytics.test.ts +277 -0
  185. package/.agents/skills/gstack/test/codex-e2e.test.ts +197 -0
  186. package/.agents/skills/gstack/test/fixtures/coverage-audit-fixture.ts +76 -0
  187. package/.agents/skills/gstack/test/fixtures/eval-baselines.json +7 -0
  188. package/.agents/skills/gstack/test/fixtures/qa-eval-checkout-ground-truth.json +43 -0
  189. package/.agents/skills/gstack/test/fixtures/qa-eval-ground-truth.json +43 -0
  190. package/.agents/skills/gstack/test/fixtures/qa-eval-spa-ground-truth.json +43 -0
  191. package/.agents/skills/gstack/test/fixtures/review-eval-design-slop.css +86 -0
  192. package/.agents/skills/gstack/test/fixtures/review-eval-design-slop.html +41 -0
  193. package/.agents/skills/gstack/test/fixtures/review-eval-enum-diff.rb +30 -0
  194. package/.agents/skills/gstack/test/fixtures/review-eval-enum.rb +27 -0
  195. package/.agents/skills/gstack/test/fixtures/review-eval-vuln.rb +14 -0
  196. package/.agents/skills/gstack/test/gemini-e2e.test.ts +173 -0
  197. package/.agents/skills/gstack/test/gen-skill-docs.test.ts +1049 -0
  198. package/.agents/skills/gstack/test/global-discover.test.ts +187 -0
  199. package/.agents/skills/gstack/test/helpers/codex-session-runner.ts +282 -0
  200. package/.agents/skills/gstack/test/helpers/e2e-helpers.ts +239 -0
  201. package/.agents/skills/gstack/test/helpers/eval-store.test.ts +548 -0
  202. package/.agents/skills/gstack/test/helpers/eval-store.ts +689 -0
  203. package/.agents/skills/gstack/test/helpers/gemini-session-runner.test.ts +104 -0
  204. package/.agents/skills/gstack/test/helpers/gemini-session-runner.ts +201 -0
  205. package/.agents/skills/gstack/test/helpers/llm-judge.ts +130 -0
  206. package/.agents/skills/gstack/test/helpers/observability.test.ts +283 -0
  207. package/.agents/skills/gstack/test/helpers/session-runner.test.ts +96 -0
  208. package/.agents/skills/gstack/test/helpers/session-runner.ts +357 -0
  209. package/.agents/skills/gstack/test/helpers/skill-parser.ts +206 -0
  210. package/.agents/skills/gstack/test/helpers/touchfiles.ts +260 -0
  211. package/.agents/skills/gstack/test/hook-scripts.test.ts +373 -0
  212. package/.agents/skills/gstack/test/skill-e2e-browse.test.ts +293 -0
  213. package/.agents/skills/gstack/test/skill-e2e-deploy.test.ts +279 -0
  214. package/.agents/skills/gstack/test/skill-e2e-design.test.ts +614 -0
  215. package/.agents/skills/gstack/test/skill-e2e-plan.test.ts +538 -0
  216. package/.agents/skills/gstack/test/skill-e2e-qa-bugs.test.ts +194 -0
  217. package/.agents/skills/gstack/test/skill-e2e-qa-workflow.test.ts +412 -0
  218. package/.agents/skills/gstack/test/skill-e2e-review.test.ts +535 -0
  219. package/.agents/skills/gstack/test/skill-e2e-workflow.test.ts +586 -0
  220. package/.agents/skills/gstack/test/skill-e2e.test.ts +3325 -0
  221. package/.agents/skills/gstack/test/skill-llm-eval.test.ts +787 -0
  222. package/.agents/skills/gstack/test/skill-parser.test.ts +179 -0
  223. package/.agents/skills/gstack/test/skill-routing-e2e.test.ts +605 -0
  224. package/.agents/skills/gstack/test/skill-validation.test.ts +1520 -0
  225. package/.agents/skills/gstack/test/telemetry.test.ts +278 -0
  226. package/.agents/skills/gstack/test/touchfiles.test.ts +262 -0
  227. package/.agents/skills/gstack/unfreeze/SKILL.md +40 -0
  228. package/.agents/skills/gstack/unfreeze/SKILL.md.tmpl +38 -0
  229. package/README.md +12 -7
  230. package/README_KO.md +12 -6
  231. package/package.json +3 -2
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Diff-based test selection for E2E and LLM-judge evals.
3
+ *
4
+ * Each test declares which source files it depends on ("touchfiles").
5
+ * The test runner checks `git diff` and only runs tests whose
6
+ * dependencies were modified. Override with EVALS_ALL=1 to run everything.
7
+ */
8
+
9
+ import { spawnSync } from 'child_process';
10
+
11
+ // --- Glob matching ---
12
+
13
+ /**
14
+ * Match a file path against a glob pattern.
15
+ * Supports:
16
+ * ** — match any number of path segments
17
+ * * — match within a single segment (no /)
18
+ */
19
+ export function matchGlob(file: string, pattern: string): boolean {
20
+ const regexStr = pattern
21
+ .replace(/\./g, '\\.')
22
+ .replace(/\*\*/g, '{{GLOBSTAR}}')
23
+ .replace(/\*/g, '[^/]*')
24
+ .replace(/\{\{GLOBSTAR\}\}/g, '.*');
25
+ return new RegExp(`^${regexStr}$`).test(file);
26
+ }
27
+
28
+ // --- Touchfile maps ---
29
+
30
+ /**
31
+ * E2E test touchfiles — keyed by testName (the string passed to runSkillTest).
32
+ * Each test lists the file patterns that, if changed, require the test to run.
33
+ */
34
+ export const E2E_TOUCHFILES: Record<string, string[]> = {
35
+ // Browse core
36
+ 'browse-basic': ['browse/src/**'],
37
+ 'browse-snapshot': ['browse/src/**'],
38
+
39
+ // SKILL.md setup + preamble (depend on ROOT SKILL.md only)
40
+ 'skillmd-setup-discovery': ['SKILL.md', 'SKILL.md.tmpl'],
41
+ 'skillmd-no-local-binary': ['SKILL.md', 'SKILL.md.tmpl'],
42
+ 'skillmd-outside-git': ['SKILL.md', 'SKILL.md.tmpl'],
43
+
44
+ 'contributor-mode': ['SKILL.md.tmpl', 'scripts/gen-skill-docs.ts'],
45
+ 'session-awareness': ['SKILL.md', 'SKILL.md.tmpl'],
46
+
47
+ // QA
48
+ 'qa-quick': ['qa/**', 'browse/src/**'],
49
+ 'qa-b6-static': ['qa/**', 'browse/src/**', 'browse/test/fixtures/qa-eval.html', 'test/fixtures/qa-eval-ground-truth.json'],
50
+ 'qa-b7-spa': ['qa/**', 'browse/src/**', 'browse/test/fixtures/qa-eval-spa.html', 'test/fixtures/qa-eval-spa-ground-truth.json'],
51
+ 'qa-b8-checkout': ['qa/**', 'browse/src/**', 'browse/test/fixtures/qa-eval-checkout.html', 'test/fixtures/qa-eval-checkout-ground-truth.json'],
52
+ 'qa-only-no-fix': ['qa-only/**', 'qa/templates/**'],
53
+ 'qa-fix-loop': ['qa/**', 'browse/src/**'],
54
+ 'qa-bootstrap': ['qa/**', 'ship/**'],
55
+
56
+ // Review
57
+ 'review-sql-injection': ['review/**', 'test/fixtures/review-eval-vuln.rb'],
58
+ 'review-enum-completeness': ['review/**', 'test/fixtures/review-eval-enum*.rb'],
59
+ 'review-base-branch': ['review/**'],
60
+ 'review-design-lite': ['review/**', 'test/fixtures/review-eval-design-slop.*'],
61
+
62
+ // Office Hours
63
+ 'office-hours-spec-review': ['office-hours/**', 'scripts/gen-skill-docs.ts'],
64
+
65
+ // Plan reviews
66
+ 'plan-ceo-review': ['plan-ceo-review/**'],
67
+ 'plan-ceo-review-selective': ['plan-ceo-review/**'],
68
+ 'plan-ceo-review-benefits': ['plan-ceo-review/**', 'scripts/gen-skill-docs.ts'],
69
+ 'plan-eng-review': ['plan-eng-review/**'],
70
+ 'plan-eng-review-artifact': ['plan-eng-review/**'],
71
+
72
+ // Ship
73
+ 'ship-base-branch': ['ship/**', 'bin/gstack-repo-mode'],
74
+ 'ship-local-workflow': ['ship/**', 'scripts/gen-skill-docs.ts'],
75
+
76
+ // Setup browser cookies
77
+ 'setup-cookies-detect': ['setup-browser-cookies/**'],
78
+
79
+ // Retro
80
+ 'retro': ['retro/**'],
81
+ 'retro-base-branch': ['retro/**'],
82
+
83
+ // Global discover
84
+ 'global-discover': ['bin/gstack-global-discover.ts', 'test/global-discover.test.ts'],
85
+
86
+ // Document-release
87
+ 'document-release': ['document-release/**'],
88
+
89
+ // Codex (Claude E2E — tests /codex skill via Claude)
90
+ 'codex-review': ['codex/**'],
91
+
92
+ // Codex E2E (tests skills via Codex CLI)
93
+ 'codex-discover-skill': ['codex/**', '.agents/skills/**', 'test/helpers/codex-session-runner.ts'],
94
+ 'codex-review-findings': ['review/**', '.agents/skills/gstack-review/**', 'codex/**', 'test/helpers/codex-session-runner.ts'],
95
+
96
+ // Gemini E2E (tests skills via Gemini CLI)
97
+ 'gemini-discover-skill': ['.agents/skills/**', 'test/helpers/gemini-session-runner.ts'],
98
+ 'gemini-review-findings': ['review/**', '.agents/skills/gstack-review/**', 'test/helpers/gemini-session-runner.ts'],
99
+
100
+
101
+ // Coverage audit (shared fixture) + triage
102
+ 'ship-coverage-audit': ['ship/**', 'test/fixtures/coverage-audit-fixture.ts', 'bin/gstack-repo-mode'],
103
+ 'review-coverage-audit': ['review/**', 'test/fixtures/coverage-audit-fixture.ts'],
104
+ 'plan-eng-coverage-audit': ['plan-eng-review/**', 'test/fixtures/coverage-audit-fixture.ts'],
105
+ 'ship-triage': ['ship/**', 'bin/gstack-repo-mode'],
106
+
107
+ // Design
108
+ 'design-consultation-core': ['design-consultation/**'],
109
+ 'design-consultation-existing': ['design-consultation/**'],
110
+ 'design-consultation-research': ['design-consultation/**'],
111
+ 'design-consultation-preview': ['design-consultation/**'],
112
+ 'plan-design-review-plan-mode': ['plan-design-review/**'],
113
+ 'plan-design-review-no-ui-scope': ['plan-design-review/**'],
114
+ 'design-review-fix': ['design-review/**', 'browse/src/**'],
115
+
116
+ // gstack-upgrade
117
+ 'gstack-upgrade-happy-path': ['gstack-upgrade/**'],
118
+
119
+ // Deploy skills
120
+ 'land-and-deploy-workflow': ['land-and-deploy/**', 'scripts/gen-skill-docs.ts'],
121
+ 'canary-workflow': ['canary/**', 'browse/src/**'],
122
+ 'benchmark-workflow': ['benchmark/**', 'browse/src/**'],
123
+ 'setup-deploy-workflow': ['setup-deploy/**', 'scripts/gen-skill-docs.ts'],
124
+
125
+ // Autoplan
126
+ 'autoplan-core': ['autoplan/**', 'plan-ceo-review/**', 'plan-eng-review/**', 'plan-design-review/**'],
127
+
128
+ // Skill routing — journey-stage tests (depend on ALL skill descriptions)
129
+ 'journey-ideation': ['*/SKILL.md.tmpl', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts'],
130
+ 'journey-plan-eng': ['*/SKILL.md.tmpl', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts'],
131
+ 'journey-think-bigger': ['*/SKILL.md.tmpl', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts'],
132
+ 'journey-debug': ['*/SKILL.md.tmpl', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts'],
133
+ 'journey-qa': ['*/SKILL.md.tmpl', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts'],
134
+ 'journey-code-review': ['*/SKILL.md.tmpl', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts'],
135
+ 'journey-ship': ['*/SKILL.md.tmpl', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts'],
136
+ 'journey-docs': ['*/SKILL.md.tmpl', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts'],
137
+ 'journey-retro': ['*/SKILL.md.tmpl', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts'],
138
+ 'journey-design-system': ['*/SKILL.md.tmpl', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts'],
139
+ 'journey-visual-qa': ['*/SKILL.md.tmpl', 'SKILL.md.tmpl', 'scripts/gen-skill-docs.ts'],
140
+ };
141
+
142
+ /**
143
+ * LLM-judge test touchfiles — keyed by test description string.
144
+ */
145
+ export const LLM_JUDGE_TOUCHFILES: Record<string, string[]> = {
146
+ 'command reference table': ['SKILL.md', 'SKILL.md.tmpl', 'browse/src/commands.ts'],
147
+ 'snapshot flags reference': ['SKILL.md', 'SKILL.md.tmpl', 'browse/src/snapshot.ts'],
148
+ 'browse/SKILL.md reference': ['browse/SKILL.md', 'browse/SKILL.md.tmpl', 'browse/src/**'],
149
+ 'setup block': ['SKILL.md', 'SKILL.md.tmpl'],
150
+ 'regression vs baseline': ['SKILL.md', 'SKILL.md.tmpl', 'browse/src/commands.ts', 'test/fixtures/eval-baselines.json'],
151
+ 'qa/SKILL.md workflow': ['qa/SKILL.md', 'qa/SKILL.md.tmpl'],
152
+ 'qa/SKILL.md health rubric': ['qa/SKILL.md', 'qa/SKILL.md.tmpl'],
153
+ 'qa/SKILL.md anti-refusal': ['qa/SKILL.md', 'qa/SKILL.md.tmpl', 'qa-only/SKILL.md', 'qa-only/SKILL.md.tmpl'],
154
+ 'cross-skill greptile consistency': ['review/SKILL.md', 'review/SKILL.md.tmpl', 'ship/SKILL.md', 'ship/SKILL.md.tmpl', 'review/greptile-triage.md', 'retro/SKILL.md', 'retro/SKILL.md.tmpl'],
155
+ 'baseline score pinning': ['SKILL.md', 'SKILL.md.tmpl', 'test/fixtures/eval-baselines.json'],
156
+
157
+ // Ship & Release
158
+ 'ship/SKILL.md workflow': ['ship/SKILL.md', 'ship/SKILL.md.tmpl'],
159
+ 'document-release/SKILL.md workflow': ['document-release/SKILL.md', 'document-release/SKILL.md.tmpl'],
160
+
161
+ // Plan Reviews
162
+ 'plan-ceo-review/SKILL.md modes': ['plan-ceo-review/SKILL.md', 'plan-ceo-review/SKILL.md.tmpl'],
163
+ 'plan-eng-review/SKILL.md sections': ['plan-eng-review/SKILL.md', 'plan-eng-review/SKILL.md.tmpl'],
164
+ 'plan-design-review/SKILL.md passes': ['plan-design-review/SKILL.md', 'plan-design-review/SKILL.md.tmpl'],
165
+
166
+ // Design skills
167
+ 'design-review/SKILL.md fix loop': ['design-review/SKILL.md', 'design-review/SKILL.md.tmpl'],
168
+ 'design-consultation/SKILL.md research': ['design-consultation/SKILL.md', 'design-consultation/SKILL.md.tmpl'],
169
+
170
+ // Office Hours
171
+ 'office-hours/SKILL.md spec review': ['office-hours/SKILL.md', 'office-hours/SKILL.md.tmpl', 'scripts/gen-skill-docs.ts'],
172
+ 'office-hours/SKILL.md design sketch': ['office-hours/SKILL.md', 'office-hours/SKILL.md.tmpl', 'scripts/gen-skill-docs.ts'],
173
+
174
+ // Deploy skills
175
+ 'land-and-deploy/SKILL.md workflow': ['land-and-deploy/SKILL.md', 'land-and-deploy/SKILL.md.tmpl'],
176
+ 'canary/SKILL.md monitoring loop': ['canary/SKILL.md', 'canary/SKILL.md.tmpl'],
177
+ 'benchmark/SKILL.md perf collection': ['benchmark/SKILL.md', 'benchmark/SKILL.md.tmpl'],
178
+ 'setup-deploy/SKILL.md platform setup': ['setup-deploy/SKILL.md', 'setup-deploy/SKILL.md.tmpl'],
179
+
180
+ // Other skills
181
+ 'retro/SKILL.md instructions': ['retro/SKILL.md', 'retro/SKILL.md.tmpl'],
182
+ 'qa-only/SKILL.md workflow': ['qa-only/SKILL.md', 'qa-only/SKILL.md.tmpl'],
183
+ 'gstack-upgrade/SKILL.md upgrade flow': ['gstack-upgrade/SKILL.md', 'gstack-upgrade/SKILL.md.tmpl'],
184
+ };
185
+
186
+ /**
187
+ * Changes to any of these files trigger ALL tests (both E2E and LLM-judge).
188
+ */
189
+ export const GLOBAL_TOUCHFILES = [
190
+ 'test/helpers/session-runner.ts',
191
+ 'test/helpers/codex-session-runner.ts',
192
+ 'test/helpers/gemini-session-runner.ts',
193
+ 'test/helpers/eval-store.ts',
194
+ 'test/helpers/llm-judge.ts',
195
+ 'scripts/gen-skill-docs.ts',
196
+ 'test/helpers/touchfiles.ts',
197
+ 'browse/test/test-server.ts',
198
+ ];
199
+
200
+ // --- Base branch detection ---
201
+
202
+ /**
203
+ * Detect the base branch by trying refs in order.
204
+ * Returns the first valid ref, or null if none found.
205
+ */
206
+ export function detectBaseBranch(cwd: string): string | null {
207
+ for (const ref of ['origin/main', 'origin/master', 'main', 'master']) {
208
+ const result = spawnSync('git', ['rev-parse', '--verify', ref], {
209
+ cwd, stdio: 'pipe', timeout: 3000,
210
+ });
211
+ if (result.status === 0) return ref;
212
+ }
213
+ return null;
214
+ }
215
+
216
+ /**
217
+ * Get list of files changed between base branch and HEAD.
218
+ */
219
+ export function getChangedFiles(baseBranch: string, cwd: string): string[] {
220
+ const result = spawnSync('git', ['diff', '--name-only', `${baseBranch}...HEAD`], {
221
+ cwd, stdio: 'pipe', timeout: 5000,
222
+ });
223
+ if (result.status !== 0) return [];
224
+ return result.stdout.toString().trim().split('\n').filter(Boolean);
225
+ }
226
+
227
+ // --- Test selection ---
228
+
229
+ /**
230
+ * Select tests to run based on changed files.
231
+ *
232
+ * Algorithm:
233
+ * 1. If any changed file matches a global touchfile → run ALL tests
234
+ * 2. Otherwise, for each test, check if any changed file matches its patterns
235
+ * 3. Return selected + skipped lists with reason
236
+ */
237
+ export function selectTests(
238
+ changedFiles: string[],
239
+ touchfiles: Record<string, string[]>,
240
+ globalTouchfiles: string[] = GLOBAL_TOUCHFILES,
241
+ ): { selected: string[]; skipped: string[]; reason: string } {
242
+ const allTestNames = Object.keys(touchfiles);
243
+
244
+ // Global touchfile hit → run all
245
+ for (const file of changedFiles) {
246
+ if (globalTouchfiles.some(g => matchGlob(file, g))) {
247
+ return { selected: allTestNames, skipped: [], reason: `global: ${file}` };
248
+ }
249
+ }
250
+
251
+ // Per-test matching
252
+ const selected: string[] = [];
253
+ const skipped: string[] = [];
254
+ for (const [testName, patterns] of Object.entries(touchfiles)) {
255
+ const hit = changedFiles.some(f => patterns.some(p => matchGlob(f, p)));
256
+ (hit ? selected : skipped).push(testName);
257
+ }
258
+
259
+ return { selected, skipped, reason: 'diff' };
260
+ }
@@ -0,0 +1,373 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { spawnSync } from 'child_process';
3
+ import * as path from 'path';
4
+ import * as fs from 'fs';
5
+ import * as os from 'os';
6
+
7
+ const ROOT = path.resolve(import.meta.dir, '..');
8
+ const CAREFUL_SCRIPT = path.join(ROOT, 'careful', 'bin', 'check-careful.sh');
9
+ const FREEZE_SCRIPT = path.join(ROOT, 'freeze', 'bin', 'check-freeze.sh');
10
+
11
+ function runHook(scriptPath: string, input: object, env?: Record<string, string>): { exitCode: number; output: any; raw: string } {
12
+ const result = spawnSync('bash', [scriptPath], {
13
+ input: JSON.stringify(input),
14
+ stdio: ['pipe', 'pipe', 'pipe'],
15
+ env: { ...process.env, ...env },
16
+ timeout: 5000,
17
+ });
18
+ const raw = result.stdout.toString().trim();
19
+ let output: any = {};
20
+ try {
21
+ output = JSON.parse(raw);
22
+ } catch {}
23
+ return { exitCode: result.status ?? 1, output, raw };
24
+ }
25
+
26
+ function runHookRaw(scriptPath: string, rawInput: string, env?: Record<string, string>): { exitCode: number; output: any; raw: string } {
27
+ const result = spawnSync('bash', [scriptPath], {
28
+ input: rawInput,
29
+ stdio: ['pipe', 'pipe', 'pipe'],
30
+ env: { ...process.env, ...env },
31
+ timeout: 5000,
32
+ });
33
+ const raw = result.stdout.toString().trim();
34
+ let output: any = {};
35
+ try {
36
+ output = JSON.parse(raw);
37
+ } catch {}
38
+ return { exitCode: result.status ?? 1, output, raw };
39
+ }
40
+
41
+ function carefulInput(command: string) {
42
+ return { tool_input: { command } };
43
+ }
44
+
45
+ function freezeInput(filePath: string) {
46
+ return { tool_input: { file_path: filePath } };
47
+ }
48
+
49
+ function withFreezeDir(freezePath: string, fn: (stateDir: string) => void) {
50
+ const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-freeze-test-'));
51
+ fs.writeFileSync(path.join(stateDir, 'freeze-dir.txt'), freezePath);
52
+ try {
53
+ fn(stateDir);
54
+ } finally {
55
+ fs.rmSync(stateDir, { recursive: true, force: true });
56
+ }
57
+ }
58
+
59
+ // Detect whether the safe-rm-targets regex works on this platform.
60
+ // macOS sed -E does not support \s, so the safe exception check fails there.
61
+ function detectSafeRmWorks(): boolean {
62
+ const { output } = runHook(CAREFUL_SCRIPT, carefulInput('rm -rf node_modules'));
63
+ return output.permissionDecision === undefined;
64
+ }
65
+
66
+ // ============================================================
67
+ // check-careful.sh tests
68
+ // ============================================================
69
+ describe('check-careful.sh', () => {
70
+
71
+ // --- Destructive rm commands ---
72
+
73
+ describe('rm -rf / rm -r', () => {
74
+ test('rm -rf /var/data warns with recursive delete message', () => {
75
+ const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput('rm -rf /var/data'));
76
+ expect(exitCode).toBe(0);
77
+ expect(output.permissionDecision).toBe('ask');
78
+ expect(output.message).toContain('recursive delete');
79
+ });
80
+
81
+ test('rm -r ./some-dir warns', () => {
82
+ const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput('rm -r ./some-dir'));
83
+ expect(exitCode).toBe(0);
84
+ expect(output.permissionDecision).toBe('ask');
85
+ expect(output.message).toContain('recursive delete');
86
+ });
87
+
88
+ test('rm -rf node_modules allows (safe exception)', () => {
89
+ const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput('rm -rf node_modules'));
90
+ expect(exitCode).toBe(0);
91
+ if (detectSafeRmWorks()) {
92
+ // GNU sed: safe exception triggers, allows through
93
+ expect(output.permissionDecision).toBeUndefined();
94
+ } else {
95
+ // macOS sed: safe exception regex uses \\s which is unsupported,
96
+ // so the safe-targets check fails and the command warns
97
+ expect(output.permissionDecision).toBe('ask');
98
+ }
99
+ });
100
+
101
+ test('rm -rf .next dist allows (multiple safe targets)', () => {
102
+ const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput('rm -rf .next dist'));
103
+ expect(exitCode).toBe(0);
104
+ if (detectSafeRmWorks()) {
105
+ expect(output.permissionDecision).toBeUndefined();
106
+ } else {
107
+ expect(output.permissionDecision).toBe('ask');
108
+ }
109
+ });
110
+
111
+ test('rm -rf node_modules /var/data warns (mixed safe+unsafe)', () => {
112
+ const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput('rm -rf node_modules /var/data'));
113
+ expect(exitCode).toBe(0);
114
+ expect(output.permissionDecision).toBe('ask');
115
+ expect(output.message).toContain('recursive delete');
116
+ });
117
+ });
118
+
119
+ // --- SQL destructive commands ---
120
+ // Note: SQL commands that contain embedded double quotes (e.g., psql -c "DROP TABLE")
121
+ // get their command value truncated by the grep-based JSON extractor because \"
122
+ // terminates the [^"]* match. We use commands WITHOUT embedded quotes so the grep
123
+ // extraction works and the SQL keywords are visible to the pattern matcher.
124
+
125
+ describe('SQL destructive commands', () => {
126
+ test('psql DROP TABLE warns with DROP in message', () => {
127
+ const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput('psql -c DROP TABLE users;'));
128
+ expect(exitCode).toBe(0);
129
+ expect(output.permissionDecision).toBe('ask');
130
+ expect(output.message).toContain('DROP');
131
+ });
132
+
133
+ test('mysql drop database warns (case insensitive)', () => {
134
+ const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput('mysql -e drop database mydb'));
135
+ expect(exitCode).toBe(0);
136
+ expect(output.permissionDecision).toBe('ask');
137
+ expect(output.message.toLowerCase()).toContain('drop');
138
+ });
139
+
140
+ test('psql TRUNCATE warns', () => {
141
+ const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput('psql -c TRUNCATE orders;'));
142
+ expect(exitCode).toBe(0);
143
+ expect(output.permissionDecision).toBe('ask');
144
+ expect(output.message).toContain('TRUNCATE');
145
+ });
146
+ });
147
+
148
+ // --- Git destructive commands ---
149
+
150
+ describe('git destructive commands', () => {
151
+ test('git push --force warns with force-push', () => {
152
+ const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput('git push --force origin main'));
153
+ expect(exitCode).toBe(0);
154
+ expect(output.permissionDecision).toBe('ask');
155
+ expect(output.message).toContain('force-push');
156
+ });
157
+
158
+ test('git push -f warns', () => {
159
+ const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput('git push -f origin main'));
160
+ expect(exitCode).toBe(0);
161
+ expect(output.permissionDecision).toBe('ask');
162
+ expect(output.message).toContain('force-push');
163
+ });
164
+
165
+ test('git reset --hard warns with uncommitted', () => {
166
+ const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput('git reset --hard HEAD~3'));
167
+ expect(exitCode).toBe(0);
168
+ expect(output.permissionDecision).toBe('ask');
169
+ expect(output.message).toContain('uncommitted');
170
+ });
171
+
172
+ test('git checkout . warns', () => {
173
+ const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput('git checkout .'));
174
+ expect(exitCode).toBe(0);
175
+ expect(output.permissionDecision).toBe('ask');
176
+ expect(output.message).toContain('uncommitted');
177
+ });
178
+
179
+ test('git restore . warns', () => {
180
+ const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput('git restore .'));
181
+ expect(exitCode).toBe(0);
182
+ expect(output.permissionDecision).toBe('ask');
183
+ expect(output.message).toContain('uncommitted');
184
+ });
185
+ });
186
+
187
+ // --- Container / infra destructive commands ---
188
+
189
+ describe('container and infra commands', () => {
190
+ test('kubectl delete warns with kubectl in message', () => {
191
+ const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput('kubectl delete pod my-pod'));
192
+ expect(exitCode).toBe(0);
193
+ expect(output.permissionDecision).toBe('ask');
194
+ expect(output.message).toContain('kubectl');
195
+ });
196
+
197
+ test('docker rm -f warns', () => {
198
+ const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput('docker rm -f container123'));
199
+ expect(exitCode).toBe(0);
200
+ expect(output.permissionDecision).toBe('ask');
201
+ expect(output.message).toContain('Docker');
202
+ });
203
+
204
+ test('docker system prune -a warns', () => {
205
+ const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput('docker system prune -a'));
206
+ expect(exitCode).toBe(0);
207
+ expect(output.permissionDecision).toBe('ask');
208
+ expect(output.message).toContain('Docker');
209
+ });
210
+ });
211
+
212
+ // --- Safe commands ---
213
+
214
+ describe('safe commands allow without warning', () => {
215
+ const safeCmds = [
216
+ 'ls -la',
217
+ 'git status',
218
+ 'npm install',
219
+ 'cat README.md',
220
+ 'echo hello',
221
+ ];
222
+
223
+ for (const cmd of safeCmds) {
224
+ test(`"${cmd}" allows`, () => {
225
+ const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput(cmd));
226
+ expect(exitCode).toBe(0);
227
+ expect(output.permissionDecision).toBeUndefined();
228
+ });
229
+ }
230
+ });
231
+
232
+ // --- Edge cases ---
233
+
234
+ describe('edge cases', () => {
235
+ test('empty command allows gracefully', () => {
236
+ const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput(''));
237
+ expect(exitCode).toBe(0);
238
+ expect(output.permissionDecision).toBeUndefined();
239
+ });
240
+
241
+ test('missing command field allows gracefully', () => {
242
+ const { exitCode, output } = runHook(CAREFUL_SCRIPT, { tool_input: {} });
243
+ expect(exitCode).toBe(0);
244
+ expect(output.permissionDecision).toBeUndefined();
245
+ });
246
+
247
+ test('malformed JSON input allows gracefully (exit 0, output {})', () => {
248
+ const { exitCode, raw } = runHookRaw(CAREFUL_SCRIPT, 'this is not json at all{{{{');
249
+ expect(exitCode).toBe(0);
250
+ expect(raw).toBe('{}');
251
+ });
252
+
253
+ test('Python fallback: grep fails on multiline JSON, Python parses it', () => {
254
+ // Construct JSON where "command": and the value are on separate lines.
255
+ // grep works line-by-line, so it cannot match "command"..."value" across lines.
256
+ // This forces CMD to be empty, triggering the Python fallback which handles
257
+ // the full JSON correctly.
258
+ const rawJson = '{"tool_input":{"command":\n"rm -rf /tmp/important"}}';
259
+ const { exitCode, output } = runHookRaw(CAREFUL_SCRIPT, rawJson);
260
+ expect(exitCode).toBe(0);
261
+ expect(output.permissionDecision).toBe('ask');
262
+ expect(output.message).toContain('recursive delete');
263
+ });
264
+ });
265
+ });
266
+
267
+ // ============================================================
268
+ // check-freeze.sh tests
269
+ // ============================================================
270
+ describe('check-freeze.sh', () => {
271
+
272
+ describe('edits inside freeze boundary', () => {
273
+ test('edit inside freeze boundary allows', () => {
274
+ withFreezeDir('/Users/dev/project/src/', (stateDir) => {
275
+ const { exitCode, output } = runHook(
276
+ FREEZE_SCRIPT,
277
+ freezeInput('/Users/dev/project/src/index.ts'),
278
+ { CLAUDE_PLUGIN_DATA: stateDir },
279
+ );
280
+ expect(exitCode).toBe(0);
281
+ expect(output.permissionDecision).toBeUndefined();
282
+ });
283
+ });
284
+
285
+ test('edit in subdirectory of freeze path allows', () => {
286
+ withFreezeDir('/Users/dev/project/src/', (stateDir) => {
287
+ const { exitCode, output } = runHook(
288
+ FREEZE_SCRIPT,
289
+ freezeInput('/Users/dev/project/src/components/Button.tsx'),
290
+ { CLAUDE_PLUGIN_DATA: stateDir },
291
+ );
292
+ expect(exitCode).toBe(0);
293
+ expect(output.permissionDecision).toBeUndefined();
294
+ });
295
+ });
296
+ });
297
+
298
+ describe('edits outside freeze boundary', () => {
299
+ test('edit outside freeze boundary denies', () => {
300
+ withFreezeDir('/Users/dev/project/src/', (stateDir) => {
301
+ const { exitCode, output } = runHook(
302
+ FREEZE_SCRIPT,
303
+ freezeInput('/Users/dev/other-project/index.ts'),
304
+ { CLAUDE_PLUGIN_DATA: stateDir },
305
+ );
306
+ expect(exitCode).toBe(0);
307
+ expect(output.permissionDecision).toBe('deny');
308
+ expect(output.message).toContain('freeze');
309
+ expect(output.message).toContain('outside');
310
+ });
311
+ });
312
+
313
+ test('write outside freeze boundary denies', () => {
314
+ withFreezeDir('/Users/dev/project/src/', (stateDir) => {
315
+ const { exitCode, output } = runHook(
316
+ FREEZE_SCRIPT,
317
+ freezeInput('/etc/hosts'),
318
+ { CLAUDE_PLUGIN_DATA: stateDir },
319
+ );
320
+ expect(exitCode).toBe(0);
321
+ expect(output.permissionDecision).toBe('deny');
322
+ expect(output.message).toContain('freeze');
323
+ expect(output.message).toContain('outside');
324
+ });
325
+ });
326
+ });
327
+
328
+ describe('trailing slash prevents prefix confusion', () => {
329
+ test('freeze at /src/ denies /src-old/ (trailing slash prevents prefix match)', () => {
330
+ withFreezeDir('/Users/dev/project/src/', (stateDir) => {
331
+ const { exitCode, output } = runHook(
332
+ FREEZE_SCRIPT,
333
+ freezeInput('/Users/dev/project/src-old/index.ts'),
334
+ { CLAUDE_PLUGIN_DATA: stateDir },
335
+ );
336
+ expect(exitCode).toBe(0);
337
+ expect(output.permissionDecision).toBe('deny');
338
+ expect(output.message).toContain('outside');
339
+ });
340
+ });
341
+ });
342
+
343
+ describe('no freeze file exists', () => {
344
+ test('allows everything when no freeze file present', () => {
345
+ const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-freeze-test-'));
346
+ try {
347
+ const { exitCode, output } = runHook(
348
+ FREEZE_SCRIPT,
349
+ freezeInput('/anywhere/at/all.ts'),
350
+ { CLAUDE_PLUGIN_DATA: stateDir },
351
+ );
352
+ expect(exitCode).toBe(0);
353
+ expect(output.permissionDecision).toBeUndefined();
354
+ } finally {
355
+ fs.rmSync(stateDir, { recursive: true, force: true });
356
+ }
357
+ });
358
+ });
359
+
360
+ describe('edge cases', () => {
361
+ test('missing file_path field allows gracefully', () => {
362
+ withFreezeDir('/Users/dev/project/src/', (stateDir) => {
363
+ const { exitCode, output } = runHook(
364
+ FREEZE_SCRIPT,
365
+ { tool_input: {} },
366
+ { CLAUDE_PLUGIN_DATA: stateDir },
367
+ );
368
+ expect(exitCode).toBe(0);
369
+ expect(output.permissionDecision).toBeUndefined();
370
+ });
371
+ });
372
+ });
373
+ });