multi-forge 0.2.0__py3-none-any.whl

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.
Files changed (311) hide show
  1. forge/__init__.py +3 -0
  2. forge/_extensions/agents/.gitkeep +0 -0
  3. forge/_extensions/commands/.gitkeep +0 -0
  4. forge/_extensions/skills/analyze/SKILL.md +87 -0
  5. forge/_extensions/skills/challenge/SKILL.md +91 -0
  6. forge/_extensions/skills/consensus/SKILL.md +120 -0
  7. forge/_extensions/skills/consensus/resources/code_consensus_evaluation.md +94 -0
  8. forge/_extensions/skills/consensus/resources/consensus_evaluation.md +70 -0
  9. forge/_extensions/skills/consensus/resources/synthesis.md +101 -0
  10. forge/_extensions/skills/debate/SKILL.md +116 -0
  11. forge/_extensions/skills/debate/resources/code_debate_evaluation.md +101 -0
  12. forge/_extensions/skills/debate/resources/debate_evaluation.md +90 -0
  13. forge/_extensions/skills/panel/SKILL.md +141 -0
  14. forge/_extensions/skills/panel/resources/synthesis.md +103 -0
  15. forge/_extensions/skills/qa/SKILL.md +704 -0
  16. forge/_extensions/skills/qa/resources/checklist/0-enable.md +78 -0
  17. forge/_extensions/skills/qa/resources/checklist/1-preflight.md +24 -0
  18. forge/_extensions/skills/qa/resources/checklist/10-resume.md +143 -0
  19. forge/_extensions/skills/qa/resources/checklist/11-config.md +150 -0
  20. forge/_extensions/skills/qa/resources/checklist/12-search.md +58 -0
  21. forge/_extensions/skills/qa/resources/checklist/13-guard.md +237 -0
  22. forge/_extensions/skills/qa/resources/checklist/14-workflow.md +305 -0
  23. forge/_extensions/skills/qa/resources/checklist/15-skills.md +155 -0
  24. forge/_extensions/skills/qa/resources/checklist/16-handoff.md +224 -0
  25. forge/_extensions/skills/qa/resources/checklist/17-info.md +50 -0
  26. forge/_extensions/skills/qa/resources/checklist/18-disable.md +84 -0
  27. forge/_extensions/skills/qa/resources/checklist/19-uninstall.md +146 -0
  28. forge/_extensions/skills/qa/resources/checklist/2-extensions.md +188 -0
  29. forge/_extensions/skills/qa/resources/checklist/20-cleanup.md +36 -0
  30. forge/_extensions/skills/qa/resources/checklist/3-auth.md +234 -0
  31. forge/_extensions/skills/qa/resources/checklist/4-proxy.md +481 -0
  32. forge/_extensions/skills/qa/resources/checklist/5-session.md +541 -0
  33. forge/_extensions/skills/qa/resources/checklist/6-hooks.md +275 -0
  34. forge/_extensions/skills/qa/resources/checklist/7-costs.md +309 -0
  35. forge/_extensions/skills/qa/resources/checklist/8-status-line.md +174 -0
  36. forge/_extensions/skills/qa/resources/checklist/9-direct-commands.md +146 -0
  37. forge/_extensions/skills/qa/resources/checklist.md +103 -0
  38. forge/_extensions/skills/qa/resources/report-template.md +62 -0
  39. forge/_extensions/skills/qa/scripts/start-container.sh +529 -0
  40. forge/_extensions/skills/qa/scripts/walkthrough-state.py +1137 -0
  41. forge/_extensions/skills/review/SKILL.md +125 -0
  42. forge/_extensions/skills/review/references/claude-4.6.md +474 -0
  43. forge/_extensions/skills/review/references/claude-4.7.md +710 -0
  44. forge/_extensions/skills/review/references/gemini-3.1.md +546 -0
  45. forge/_extensions/skills/review/references/gpt-5.5.md +490 -0
  46. forge/_extensions/skills/review/references/skills-writing-guide.md +1588 -0
  47. forge/_extensions/skills/review/resources/code-anthropic.md +160 -0
  48. forge/_extensions/skills/review/resources/code-gemini.md +184 -0
  49. forge/_extensions/skills/review/resources/code-openai.md +203 -0
  50. forge/_extensions/skills/review/resources/code.md +160 -0
  51. forge/_extensions/skills/review-docs/SKILL.md +121 -0
  52. forge/_extensions/skills/review-docs/resources/docs-anthropic.md +170 -0
  53. forge/_extensions/skills/review-docs/resources/docs-gemini.md +204 -0
  54. forge/_extensions/skills/review-docs/resources/docs-openai.md +231 -0
  55. forge/_extensions/skills/review-docs/resources/docs.md +170 -0
  56. forge/_extensions/skills/smoke-test/SKILL.md +27 -0
  57. forge/_extensions/skills/smoke-test/scripts/smoke-test.sh +118 -0
  58. forge/_extensions/skills/understand/SKILL.md +148 -0
  59. forge/_extensions/skills/understand/resources/code-anthropic.md +163 -0
  60. forge/_extensions/skills/understand/resources/code-gemini.md +194 -0
  61. forge/_extensions/skills/understand/resources/code-openai.md +181 -0
  62. forge/_extensions/skills/understand/resources/code.md +163 -0
  63. forge/_extensions/skills/understand/resources/docs-anthropic.md +177 -0
  64. forge/_extensions/skills/understand/resources/docs-gemini.md +202 -0
  65. forge/_extensions/skills/understand/resources/docs-openai.md +191 -0
  66. forge/_extensions/skills/understand/resources/docs.md +177 -0
  67. forge/_extensions/skills/walkthrough/SKILL.md +599 -0
  68. forge/_extensions/skills/walkthrough/resources/checklist.md +765 -0
  69. forge/_extensions/skills/walkthrough/scripts/run-in-repo.sh +118 -0
  70. forge/_extensions/skills/walkthrough/scripts/setup-test-repo.sh +198 -0
  71. forge/_extensions/skills/walkthrough/scripts/walkthrough-state.py +1137 -0
  72. forge/backend/__init__.py +174 -0
  73. forge/backend/adapters/__init__.py +38 -0
  74. forge/backend/adapters/litellm.py +158 -0
  75. forge/backend/creation.py +89 -0
  76. forge/backend/registry.py +178 -0
  77. forge/cli/__init__.py +16 -0
  78. forge/cli/auth.py +483 -0
  79. forge/cli/backend.py +298 -0
  80. forge/cli/claude.py +411 -0
  81. forge/cli/config_cmd.py +303 -0
  82. forge/cli/extensions.py +1001 -0
  83. forge/cli/gc.py +165 -0
  84. forge/cli/guard.py +1018 -0
  85. forge/cli/guards.py +106 -0
  86. forge/cli/handoff.py +110 -0
  87. forge/cli/hooks/__init__.py +36 -0
  88. forge/cli/hooks/_group.py +20 -0
  89. forge/cli/hooks/_helpers.py +149 -0
  90. forge/cli/hooks/commands.py +1677 -0
  91. forge/cli/hooks/direct_commands.py +1304 -0
  92. forge/cli/hooks/install.py +232 -0
  93. forge/cli/hooks/policy.py +151 -0
  94. forge/cli/hooks/read_hygiene.py +74 -0
  95. forge/cli/hooks/verification.py +370 -0
  96. forge/cli/logs.py +406 -0
  97. forge/cli/main.py +292 -0
  98. forge/cli/proxy.py +1821 -0
  99. forge/cli/proxy_costs.py +313 -0
  100. forge/cli/search.py +416 -0
  101. forge/cli/session.py +892 -0
  102. forge/cli/session_addendum.py +81 -0
  103. forge/cli/session_fork.py +750 -0
  104. forge/cli/session_handoff.py +141 -0
  105. forge/cli/session_lifecycle.py +2053 -0
  106. forge/cli/session_manage.py +1336 -0
  107. forge/cli/session_memory.py +201 -0
  108. forge/cli/status_line.py +1398 -0
  109. forge/cli/workflow.py +1964 -0
  110. forge/config/__init__.py +110 -0
  111. forge/config/dataclass_utils.py +88 -0
  112. forge/config/defaults/__init__.py +0 -0
  113. forge/config/defaults/backends/__init__.py +0 -0
  114. forge/config/defaults/backends/litellm.yaml +196 -0
  115. forge/config/defaults/templates/__init__.py +0 -0
  116. forge/config/defaults/templates/litellm-anthropic-local.yaml +33 -0
  117. forge/config/defaults/templates/litellm-anthropic.yaml +24 -0
  118. forge/config/defaults/templates/litellm-gemini-flash-local.yaml +37 -0
  119. forge/config/defaults/templates/litellm-gemini-local.yaml +32 -0
  120. forge/config/defaults/templates/litellm-gemini-test.yaml +34 -0
  121. forge/config/defaults/templates/litellm-gemini.yaml +21 -0
  122. forge/config/defaults/templates/litellm-openai-codex-local.yaml +36 -0
  123. forge/config/defaults/templates/litellm-openai-local.yaml +38 -0
  124. forge/config/defaults/templates/litellm-openai.yaml +28 -0
  125. forge/config/defaults/templates/openrouter-anthropic.yaml +23 -0
  126. forge/config/defaults/templates/openrouter-deepseek.yaml +26 -0
  127. forge/config/defaults/templates/openrouter-gemini-flash.yaml +26 -0
  128. forge/config/defaults/templates/openrouter-gemini.yaml +23 -0
  129. forge/config/defaults/templates/openrouter-glm.yaml +23 -0
  130. forge/config/defaults/templates/openrouter-kimi.yaml +30 -0
  131. forge/config/defaults/templates/openrouter-minimax.yaml +26 -0
  132. forge/config/defaults/templates/openrouter-openai-codex.yaml +23 -0
  133. forge/config/defaults/templates/openrouter-openai.yaml +28 -0
  134. forge/config/defaults/templates/openrouter-qwen.yaml +25 -0
  135. forge/config/loader.py +675 -0
  136. forge/config/schema.py +448 -0
  137. forge/core/__init__.py +5 -0
  138. forge/core/auth/__init__.py +67 -0
  139. forge/core/auth/capabilities.py +219 -0
  140. forge/core/auth/credentials_file.py +244 -0
  141. forge/core/auth/protocols.py +18 -0
  142. forge/core/auth/secrets.py +243 -0
  143. forge/core/auth/template_secrets.py +112 -0
  144. forge/core/data/__init__.py +5 -0
  145. forge/core/data/model_catalog.yaml +1522 -0
  146. forge/core/data/pricing.yaml +140 -0
  147. forge/core/data/system_prompt_addendums/__init__.py +0 -0
  148. forge/core/data/system_prompt_addendums/gemini.md +330 -0
  149. forge/core/data/system_prompt_addendums/openai.md +328 -0
  150. forge/core/llm/__init__.py +231 -0
  151. forge/core/llm/clients/__init__.py +14 -0
  152. forge/core/llm/clients/base.py +115 -0
  153. forge/core/llm/clients/litellm.py +619 -0
  154. forge/core/llm/clients/openai_compat.py +244 -0
  155. forge/core/llm/clients/openrouter.py +234 -0
  156. forge/core/llm/credentials.py +439 -0
  157. forge/core/llm/detection.py +86 -0
  158. forge/core/llm/errors.py +44 -0
  159. forge/core/llm/protocols.py +80 -0
  160. forge/core/llm/types.py +176 -0
  161. forge/core/logging.py +146 -0
  162. forge/core/models/__init__.py +91 -0
  163. forge/core/models/catalog.py +467 -0
  164. forge/core/models/pricing.py +165 -0
  165. forge/core/models/types.py +167 -0
  166. forge/core/naming.py +212 -0
  167. forge/core/ops/__init__.py +73 -0
  168. forge/core/ops/context.py +141 -0
  169. forge/core/ops/gc.py +802 -0
  170. forge/core/ops/proxy.py +146 -0
  171. forge/core/ops/resolution.py +135 -0
  172. forge/core/ops/session.py +344 -0
  173. forge/core/ops/session_context.py +548 -0
  174. forge/core/paths.py +38 -0
  175. forge/core/process.py +54 -0
  176. forge/core/reactive/__init__.py +38 -0
  177. forge/core/reactive/cost_tracking.py +300 -0
  178. forge/core/reactive/env.py +180 -0
  179. forge/core/reactive/proxy.py +78 -0
  180. forge/core/reactive/routing.py +622 -0
  181. forge/core/reactive/session_runner.py +185 -0
  182. forge/core/reactive/structured_output.py +62 -0
  183. forge/core/reactive/tagger.py +94 -0
  184. forge/core/reactive/throttle.py +132 -0
  185. forge/core/state/__init__.py +59 -0
  186. forge/core/state/exceptions.py +59 -0
  187. forge/core/state/io.py +140 -0
  188. forge/core/state/lock.py +99 -0
  189. forge/core/state/timestamps.py +60 -0
  190. forge/core/transcript.py +78 -0
  191. forge/core/typing_helpers.py +24 -0
  192. forge/core/workqueue/__init__.py +67 -0
  193. forge/core/workqueue/queue.py +552 -0
  194. forge/core/workqueue/types.py +63 -0
  195. forge/guard/__init__.py +26 -0
  196. forge/guard/deterministic/__init__.py +26 -0
  197. forge/guard/deterministic/base.py +158 -0
  198. forge/guard/deterministic/coding_standards.py +256 -0
  199. forge/guard/deterministic/registry.py +148 -0
  200. forge/guard/deterministic/tdd.py +171 -0
  201. forge/guard/engine.py +216 -0
  202. forge/guard/protocols.py +91 -0
  203. forge/guard/queries.py +96 -0
  204. forge/guard/semantic/__init__.py +34 -0
  205. forge/guard/semantic/promotion.py +18 -0
  206. forge/guard/semantic/supervisor.py +813 -0
  207. forge/guard/semantic/verdict.py +183 -0
  208. forge/guard/store.py +124 -0
  209. forge/guard/team/__init__.py +6 -0
  210. forge/guard/team/config.py +24 -0
  211. forge/guard/team/handlers.py +209 -0
  212. forge/guard/team/prompts.py +41 -0
  213. forge/guard/types.py +125 -0
  214. forge/guard/workflow/__init__.py +17 -0
  215. forge/guard/workflow/branches.py +67 -0
  216. forge/guard/workflow/config.py +63 -0
  217. forge/guard/workflow/divergence.py +113 -0
  218. forge/guard/workflow/policy.py +87 -0
  219. forge/guard/workflow/stages.py +205 -0
  220. forge/install/__init__.py +55 -0
  221. forge/install/cli.py +281 -0
  222. forge/install/exceptions.py +163 -0
  223. forge/install/hooks.py +109 -0
  224. forge/install/installer.py +1037 -0
  225. forge/install/models.py +321 -0
  226. forge/install/preset.py +272 -0
  227. forge/install/settings_merge.py +831 -0
  228. forge/install/tracking.py +238 -0
  229. forge/install/version.py +141 -0
  230. forge/proxy/__init__.py +0 -0
  231. forge/proxy/base_client.py +181 -0
  232. forge/proxy/client_adapter.py +476 -0
  233. forge/proxy/client_factory.py +531 -0
  234. forge/proxy/converters.py +1206 -0
  235. forge/proxy/cost_logger.py +132 -0
  236. forge/proxy/cost_tracker.py +242 -0
  237. forge/proxy/data_models.py +338 -0
  238. forge/proxy/error_hints.py +92 -0
  239. forge/proxy/metrics.py +222 -0
  240. forge/proxy/model_spec.py +158 -0
  241. forge/proxy/proxies.py +333 -0
  242. forge/proxy/proxy_identity.py +134 -0
  243. forge/proxy/proxy_orchestrator.py +1018 -0
  244. forge/proxy/proxy_startup.py +54 -0
  245. forge/proxy/server.py +1561 -0
  246. forge/proxy/utils.py +537 -0
  247. forge/review/__init__.py +6 -0
  248. forge/review/adversarial.py +111 -0
  249. forge/review/consensus.py +236 -0
  250. forge/review/engine.py +356 -0
  251. forge/review/models.py +437 -0
  252. forge/review/resources/__init__.py +5 -0
  253. forge/review/resources/codereview-performance.md +85 -0
  254. forge/review/resources/codereview-quick.md +75 -0
  255. forge/review/resources/codereview-security.md +92 -0
  256. forge/review/resources/codereview.md +85 -0
  257. forge/review/resources/docreview-quick.md +75 -0
  258. forge/review/resources/docreview.md +86 -0
  259. forge/review/resources/thinkdeep.md +89 -0
  260. forge/review/routing.py +368 -0
  261. forge/review/synthesis.py +73 -0
  262. forge/runtime_config.py +438 -0
  263. forge/search/__init__.py +55 -0
  264. forge/search/bm25_store.py +264 -0
  265. forge/search/content_store.py +197 -0
  266. forge/search/engine.py +352 -0
  267. forge/search/exceptions.py +51 -0
  268. forge/search/extractor.py +234 -0
  269. forge/search/index_state.py +295 -0
  270. forge/search/store.py +215 -0
  271. forge/search/tokenizer.py +24 -0
  272. forge/session/__init__.py +130 -0
  273. forge/session/active.py +339 -0
  274. forge/session/artifacts.py +202 -0
  275. forge/session/claude/__init__.py +50 -0
  276. forge/session/claude/cleanup.py +105 -0
  277. forge/session/claude/invoke.py +236 -0
  278. forge/session/claude/paths.py +200 -0
  279. forge/session/cleanup.py +216 -0
  280. forge/session/config.py +34 -0
  281. forge/session/direct_model.py +107 -0
  282. forge/session/effective.py +169 -0
  283. forge/session/exceptions.py +255 -0
  284. forge/session/handoff.py +881 -0
  285. forge/session/handoff_agent.py +544 -0
  286. forge/session/hooks/__init__.py +35 -0
  287. forge/session/hooks/models.py +73 -0
  288. forge/session/hooks/session_start.py +507 -0
  289. forge/session/identity.py +84 -0
  290. forge/session/index.py +553 -0
  291. forge/session/manager.py +1506 -0
  292. forge/session/models.py +572 -0
  293. forge/session/overrides.py +344 -0
  294. forge/session/plan_resolution.py +286 -0
  295. forge/session/prev_sessions.py +128 -0
  296. forge/session/store.py +431 -0
  297. forge/session/validation.py +47 -0
  298. forge/session/worktree/__init__.py +65 -0
  299. forge/session/worktree/cleanup.py +262 -0
  300. forge/session/worktree/config_copy.py +203 -0
  301. forge/session/worktree/create.py +332 -0
  302. forge/sidecar/__init__.py +29 -0
  303. forge/sidecar/container.py +161 -0
  304. forge/sidecar/docker.py +86 -0
  305. forge/sidecar/secrets.py +19 -0
  306. multi_forge-0.2.0.dist-info/METADATA +242 -0
  307. multi_forge-0.2.0.dist-info/RECORD +311 -0
  308. multi_forge-0.2.0.dist-info/WHEEL +4 -0
  309. multi_forge-0.2.0.dist-info/entry_points.txt +2 -0
  310. multi_forge-0.2.0.dist-info/licenses/LICENSE +203 -0
  311. multi_forge-0.2.0.dist-info/licenses/NOTICE +14 -0
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env bash
2
+ # Safety wrapper for Forge walkthrough commands.
3
+ # Sources env.sh, verifies isolation through 4 gates, cd's to test repo, runs the command.
4
+ #
5
+ # Usage:
6
+ # bash run-in-repo.sh forge session list # cd's to test repo automatically
7
+ # bash run-in-repo.sh jq '.' .claude/settings.json # relative paths work
8
+ # bash run-in-repo.sh --no-cd docker info # skip cd (maintainer-only)
9
+ #
10
+ # Exit codes:
11
+ # Command's exit code on success
12
+ # 1 on any gate failure
13
+
14
+ set -euo pipefail
15
+
16
+ # --- Parse --no-cd flag (maintainer-only: only for commands with no path arguments) ---
17
+ NO_CD=false
18
+ if [ "${1:-}" = "--no-cd" ]; then
19
+ NO_CD=true
20
+ shift
21
+ fi
22
+
23
+ if [ $# -eq 0 ]; then
24
+ echo "ERROR: No command specified." >&2
25
+ echo "Usage: bash run-in-repo.sh [--no-cd] <command...>" >&2
26
+ exit 1
27
+ fi
28
+
29
+ # --- Resolve FORGE_TEST_REPO ---
30
+ # Check for explicitly-set empty value before applying default
31
+ if [ "${FORGE_TEST_REPO+set}" = "set" ] && [ -z "$FORGE_TEST_REPO" ]; then
32
+ echo "ERROR: FORGE_TEST_REPO is explicitly set to empty. Refusing to proceed." >&2
33
+ exit 1
34
+ fi
35
+ FORGE_TEST_REPO="${FORGE_TEST_REPO:-${FORGE_HOME:-$HOME/.forge}/manual-testing/walkthrough/test-repo}"
36
+ FORGE_TEST_REPO="$(python3 -c 'import os,sys; print(os.path.abspath(os.path.expanduser(sys.argv[1])))' "$FORGE_TEST_REPO")"
37
+
38
+ # --- Denylist: refuse obviously dangerous values ---
39
+ check_safe_path() {
40
+ local resolved="$1"
41
+
42
+ if [ -z "$resolved" ]; then
43
+ echo "ERROR: FORGE_TEST_REPO is empty. Refusing to proceed." >&2
44
+ exit 1
45
+ fi
46
+
47
+ local -a denylist=("/" "$HOME" "/Users" "/tmp" "/var" "/etc" "/opt" "/usr")
48
+ for bad in "${denylist[@]}"; do
49
+ local bad_resolved
50
+ bad_resolved="$(realpath "$bad" 2>/dev/null || echo "$bad")"
51
+ if [ "$resolved" = "$bad" ] || [ "$resolved" = "$bad_resolved" ]; then
52
+ echo "ERROR: FORGE_TEST_REPO='$resolved' is a denylisted path. Refusing to proceed." >&2
53
+ echo " Set FORGE_TEST_REPO to a safe test directory (not $bad)." >&2
54
+ exit 1
55
+ fi
56
+ done
57
+ }
58
+
59
+ check_safe_path "$FORGE_TEST_REPO"
60
+
61
+ # --- Gate 1: env.sh exists ---
62
+ ENV_FILE="$FORGE_TEST_REPO/.forge/walkthrough/env.sh"
63
+ if [ ! -f "$ENV_FILE" ]; then
64
+ echo "ERROR: env.sh not found at: $ENV_FILE" >&2
65
+ echo "" >&2
66
+ echo " The test environment is missing. Likely causes:" >&2
67
+ echo " - Test repo was deleted (rm -rf $FORGE_TEST_REPO)" >&2
68
+ echo " - setup-test-repo.sh has not been run" >&2
69
+ echo "" >&2
70
+ echo " Fix: Run setup-test-repo.sh to recreate the test environment." >&2
71
+ exit 1
72
+ fi
73
+
74
+ # shellcheck source=/dev/null
75
+ source "$ENV_FILE"
76
+
77
+ # --- Gate 2: marker file exists ---
78
+ MARKER_FILE="$FORGE_TEST_REPO/.forge-walkthrough-marker"
79
+ if [ ! -f "$MARKER_FILE" ]; then
80
+ echo "ERROR: Marker file missing at: $MARKER_FILE" >&2
81
+ echo " This directory was not created by setup-test-repo.sh." >&2
82
+ echo " Refusing to run commands -- your real system may be at risk." >&2
83
+ exit 1
84
+ fi
85
+
86
+ # --- Gate 3: FORGE_HOME isolation ---
87
+ EXPECTED_FORGE_HOME="$FORGE_TEST_REPO/.forge-home"
88
+ if [ "${FORGE_HOME:-}" != "$EXPECTED_FORGE_HOME" ]; then
89
+ echo "ERROR: FORGE_HOME is not redirected to the test sandbox." >&2
90
+ echo " Expected: $EXPECTED_FORGE_HOME" >&2
91
+ echo " Actual: ${FORGE_HOME:-<unset>}" >&2
92
+ echo " Did you source env.sh?" >&2
93
+ exit 1
94
+ fi
95
+
96
+ # --- Gate 4: structure check ---
97
+ if [ ! -d "$FORGE_TEST_REPO/.forge/walkthrough" ]; then
98
+ echo "ERROR: Expected directory missing: $FORGE_TEST_REPO/.forge/walkthrough/" >&2
99
+ echo " The test repo structure is incomplete. Run setup-test-repo.sh." >&2
100
+ exit 1
101
+ fi
102
+
103
+ if [ ! -f "$FORGE_TEST_REPO/CLAUDE.md" ]; then
104
+ echo "ERROR: Expected file missing: $FORGE_TEST_REPO/CLAUDE.md" >&2
105
+ echo " This doesn't look like a forge walkthrough test repo." >&2
106
+ exit 1
107
+ fi
108
+
109
+ # --- cd to test repo (unless --no-cd) ---
110
+ if [ "$NO_CD" = false ]; then
111
+ cd "$FORGE_TEST_REPO" || {
112
+ echo "ERROR: Cannot cd to test repo: $FORGE_TEST_REPO" >&2
113
+ exit 1
114
+ }
115
+ fi
116
+
117
+ # --- Execute the command ---
118
+ exec "$@"
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env bash
2
+ # Setup script for Forge walkthrough test repo.
3
+ # Creates an isolated test workspace with hermetic FORGE_HOME.
4
+ #
5
+ # Usage:
6
+ # setup-test-repo.sh # Create test repo (default location)
7
+ # setup-test-repo.sh --reset # Reset existing repo to clean baseline
8
+ #
9
+ # Environment:
10
+ # FORGE_TEST_REPO Override test repo location (default: ~/.forge/manual-testing/walkthrough/test-repo)
11
+
12
+ set -euo pipefail
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
15
+
16
+ FORGE_TEST_REPO="${FORGE_TEST_REPO:-${FORGE_HOME:-$HOME/.forge}/manual-testing/walkthrough/test-repo}"
17
+ # Resolve to absolute path (and expand leading "~"), even if parent dirs don't exist yet.
18
+ FORGE_TEST_REPO="$(python3 -c 'import os,sys; print(os.path.abspath(os.path.expanduser(sys.argv[1])))' "$FORGE_TEST_REPO")"
19
+
20
+ MARKER_FILE="$FORGE_TEST_REPO/.forge-walkthrough-marker"
21
+
22
+ # --- Safety: refuse known-bad paths ---
23
+ check_safe_path() {
24
+ local resolved
25
+ resolved="$(realpath "$1" 2>/dev/null || echo "$1")"
26
+
27
+ local -a denylist=("/" "$HOME" "/Users" "/tmp" "/var" "/etc" "/opt" "/usr")
28
+ for bad in "${denylist[@]}"; do
29
+ if [ "$resolved" = "$bad" ] || [ "$resolved" = "$(realpath "$bad" 2>/dev/null || echo "$bad")" ]; then
30
+ echo "ERROR: Refusing to operate on '$resolved' (denylisted path)" >&2
31
+ exit 1
32
+ fi
33
+ done
34
+ }
35
+
36
+ check_safe_path "$FORGE_TEST_REPO"
37
+
38
+ # --- Generate env.sh (defined early, used by both --reset and fresh init) ---
39
+ generate_env() {
40
+ mkdir -p "$FORGE_TEST_REPO/.forge/walkthrough"
41
+ cat > "$FORGE_TEST_REPO/.forge/walkthrough/env.sh" << ENVEOF
42
+ # Generated by setup-test-repo.sh -- sandbox Forge state only.
43
+ # HOME stays real (Claude auth, native install, shell config all work).
44
+ # Only FORGE_HOME is redirected so forge CLI state is isolated.
45
+ export FORGE_TEST_REPO="$FORGE_TEST_REPO"
46
+ export FORGE_HOME="$FORGE_TEST_REPO/.forge-home"
47
+ export FORGE_DEBUG="1"
48
+ echo "Forge walkthrough sandbox active:" >&2
49
+ echo " FORGE_HOME = \$FORGE_HOME (isolated)" >&2
50
+ echo " HOME = \$HOME (unchanged)" >&2
51
+ echo " FORGE_DEBUG = \$FORGE_DEBUG (sandbox debug logging)" >&2
52
+ ENVEOF
53
+ }
54
+
55
+ # --- Remove walkthrough-derived state that should not persist across reruns ---
56
+ scrub_volatile_state() {
57
+ rm -rf "$FORGE_TEST_REPO/.forge/artifacts"
58
+ rm -rf "$FORGE_TEST_REPO/.forge/search-index"
59
+ rm -rf "$FORGE_TEST_REPO/.forge-home/logs"
60
+ }
61
+
62
+ # --- Handle --reset ---
63
+ if [ "${1:-}" = "--reset" ]; then
64
+ if [ -d "$FORGE_TEST_REPO" ] && [ -f "$MARKER_FILE" ]; then
65
+ # Marker exists: fast reset via git
66
+ if [ ! -f "$FORGE_TEST_REPO/src/main.py" ] || [ ! -f "$FORGE_TEST_REPO/CLAUDE.md" ]; then
67
+ echo "ERROR: Expected structure not found (src/main.py, CLAUDE.md)." >&2
68
+ echo "This does not look like a forge-walkthrough repo. Refusing --reset." >&2
69
+ exit 1
70
+ fi
71
+ echo "Resetting test repo: $FORGE_TEST_REPO" >&2
72
+ cd "$FORGE_TEST_REPO"
73
+ git clean -fdx -e .forge/walkthrough/ -e .forge-home/
74
+ git checkout -- .
75
+ mkdir -p .forge-home
76
+ mkdir -p .forge/walkthrough
77
+ scrub_volatile_state
78
+ echo "forge-walkthrough-marker" > "$MARKER_FILE"
79
+ generate_env
80
+ echo "Reset complete." >&2
81
+ exit 0
82
+ elif [ -d "$FORGE_TEST_REPO" ]; then
83
+ # No marker but dir exists: nuke and fall through to fresh init
84
+ echo "No marker file found. Removing stale directory and recreating." >&2
85
+ rm -rf "$FORGE_TEST_REPO"
86
+ fi
87
+ # Fall through to fresh init below
88
+ fi
89
+
90
+ # --- Idempotent: skip if already exists ---
91
+ if [ -d "$FORGE_TEST_REPO" ] && [ -f "$MARKER_FILE" ]; then
92
+ # Keep the repo baseline, but scrub walkthrough-derived artifacts so reruns start clean.
93
+ scrub_volatile_state
94
+ # Ensure env.sh exists and stays up to date (e.g., if skill path moved).
95
+ generate_env
96
+ echo "Test repo already exists: $FORGE_TEST_REPO" >&2
97
+ echo "Scrubbed volatile walkthrough state (.forge/artifacts, .forge/search-index, .forge-home/logs)." >&2
98
+ echo "Env file: $FORGE_TEST_REPO/.forge/walkthrough/env.sh" >&2
99
+ echo "Use --reset to clean and re-initialize." >&2
100
+ exit 0
101
+ fi
102
+
103
+ # --- Refuse to init into an existing directory without a marker ---
104
+ if [ -d "$FORGE_TEST_REPO" ] && [ ! -f "$MARKER_FILE" ]; then
105
+ echo "ERROR: Directory exists but has no marker: $FORGE_TEST_REPO" >&2
106
+ echo "This was not created by setup-test-repo.sh. Refusing to initialize." >&2
107
+ echo "Remove it manually or choose a different FORGE_TEST_REPO path." >&2
108
+ exit 1
109
+ fi
110
+
111
+ # --- Fresh init ---
112
+ echo "Creating test repo: $FORGE_TEST_REPO" >&2
113
+
114
+ mkdir -p "$FORGE_TEST_REPO"
115
+ cd "$FORGE_TEST_REPO"
116
+
117
+ # Isolated Forge state (only FORGE_HOME is sandboxed, not HOME)
118
+ mkdir -p .forge-home
119
+
120
+ # State/reports namespace
121
+ mkdir -p .forge/walkthrough
122
+
123
+ # Source tree
124
+ mkdir -p src tests .claude
125
+
126
+ # --- Write fixture files ---
127
+
128
+ cat > src/main.py << 'PYEOF'
129
+ def hello():
130
+ return "world"
131
+ PYEOF
132
+
133
+ cat > tests/test_main.py << 'PYEOF'
134
+ from src.main import hello
135
+
136
+
137
+ def test_hello():
138
+ assert hello() == "world"
139
+ PYEOF
140
+
141
+ cat > CLAUDE.md << 'PYEOF'
142
+ # forge-walkthrough
143
+
144
+ This is a test repo for the Forge walkthrough skill.
145
+ PYEOF
146
+
147
+ cat > README.md << 'PYEOF'
148
+ # forge-walkthrough
149
+
150
+ Test workspace for the Forge `/walkthrough` skill.
151
+ PYEOF
152
+
153
+ cat > .claude/settings.local.json << 'JSONEOF'
154
+ {
155
+ "permissions": {
156
+ "allow": [
157
+ "Bash(npm test)",
158
+ "Bash(uv run pytest*)"
159
+ ]
160
+ },
161
+ "env": {
162
+ "MY_CUSTOM_VAR": "should-survive-forge"
163
+ }
164
+ }
165
+ JSONEOF
166
+
167
+ cat > .gitignore << 'GITEOF'
168
+ .DS_Store
169
+ .idea/
170
+ .env
171
+ .forge-home/
172
+ .forge/
173
+ __pycache__/
174
+ *.pyc
175
+ GITEOF
176
+
177
+ # --- Git init ---
178
+ git init -q
179
+ git config user.email "forge-test@localhost"
180
+ git config user.name "Forge Test"
181
+ git config commit.gpgsign false
182
+ git add -A
183
+ # Force-add paths excluded by Claude Code's global gitignore (~/.config/git/ignore):
184
+ # **/.claude/settings.local.json, **/CLAUDE.local.md
185
+ for f in .claude/ CLAUDE.local.md; do
186
+ [ -e "$f" ] && git add -f "$f"
187
+ done
188
+ git commit -q -m "Initial test repo for forge walkthrough"
189
+
190
+ # --- Write marker (after commit so it's untracked = survives git checkout) ---
191
+ echo "forge-walkthrough-marker" > "$MARKER_FILE"
192
+
193
+ # --- Generate env.sh ---
194
+ generate_env
195
+
196
+ echo "Test repo created: $FORGE_TEST_REPO" >&2
197
+ echo "FORGE_HOME: $FORGE_TEST_REPO/.forge-home/" >&2
198
+ echo "Env file: $FORGE_TEST_REPO/.forge/walkthrough/env.sh" >&2