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.
- forge/__init__.py +3 -0
- forge/_extensions/agents/.gitkeep +0 -0
- forge/_extensions/commands/.gitkeep +0 -0
- forge/_extensions/skills/analyze/SKILL.md +87 -0
- forge/_extensions/skills/challenge/SKILL.md +91 -0
- forge/_extensions/skills/consensus/SKILL.md +120 -0
- forge/_extensions/skills/consensus/resources/code_consensus_evaluation.md +94 -0
- forge/_extensions/skills/consensus/resources/consensus_evaluation.md +70 -0
- forge/_extensions/skills/consensus/resources/synthesis.md +101 -0
- forge/_extensions/skills/debate/SKILL.md +116 -0
- forge/_extensions/skills/debate/resources/code_debate_evaluation.md +101 -0
- forge/_extensions/skills/debate/resources/debate_evaluation.md +90 -0
- forge/_extensions/skills/panel/SKILL.md +141 -0
- forge/_extensions/skills/panel/resources/synthesis.md +103 -0
- forge/_extensions/skills/qa/SKILL.md +704 -0
- forge/_extensions/skills/qa/resources/checklist/0-enable.md +78 -0
- forge/_extensions/skills/qa/resources/checklist/1-preflight.md +24 -0
- forge/_extensions/skills/qa/resources/checklist/10-resume.md +143 -0
- forge/_extensions/skills/qa/resources/checklist/11-config.md +150 -0
- forge/_extensions/skills/qa/resources/checklist/12-search.md +58 -0
- forge/_extensions/skills/qa/resources/checklist/13-guard.md +237 -0
- forge/_extensions/skills/qa/resources/checklist/14-workflow.md +305 -0
- forge/_extensions/skills/qa/resources/checklist/15-skills.md +155 -0
- forge/_extensions/skills/qa/resources/checklist/16-handoff.md +224 -0
- forge/_extensions/skills/qa/resources/checklist/17-info.md +50 -0
- forge/_extensions/skills/qa/resources/checklist/18-disable.md +84 -0
- forge/_extensions/skills/qa/resources/checklist/19-uninstall.md +146 -0
- forge/_extensions/skills/qa/resources/checklist/2-extensions.md +188 -0
- forge/_extensions/skills/qa/resources/checklist/20-cleanup.md +36 -0
- forge/_extensions/skills/qa/resources/checklist/3-auth.md +234 -0
- forge/_extensions/skills/qa/resources/checklist/4-proxy.md +481 -0
- forge/_extensions/skills/qa/resources/checklist/5-session.md +541 -0
- forge/_extensions/skills/qa/resources/checklist/6-hooks.md +275 -0
- forge/_extensions/skills/qa/resources/checklist/7-costs.md +309 -0
- forge/_extensions/skills/qa/resources/checklist/8-status-line.md +174 -0
- forge/_extensions/skills/qa/resources/checklist/9-direct-commands.md +146 -0
- forge/_extensions/skills/qa/resources/checklist.md +103 -0
- forge/_extensions/skills/qa/resources/report-template.md +62 -0
- forge/_extensions/skills/qa/scripts/start-container.sh +529 -0
- forge/_extensions/skills/qa/scripts/walkthrough-state.py +1137 -0
- forge/_extensions/skills/review/SKILL.md +125 -0
- forge/_extensions/skills/review/references/claude-4.6.md +474 -0
- forge/_extensions/skills/review/references/claude-4.7.md +710 -0
- forge/_extensions/skills/review/references/gemini-3.1.md +546 -0
- forge/_extensions/skills/review/references/gpt-5.5.md +490 -0
- forge/_extensions/skills/review/references/skills-writing-guide.md +1588 -0
- forge/_extensions/skills/review/resources/code-anthropic.md +160 -0
- forge/_extensions/skills/review/resources/code-gemini.md +184 -0
- forge/_extensions/skills/review/resources/code-openai.md +203 -0
- forge/_extensions/skills/review/resources/code.md +160 -0
- forge/_extensions/skills/review-docs/SKILL.md +121 -0
- forge/_extensions/skills/review-docs/resources/docs-anthropic.md +170 -0
- forge/_extensions/skills/review-docs/resources/docs-gemini.md +204 -0
- forge/_extensions/skills/review-docs/resources/docs-openai.md +231 -0
- forge/_extensions/skills/review-docs/resources/docs.md +170 -0
- forge/_extensions/skills/smoke-test/SKILL.md +27 -0
- forge/_extensions/skills/smoke-test/scripts/smoke-test.sh +118 -0
- forge/_extensions/skills/understand/SKILL.md +148 -0
- forge/_extensions/skills/understand/resources/code-anthropic.md +163 -0
- forge/_extensions/skills/understand/resources/code-gemini.md +194 -0
- forge/_extensions/skills/understand/resources/code-openai.md +181 -0
- forge/_extensions/skills/understand/resources/code.md +163 -0
- forge/_extensions/skills/understand/resources/docs-anthropic.md +177 -0
- forge/_extensions/skills/understand/resources/docs-gemini.md +202 -0
- forge/_extensions/skills/understand/resources/docs-openai.md +191 -0
- forge/_extensions/skills/understand/resources/docs.md +177 -0
- forge/_extensions/skills/walkthrough/SKILL.md +599 -0
- forge/_extensions/skills/walkthrough/resources/checklist.md +765 -0
- forge/_extensions/skills/walkthrough/scripts/run-in-repo.sh +118 -0
- forge/_extensions/skills/walkthrough/scripts/setup-test-repo.sh +198 -0
- forge/_extensions/skills/walkthrough/scripts/walkthrough-state.py +1137 -0
- forge/backend/__init__.py +174 -0
- forge/backend/adapters/__init__.py +38 -0
- forge/backend/adapters/litellm.py +158 -0
- forge/backend/creation.py +89 -0
- forge/backend/registry.py +178 -0
- forge/cli/__init__.py +16 -0
- forge/cli/auth.py +483 -0
- forge/cli/backend.py +298 -0
- forge/cli/claude.py +411 -0
- forge/cli/config_cmd.py +303 -0
- forge/cli/extensions.py +1001 -0
- forge/cli/gc.py +165 -0
- forge/cli/guard.py +1018 -0
- forge/cli/guards.py +106 -0
- forge/cli/handoff.py +110 -0
- forge/cli/hooks/__init__.py +36 -0
- forge/cli/hooks/_group.py +20 -0
- forge/cli/hooks/_helpers.py +149 -0
- forge/cli/hooks/commands.py +1677 -0
- forge/cli/hooks/direct_commands.py +1304 -0
- forge/cli/hooks/install.py +232 -0
- forge/cli/hooks/policy.py +151 -0
- forge/cli/hooks/read_hygiene.py +74 -0
- forge/cli/hooks/verification.py +370 -0
- forge/cli/logs.py +406 -0
- forge/cli/main.py +292 -0
- forge/cli/proxy.py +1821 -0
- forge/cli/proxy_costs.py +313 -0
- forge/cli/search.py +416 -0
- forge/cli/session.py +892 -0
- forge/cli/session_addendum.py +81 -0
- forge/cli/session_fork.py +750 -0
- forge/cli/session_handoff.py +141 -0
- forge/cli/session_lifecycle.py +2053 -0
- forge/cli/session_manage.py +1336 -0
- forge/cli/session_memory.py +201 -0
- forge/cli/status_line.py +1398 -0
- forge/cli/workflow.py +1964 -0
- forge/config/__init__.py +110 -0
- forge/config/dataclass_utils.py +88 -0
- forge/config/defaults/__init__.py +0 -0
- forge/config/defaults/backends/__init__.py +0 -0
- forge/config/defaults/backends/litellm.yaml +196 -0
- forge/config/defaults/templates/__init__.py +0 -0
- forge/config/defaults/templates/litellm-anthropic-local.yaml +33 -0
- forge/config/defaults/templates/litellm-anthropic.yaml +24 -0
- forge/config/defaults/templates/litellm-gemini-flash-local.yaml +37 -0
- forge/config/defaults/templates/litellm-gemini-local.yaml +32 -0
- forge/config/defaults/templates/litellm-gemini-test.yaml +34 -0
- forge/config/defaults/templates/litellm-gemini.yaml +21 -0
- forge/config/defaults/templates/litellm-openai-codex-local.yaml +36 -0
- forge/config/defaults/templates/litellm-openai-local.yaml +38 -0
- forge/config/defaults/templates/litellm-openai.yaml +28 -0
- forge/config/defaults/templates/openrouter-anthropic.yaml +23 -0
- forge/config/defaults/templates/openrouter-deepseek.yaml +26 -0
- forge/config/defaults/templates/openrouter-gemini-flash.yaml +26 -0
- forge/config/defaults/templates/openrouter-gemini.yaml +23 -0
- forge/config/defaults/templates/openrouter-glm.yaml +23 -0
- forge/config/defaults/templates/openrouter-kimi.yaml +30 -0
- forge/config/defaults/templates/openrouter-minimax.yaml +26 -0
- forge/config/defaults/templates/openrouter-openai-codex.yaml +23 -0
- forge/config/defaults/templates/openrouter-openai.yaml +28 -0
- forge/config/defaults/templates/openrouter-qwen.yaml +25 -0
- forge/config/loader.py +675 -0
- forge/config/schema.py +448 -0
- forge/core/__init__.py +5 -0
- forge/core/auth/__init__.py +67 -0
- forge/core/auth/capabilities.py +219 -0
- forge/core/auth/credentials_file.py +244 -0
- forge/core/auth/protocols.py +18 -0
- forge/core/auth/secrets.py +243 -0
- forge/core/auth/template_secrets.py +112 -0
- forge/core/data/__init__.py +5 -0
- forge/core/data/model_catalog.yaml +1522 -0
- forge/core/data/pricing.yaml +140 -0
- forge/core/data/system_prompt_addendums/__init__.py +0 -0
- forge/core/data/system_prompt_addendums/gemini.md +330 -0
- forge/core/data/system_prompt_addendums/openai.md +328 -0
- forge/core/llm/__init__.py +231 -0
- forge/core/llm/clients/__init__.py +14 -0
- forge/core/llm/clients/base.py +115 -0
- forge/core/llm/clients/litellm.py +619 -0
- forge/core/llm/clients/openai_compat.py +244 -0
- forge/core/llm/clients/openrouter.py +234 -0
- forge/core/llm/credentials.py +439 -0
- forge/core/llm/detection.py +86 -0
- forge/core/llm/errors.py +44 -0
- forge/core/llm/protocols.py +80 -0
- forge/core/llm/types.py +176 -0
- forge/core/logging.py +146 -0
- forge/core/models/__init__.py +91 -0
- forge/core/models/catalog.py +467 -0
- forge/core/models/pricing.py +165 -0
- forge/core/models/types.py +167 -0
- forge/core/naming.py +212 -0
- forge/core/ops/__init__.py +73 -0
- forge/core/ops/context.py +141 -0
- forge/core/ops/gc.py +802 -0
- forge/core/ops/proxy.py +146 -0
- forge/core/ops/resolution.py +135 -0
- forge/core/ops/session.py +344 -0
- forge/core/ops/session_context.py +548 -0
- forge/core/paths.py +38 -0
- forge/core/process.py +54 -0
- forge/core/reactive/__init__.py +38 -0
- forge/core/reactive/cost_tracking.py +300 -0
- forge/core/reactive/env.py +180 -0
- forge/core/reactive/proxy.py +78 -0
- forge/core/reactive/routing.py +622 -0
- forge/core/reactive/session_runner.py +185 -0
- forge/core/reactive/structured_output.py +62 -0
- forge/core/reactive/tagger.py +94 -0
- forge/core/reactive/throttle.py +132 -0
- forge/core/state/__init__.py +59 -0
- forge/core/state/exceptions.py +59 -0
- forge/core/state/io.py +140 -0
- forge/core/state/lock.py +99 -0
- forge/core/state/timestamps.py +60 -0
- forge/core/transcript.py +78 -0
- forge/core/typing_helpers.py +24 -0
- forge/core/workqueue/__init__.py +67 -0
- forge/core/workqueue/queue.py +552 -0
- forge/core/workqueue/types.py +63 -0
- forge/guard/__init__.py +26 -0
- forge/guard/deterministic/__init__.py +26 -0
- forge/guard/deterministic/base.py +158 -0
- forge/guard/deterministic/coding_standards.py +256 -0
- forge/guard/deterministic/registry.py +148 -0
- forge/guard/deterministic/tdd.py +171 -0
- forge/guard/engine.py +216 -0
- forge/guard/protocols.py +91 -0
- forge/guard/queries.py +96 -0
- forge/guard/semantic/__init__.py +34 -0
- forge/guard/semantic/promotion.py +18 -0
- forge/guard/semantic/supervisor.py +813 -0
- forge/guard/semantic/verdict.py +183 -0
- forge/guard/store.py +124 -0
- forge/guard/team/__init__.py +6 -0
- forge/guard/team/config.py +24 -0
- forge/guard/team/handlers.py +209 -0
- forge/guard/team/prompts.py +41 -0
- forge/guard/types.py +125 -0
- forge/guard/workflow/__init__.py +17 -0
- forge/guard/workflow/branches.py +67 -0
- forge/guard/workflow/config.py +63 -0
- forge/guard/workflow/divergence.py +113 -0
- forge/guard/workflow/policy.py +87 -0
- forge/guard/workflow/stages.py +205 -0
- forge/install/__init__.py +55 -0
- forge/install/cli.py +281 -0
- forge/install/exceptions.py +163 -0
- forge/install/hooks.py +109 -0
- forge/install/installer.py +1037 -0
- forge/install/models.py +321 -0
- forge/install/preset.py +272 -0
- forge/install/settings_merge.py +831 -0
- forge/install/tracking.py +238 -0
- forge/install/version.py +141 -0
- forge/proxy/__init__.py +0 -0
- forge/proxy/base_client.py +181 -0
- forge/proxy/client_adapter.py +476 -0
- forge/proxy/client_factory.py +531 -0
- forge/proxy/converters.py +1206 -0
- forge/proxy/cost_logger.py +132 -0
- forge/proxy/cost_tracker.py +242 -0
- forge/proxy/data_models.py +338 -0
- forge/proxy/error_hints.py +92 -0
- forge/proxy/metrics.py +222 -0
- forge/proxy/model_spec.py +158 -0
- forge/proxy/proxies.py +333 -0
- forge/proxy/proxy_identity.py +134 -0
- forge/proxy/proxy_orchestrator.py +1018 -0
- forge/proxy/proxy_startup.py +54 -0
- forge/proxy/server.py +1561 -0
- forge/proxy/utils.py +537 -0
- forge/review/__init__.py +6 -0
- forge/review/adversarial.py +111 -0
- forge/review/consensus.py +236 -0
- forge/review/engine.py +356 -0
- forge/review/models.py +437 -0
- forge/review/resources/__init__.py +5 -0
- forge/review/resources/codereview-performance.md +85 -0
- forge/review/resources/codereview-quick.md +75 -0
- forge/review/resources/codereview-security.md +92 -0
- forge/review/resources/codereview.md +85 -0
- forge/review/resources/docreview-quick.md +75 -0
- forge/review/resources/docreview.md +86 -0
- forge/review/resources/thinkdeep.md +89 -0
- forge/review/routing.py +368 -0
- forge/review/synthesis.py +73 -0
- forge/runtime_config.py +438 -0
- forge/search/__init__.py +55 -0
- forge/search/bm25_store.py +264 -0
- forge/search/content_store.py +197 -0
- forge/search/engine.py +352 -0
- forge/search/exceptions.py +51 -0
- forge/search/extractor.py +234 -0
- forge/search/index_state.py +295 -0
- forge/search/store.py +215 -0
- forge/search/tokenizer.py +24 -0
- forge/session/__init__.py +130 -0
- forge/session/active.py +339 -0
- forge/session/artifacts.py +202 -0
- forge/session/claude/__init__.py +50 -0
- forge/session/claude/cleanup.py +105 -0
- forge/session/claude/invoke.py +236 -0
- forge/session/claude/paths.py +200 -0
- forge/session/cleanup.py +216 -0
- forge/session/config.py +34 -0
- forge/session/direct_model.py +107 -0
- forge/session/effective.py +169 -0
- forge/session/exceptions.py +255 -0
- forge/session/handoff.py +881 -0
- forge/session/handoff_agent.py +544 -0
- forge/session/hooks/__init__.py +35 -0
- forge/session/hooks/models.py +73 -0
- forge/session/hooks/session_start.py +507 -0
- forge/session/identity.py +84 -0
- forge/session/index.py +553 -0
- forge/session/manager.py +1506 -0
- forge/session/models.py +572 -0
- forge/session/overrides.py +344 -0
- forge/session/plan_resolution.py +286 -0
- forge/session/prev_sessions.py +128 -0
- forge/session/store.py +431 -0
- forge/session/validation.py +47 -0
- forge/session/worktree/__init__.py +65 -0
- forge/session/worktree/cleanup.py +262 -0
- forge/session/worktree/config_copy.py +203 -0
- forge/session/worktree/create.py +332 -0
- forge/sidecar/__init__.py +29 -0
- forge/sidecar/container.py +161 -0
- forge/sidecar/docker.py +86 -0
- forge/sidecar/secrets.py +19 -0
- multi_forge-0.2.0.dist-info/METADATA +242 -0
- multi_forge-0.2.0.dist-info/RECORD +311 -0
- multi_forge-0.2.0.dist-info/WHEEL +4 -0
- multi_forge-0.2.0.dist-info/entry_points.txt +2 -0
- multi_forge-0.2.0.dist-info/licenses/LICENSE +203 -0
- 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"
|