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,529 @@
1
+ #!/usr/bin/env bash
2
+ # Start or reuse a Docker container for full QA mode.
3
+ #
4
+ # Usage:
5
+ # bash start-container.sh # Start or reuse container
6
+ # bash start-container.sh --provider-profile remote-litellm # Use legacy remote LiteLLM QA profile
7
+ # bash start-container.sh --reset # Kill container, remove image, rebuild and start
8
+ # bash start-container.sh --stop # Stop and remove container
9
+ # bash start-container.sh --status # Check container status
10
+ #
11
+ # Outputs container name to stdout on success.
12
+ # Exit codes: 0=ready, 1=no docker, 2=build failed, 3=start failed
13
+
14
+ set -euo pipefail
15
+
16
+ CONTAINER_NAME="forge-qa"
17
+ PROVIDER_PROFILE="openrouter"
18
+ RESET=false
19
+ ACTION="start"
20
+
21
+ # --- Resolve repo root and image tag ---
22
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
23
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd -P)"
24
+
25
+ # Detect Claude Code version from installed binary
26
+ if command -v claude &>/dev/null; then
27
+ CLAUDE_VERSION="$(claude --version 2>/dev/null | awk '{print $1}')"
28
+ fi
29
+ CLAUDE_VERSION="${CLAUDE_VERSION:-latest}"
30
+ IMAGE_NAME="forge-claude-test:${CLAUDE_VERSION}"
31
+
32
+ # --- Helper functions ---
33
+ error() { echo "ERROR: $*" >&2; }
34
+ info() { echo "INFO: $*" >&2; }
35
+
36
+ usage() {
37
+ cat >&2 <<'EOF'
38
+ Usage: start-container.sh [--provider-profile openrouter|remote-litellm] [--reset|--stop|--status]
39
+ EOF
40
+ }
41
+
42
+ while [[ $# -gt 0 ]]; do
43
+ case "$1" in
44
+ --provider-profile)
45
+ if [[ -z "${2:-}" ]]; then
46
+ error "--provider-profile requires a value: openrouter or remote-litellm"
47
+ usage
48
+ exit 1
49
+ fi
50
+ PROVIDER_PROFILE="$2"
51
+ shift 2
52
+ ;;
53
+ --provider-profile=*)
54
+ PROVIDER_PROFILE="${1#--provider-profile=}"
55
+ shift
56
+ ;;
57
+ --reset)
58
+ RESET=true
59
+ shift
60
+ ;;
61
+ --stop)
62
+ ACTION="stop"
63
+ shift
64
+ ;;
65
+ --status)
66
+ ACTION="status"
67
+ shift
68
+ ;;
69
+ -h|--help)
70
+ usage
71
+ exit 0
72
+ ;;
73
+ *)
74
+ error "Unknown argument: $1"
75
+ usage
76
+ exit 1
77
+ ;;
78
+ esac
79
+ done
80
+
81
+ case "$PROVIDER_PROFILE" in
82
+ openrouter)
83
+ FORGE_QA_OPENAI_TEMPLATE="openrouter-openai"
84
+ FORGE_QA_GEMINI_TEMPLATE="openrouter-gemini"
85
+ FORGE_QA_ANTHROPIC_TEMPLATE="openrouter-anthropic"
86
+ : "${FORGE_QA_WORKFLOW_MODELS:=deepseek-v4-pro,minimax-m2.7}"
87
+ : "${FORGE_QA_WORKFLOW_MODEL_A:=deepseek-v4-pro}"
88
+ : "${FORGE_QA_WORKFLOW_MODEL_B:=minimax-m2.7}"
89
+ FORGE_QA_DEEPSEEK_TEMPLATE="openrouter-deepseek"
90
+ FORGE_QA_MINIMAX_TEMPLATE="openrouter-minimax"
91
+ ;;
92
+ remote-litellm)
93
+ FORGE_QA_OPENAI_TEMPLATE="litellm-openai"
94
+ FORGE_QA_GEMINI_TEMPLATE="litellm-gemini"
95
+ FORGE_QA_ANTHROPIC_TEMPLATE="litellm-anthropic"
96
+ : "${FORGE_QA_WORKFLOW_MODELS:=gpt-5.5,gemini-3.1-pro-preview}"
97
+ : "${FORGE_QA_WORKFLOW_MODEL_A:=gpt-5.5}"
98
+ : "${FORGE_QA_WORKFLOW_MODEL_B:=gemini-3.1-pro-preview}"
99
+ FORGE_QA_DEEPSEEK_TEMPLATE=""
100
+ FORGE_QA_MINIMAX_TEMPLATE=""
101
+ ;;
102
+ *)
103
+ error "Invalid --provider-profile '$PROVIDER_PROFILE' (expected: openrouter or remote-litellm)"
104
+ exit 1
105
+ ;;
106
+ esac
107
+
108
+ FORGE_QA_PROVIDER_PROFILE="$PROVIDER_PROFILE"
109
+ FORGE_QA_OPENAI_PROXY="qa-openai"
110
+ FORGE_QA_GEMINI_PROXY="qa-gemini"
111
+ FORGE_QA_ANTHROPIC_PROXY="qa-anthropic"
112
+
113
+ load_env_var() {
114
+ local var="$1"
115
+ if [[ -z "${!var:-}" && -f "$REPO_ROOT/.env" ]]; then
116
+ local val
117
+ val="$(grep "^${var}=" "$REPO_ROOT/.env" 2>/dev/null | tail -n 1 | cut -d= -f2- || true)"
118
+ val="${val%\"}" ; val="${val#\"}"
119
+ val="${val%\'}" ; val="${val#\'}"
120
+ if [[ -n "$val" ]]; then
121
+ printf -v "$var" '%s' "$val"
122
+ export "$var"
123
+ fi
124
+ fi
125
+ }
126
+
127
+ load_qa_env() {
128
+ local var
129
+ for var in \
130
+ GEMINI_API_KEY \
131
+ ANTHROPIC_API_KEY \
132
+ LITELLM_API_KEY \
133
+ LITELLM_BASE_URL \
134
+ OPENAI_API_KEY \
135
+ OPENROUTER_API_KEY \
136
+ OPENROUTER_BASE_URL; do
137
+ load_env_var "$var"
138
+ done
139
+ }
140
+
141
+ validate_provider_profile() {
142
+ load_qa_env
143
+
144
+ case "$PROVIDER_PROFILE" in
145
+ openrouter)
146
+ if [[ -z "${OPENROUTER_API_KEY:-}" ]]; then
147
+ error "QA provider profile 'openrouter' requires OPENROUTER_API_KEY."
148
+ error "Set it in your environment or repo .env, or use --provider-profile remote-litellm."
149
+ exit 1
150
+ fi
151
+ ;;
152
+ remote-litellm)
153
+ if [[ -z "${LITELLM_API_KEY:-}" || -z "${LITELLM_BASE_URL:-}" ]]; then
154
+ error "QA provider profile 'remote-litellm' requires LITELLM_API_KEY and LITELLM_BASE_URL."
155
+ error "Set both in your environment or repo .env, or use the default OpenRouter profile."
156
+ exit 1
157
+ fi
158
+ ;;
159
+ esac
160
+ }
161
+
162
+ validate_running_container_profile() {
163
+ case "$PROVIDER_PROFILE" in
164
+ openrouter)
165
+ if ! docker exec "$CONTAINER_NAME" sh -c 'test -n "${OPENROUTER_API_KEY:-}"' >/dev/null 2>&1; then
166
+ error "Running QA container for profile 'openrouter' is missing OPENROUTER_API_KEY."
167
+ error "Run 'bash start-container.sh --stop' and restart it with OPENROUTER_API_KEY set."
168
+ exit 3
169
+ fi
170
+ ;;
171
+ remote-litellm)
172
+ if ! docker exec "$CONTAINER_NAME" sh -c \
173
+ 'test -n "${LITELLM_API_KEY:-}" && test -n "${LITELLM_BASE_URL:-}"' >/dev/null 2>&1; then
174
+ error "Running QA container for profile 'remote-litellm' is missing LITELLM_API_KEY or LITELLM_BASE_URL."
175
+ error "Run 'bash start-container.sh --stop' and restart it with both variables set."
176
+ exit 3
177
+ fi
178
+ ;;
179
+ esac
180
+ }
181
+
182
+ docker_env_args() {
183
+ local args=(
184
+ -e "PATH=/forge/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
185
+ -e "FORGE_HOME=/root/.forge"
186
+ -e "CLAUDE_HOME=/root/.claude"
187
+ -e "FORGE_TEST_REPO=/workspace"
188
+ -e "FORGE_DEBUG=1"
189
+ -e "FORGE_QA_PROVIDER_PROFILE=$FORGE_QA_PROVIDER_PROFILE"
190
+ -e "FORGE_QA_OPENAI_TEMPLATE=$FORGE_QA_OPENAI_TEMPLATE"
191
+ -e "FORGE_QA_GEMINI_TEMPLATE=$FORGE_QA_GEMINI_TEMPLATE"
192
+ -e "FORGE_QA_ANTHROPIC_TEMPLATE=$FORGE_QA_ANTHROPIC_TEMPLATE"
193
+ -e "FORGE_QA_OPENAI_PROXY=$FORGE_QA_OPENAI_PROXY"
194
+ -e "FORGE_QA_GEMINI_PROXY=$FORGE_QA_GEMINI_PROXY"
195
+ -e "FORGE_QA_ANTHROPIC_PROXY=$FORGE_QA_ANTHROPIC_PROXY"
196
+ -e "FORGE_QA_WORKFLOW_MODELS=$FORGE_QA_WORKFLOW_MODELS"
197
+ -e "FORGE_QA_WORKFLOW_MODEL_A=$FORGE_QA_WORKFLOW_MODEL_A"
198
+ -e "FORGE_QA_WORKFLOW_MODEL_B=$FORGE_QA_WORKFLOW_MODEL_B"
199
+ -e "FORGE_QA_DEEPSEEK_TEMPLATE=${FORGE_QA_DEEPSEEK_TEMPLATE:-}"
200
+ -e "FORGE_QA_MINIMAX_TEMPLATE=${FORGE_QA_MINIMAX_TEMPLATE:-}"
201
+ )
202
+
203
+ local var
204
+ for var in \
205
+ GEMINI_API_KEY \
206
+ ANTHROPIC_API_KEY \
207
+ LITELLM_API_KEY \
208
+ LITELLM_BASE_URL \
209
+ OPENAI_API_KEY \
210
+ OPENROUTER_API_KEY \
211
+ OPENROUTER_BASE_URL; do
212
+ if [[ -n "${!var:-}" ]]; then
213
+ args+=(-e "$var=${!var}")
214
+ fi
215
+ done
216
+
217
+ printf '%s\n' "${args[@]}"
218
+ }
219
+
220
+ # --- Host state dir (mounted into container) ---
221
+ HOST_STATE_DIR_RAW="${FORGE_HOME:-$HOME/.forge}/manual-testing/qa"
222
+ HOST_STATE_DIR="$(python3 -c 'import os,sys; print(os.path.abspath(os.path.expanduser(os.path.expandvars(sys.argv[1]))))' "$HOST_STATE_DIR_RAW")"
223
+ mkdir -p "$HOST_STATE_DIR"
224
+
225
+ # --- Docker availability check ---
226
+ if ! command -v docker &> /dev/null; then
227
+ error "Docker command not found. Install Docker: https://docs.docker.com/get-docker/"
228
+ exit 1
229
+ fi
230
+
231
+ if ! docker info &> /dev/null; then
232
+ error "Docker daemon is not running. Start Docker Desktop and try again."
233
+ exit 1
234
+ fi
235
+
236
+ # --- Handle --stop ---
237
+ if [[ "$ACTION" == "stop" ]]; then
238
+ if docker ps -q -f "name=^${CONTAINER_NAME}$" | grep -q .; then
239
+ info "Stopping and removing container: $CONTAINER_NAME"
240
+ docker stop "$CONTAINER_NAME" > /dev/null 2>&1 || true
241
+ docker rm "$CONTAINER_NAME" > /dev/null 2>&1 || true
242
+ info "Container removed."
243
+ else
244
+ info "No running container named $CONTAINER_NAME."
245
+ docker rm "$CONTAINER_NAME" > /dev/null 2>&1 || true
246
+ fi
247
+ exit 0
248
+ fi
249
+
250
+ # --- Handle --status ---
251
+ if [[ "$ACTION" == "status" ]]; then
252
+ if docker ps -q -f "name=^${CONTAINER_NAME}$" | grep -q .; then
253
+ info "Container $CONTAINER_NAME is running."
254
+ forge_ver="$(docker exec "$CONTAINER_NAME" bash -lc 'cd /forge && uv run python -c "import forge; print(getattr(forge, \"__version__\", \"unknown\"))"' 2>/dev/null || echo "unknown")"
255
+ info "Forge: $forge_ver"
256
+ profile="$(docker exec "$CONTAINER_NAME" sh -c 'printf "%s" "${FORGE_QA_PROVIDER_PROFILE:-unknown}"' 2>/dev/null || echo "unknown")"
257
+ info "QA provider profile: $profile"
258
+ exit 0
259
+ elif docker ps -aq -f "name=^${CONTAINER_NAME}$" | grep -q .; then
260
+ info "Container $CONTAINER_NAME exists but is stopped."
261
+ exit 1
262
+ else
263
+ info "No container named $CONTAINER_NAME."
264
+ exit 1
265
+ fi
266
+ fi
267
+
268
+ # --- Reuse if already running ---
269
+ if [[ "$RESET" != "true" ]] && docker ps -q -f "name=^${CONTAINER_NAME}$" | grep -q .; then
270
+ existing_profile="$(docker exec "$CONTAINER_NAME" sh -c 'printf "%s" "${FORGE_QA_PROVIDER_PROFILE:-}"' 2>/dev/null || true)"
271
+ if [[ "$existing_profile" != "$FORGE_QA_PROVIDER_PROFILE" ]]; then
272
+ error "Running container '$CONTAINER_NAME' was created with provider profile '${existing_profile:-unknown}', not '$FORGE_QA_PROVIDER_PROFILE'."
273
+ error "Run 'bash start-container.sh --stop' or rerun QA with --reset before switching provider profiles."
274
+ exit 3
275
+ fi
276
+ for wf_var in FORGE_QA_WORKFLOW_MODELS FORGE_QA_WORKFLOW_MODEL_A FORGE_QA_WORKFLOW_MODEL_B; do
277
+ wf_expected="${!wf_var}"
278
+ wf_actual="$(docker exec "$CONTAINER_NAME" sh -c "printf '%s' \"\${${wf_var}:-}\"" 2>/dev/null || true)"
279
+ if [[ "$wf_actual" != "$wf_expected" ]]; then
280
+ error "Running container '$CONTAINER_NAME' has $wf_var='${wf_actual:-<unset>}', expected '$wf_expected'."
281
+ error "Run 'bash start-container.sh --stop' then restart, or rerun QA with --reset."
282
+ exit 3
283
+ fi
284
+ done
285
+ validate_running_container_profile
286
+ info "Reusing running container: $CONTAINER_NAME"
287
+ echo "$CONTAINER_NAME"
288
+ exit 0
289
+ fi
290
+
291
+ validate_provider_profile
292
+
293
+ # --- Handle --reset (kill container + remove image, then fall through to rebuild) ---
294
+ if [[ "$RESET" == "true" ]]; then
295
+ info "Rebuild: removing container and image..."
296
+ docker stop "$CONTAINER_NAME" > /dev/null 2>&1 || true
297
+ docker rm "$CONTAINER_NAME" > /dev/null 2>&1 || true
298
+ docker rmi "$IMAGE_NAME" > /dev/null 2>&1 || true
299
+ info "Cleaned up. Rebuilding from scratch..."
300
+ fi
301
+
302
+ # --- Remove stopped container with same name ---
303
+ if docker ps -aq -f "name=^${CONTAINER_NAME}$" | grep -q .; then
304
+ info "Removing stopped container: $CONTAINER_NAME"
305
+ docker rm "$CONTAINER_NAME" > /dev/null 2>&1 || true
306
+ fi
307
+
308
+ DOCKERFILE="$REPO_ROOT/docker/Dockerfile.forge"
309
+
310
+ # --- Image staleness detection (reuse pattern from scripts/test-integration.sh) ---
311
+ get_forge_rev() {
312
+ if command -v git &>/dev/null && git -C "$REPO_ROOT" rev-parse --is-inside-work-tree &>/dev/null; then
313
+ local rev
314
+ rev="$(git -C "$REPO_ROOT" rev-parse HEAD)"
315
+ if [[ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]]; then
316
+ echo "${rev}-dirty"
317
+ else
318
+ echo "${rev}"
319
+ fi
320
+ return 0
321
+ fi
322
+ echo "unknown"
323
+ }
324
+
325
+ FORGE_REV="$(get_forge_rev)"
326
+
327
+ needs_build=false
328
+ if ! docker image inspect "$IMAGE_NAME" &> /dev/null; then
329
+ needs_build=true
330
+ info "Image $IMAGE_NAME not found. Building..."
331
+ else
332
+ image_rev="$(docker image inspect -f '{{ index .Config.Labels "org.opencontainers.image.revision" }}' "$IMAGE_NAME")" || {
333
+ info "Failed to read image revision label; forcing rebuild."
334
+ image_rev=""
335
+ }
336
+ if [[ -z "${image_rev}" || "${image_rev}" != "${FORGE_REV}" ]]; then
337
+ needs_build=true
338
+ info "Image stale (image=${image_rev:-<missing>}, repo=${FORGE_REV}). Rebuilding..."
339
+ fi
340
+ fi
341
+
342
+ if [[ "$needs_build" == "true" ]]; then
343
+ if [[ ! -f "$DOCKERFILE" ]]; then
344
+ if docker image inspect "$IMAGE_NAME" &> /dev/null; then
345
+ info "Source repo not available ($DOCKERFILE missing). Using existing image: $IMAGE_NAME"
346
+ needs_build=false
347
+ else
348
+ error "Dockerfile not found at $DOCKERFILE"
349
+ error "Source repo is required to build the QA image."
350
+ error "Fix: run from the Forge source repo or install it so docker/Dockerfile.forge is available."
351
+ exit 2
352
+ fi
353
+ fi
354
+
355
+ if [[ "$needs_build" == "true" ]]; then
356
+ info "Building Docker image (this may take a few minutes)..."
357
+ if ! docker build \
358
+ -f "$DOCKERFILE" \
359
+ --build-arg "CLAUDE_VERSION=$CLAUDE_VERSION" \
360
+ --build-arg "FORGE_REV=$FORGE_REV" \
361
+ -t "$IMAGE_NAME" \
362
+ "$REPO_ROOT"; then
363
+ error "Docker build failed."
364
+ exit 2
365
+ fi
366
+ info "Build complete: $IMAGE_NAME"
367
+ fi
368
+ fi
369
+
370
+ # --- Start container ---
371
+ info "Starting container: $CONTAINER_NAME"
372
+ DOCKER_ENV=()
373
+ while IFS= read -r docker_env_arg; do
374
+ DOCKER_ENV+=("$docker_env_arg")
375
+ done < <(docker_env_args)
376
+ if ! docker run -d \
377
+ --name "$CONTAINER_NAME" \
378
+ "${DOCKER_ENV[@]}" \
379
+ -v "$HOST_STATE_DIR:/workspace/.forge/qa" \
380
+ -w /workspace \
381
+ "$IMAGE_NAME" \
382
+ tail -f /dev/null > /dev/null; then
383
+ error "Failed to start container."
384
+ exit 3
385
+ fi
386
+
387
+ # --- Remove leaked .env before any forge imports ---
388
+ # load_dotenv() in cli/main.py:16 fires at import time. If /forge/.env survived
389
+ # from a stale image (built before .dockerignore excluded it), it contaminates
390
+ # all forge commands. Remove before the "Forge importable" preflight check.
391
+ docker exec "$CONTAINER_NAME" bash -c 'rm -f /forge/.env /forge/.env.*'
392
+
393
+ # --- Preflight inside container ---
394
+ info "Running preflight checks..."
395
+
396
+ # Install jq (many checklist items use it)
397
+ docker exec "$CONTAINER_NAME" bash -c 'apt-get update -qq && apt-get install -y -qq jq > /dev/null 2>&1' || {
398
+ error "Failed to install jq in container."
399
+ exit 3
400
+ }
401
+
402
+ # Set a profile for interactive debugging shells. Checklist execution relies on
403
+ # docker run -e above so plain docker exec calls see the same values.
404
+ {
405
+ echo 'export PATH="/forge/.venv/bin:$PATH"'
406
+ echo 'export FORGE_HOME="/root/.forge"'
407
+ echo 'export CLAUDE_HOME="/root/.claude"'
408
+ echo 'export FORGE_TEST_REPO="/workspace"'
409
+ # QA defaults to debug logging so every Forge command leaves evidence.
410
+ echo 'export FORGE_DEBUG="1"'
411
+ for var in \
412
+ FORGE_QA_PROVIDER_PROFILE \
413
+ FORGE_QA_OPENAI_TEMPLATE \
414
+ FORGE_QA_GEMINI_TEMPLATE \
415
+ FORGE_QA_ANTHROPIC_TEMPLATE \
416
+ FORGE_QA_OPENAI_PROXY \
417
+ FORGE_QA_GEMINI_PROXY \
418
+ FORGE_QA_ANTHROPIC_PROXY \
419
+ FORGE_QA_WORKFLOW_MODELS \
420
+ FORGE_QA_WORKFLOW_MODEL_A \
421
+ FORGE_QA_WORKFLOW_MODEL_B \
422
+ FORGE_QA_DEEPSEEK_TEMPLATE \
423
+ FORGE_QA_MINIMAX_TEMPLATE \
424
+ GEMINI_API_KEY \
425
+ ANTHROPIC_API_KEY \
426
+ LITELLM_API_KEY \
427
+ LITELLM_BASE_URL \
428
+ OPENAI_API_KEY \
429
+ OPENROUTER_API_KEY \
430
+ OPENROUTER_BASE_URL; do
431
+ if [[ -n "${!var:-}" ]]; then
432
+ printf 'export %s=%q\n' "$var" "${!var}"
433
+ fi
434
+ done
435
+ } | docker exec -i "$CONTAINER_NAME" bash -c 'cat > /etc/profile.d/forge-qa.sh && chmod 600 /etc/profile.d/forge-qa.sh' || {
436
+ error "Failed to write /etc/profile.d/forge-qa.sh"
437
+ exit 3
438
+ }
439
+
440
+ docker exec "$CONTAINER_NAME" bash -lc 'test -x /forge/.venv/bin/forge' || {
441
+ error "forge not found at /forge/.venv/bin/forge"
442
+ exit 3
443
+ }
444
+
445
+ # Configure Claude Code auth for container environment.
446
+ # ANTHROPIC_API_KEY from the env profile (set above) is the sole auth mechanism.
447
+ # hasCompletedOnboarding skips the first-run screen.
448
+ # settings.json starts empty; `forge extension enable` (section 2) merges hooks into it.
449
+ # See: github.com/anthropics/claude-code/issues/9699
450
+ docker exec "$CONTAINER_NAME" bash -c 'mkdir -p /root/.claude'
451
+
452
+ docker exec -i "$CONTAINER_NAME" bash -c 'cat > /root/.claude/settings.json && chmod 600 /root/.claude/settings.json' <<'SETTINGSEOF'
453
+ {}
454
+ SETTINGSEOF
455
+
456
+ docker exec -i "$CONTAINER_NAME" bash -c 'cat > /root/.claude.json && chmod 600 /root/.claude.json' <<'ONBOARDEOF'
457
+ {"hasCompletedOnboarding":true}
458
+ ONBOARDEOF
459
+
460
+ # Verify Forge is importable
461
+ docker exec "$CONTAINER_NAME" bash -lc 'cd /forge && uv run python -c "import forge.cli.main"' || {
462
+ error "Forge is not importable in container."
463
+ exit 3
464
+ }
465
+
466
+ # --- Initialize workspace ---
467
+ docker exec "$CONTAINER_NAME" bash -c '
468
+ mkdir -p /workspace/src /workspace/tests /workspace/.claude /workspace/.forge/qa /workspace/.forge/qa/logs
469
+ cd /workspace
470
+
471
+ cat > src/main.py << "PYEOF"
472
+ def hello():
473
+ return "world"
474
+ PYEOF
475
+
476
+ cat > tests/test_main.py << "PYEOF"
477
+ from src.main import hello
478
+
479
+ def test_hello():
480
+ assert hello() == "world"
481
+ PYEOF
482
+
483
+ cat > CLAUDE.md << "PYEOF"
484
+ # forge-walkthrough
485
+ This is a test repo for the Forge walkthrough skill.
486
+ PYEOF
487
+
488
+ cat > README.md << "PYEOF"
489
+ # forge-walkthrough
490
+ Test workspace for the Forge walkthrough skill.
491
+ PYEOF
492
+
493
+ cat > .claude/settings.local.json << "JSONEOF"
494
+ {
495
+ "permissions": {
496
+ "allow": [
497
+ "Bash(npm test)",
498
+ "Bash(uv run pytest*)"
499
+ ]
500
+ },
501
+ "env": {
502
+ "MY_CUSTOM_VAR": "should-survive-forge"
503
+ }
504
+ }
505
+ JSONEOF
506
+
507
+ cat > .gitignore << "GITEOF"
508
+ .DS_Store
509
+ .idea/
510
+ .env
511
+ .test-home/
512
+ .forge/
513
+ __pycache__/
514
+ *.pyc
515
+ GITEOF
516
+
517
+ git init -q -b main
518
+ git config user.email "forge-qa@localhost"
519
+ git config user.name "Forge QA"
520
+ git config commit.gpgsign false
521
+ git add -A
522
+ git commit -q -m "Initial test repo for forge walkthrough --full"
523
+ ' || {
524
+ error "Failed to initialize workspace in container."
525
+ exit 3
526
+ }
527
+
528
+ info "Container ready: $CONTAINER_NAME (image: $IMAGE_NAME)"
529
+ echo "$CONTAINER_NAME"