forge-workflow 0.0.4 → 0.0.6
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.
- package/.claude/commands/dev.md +345 -340
- package/.claude/commands/plan.md +566 -521
- package/.claude/commands/premerge.md +186 -176
- package/.claude/commands/research.md +42 -42
- package/.claude/commands/review.md +448 -442
- package/.claude/commands/rollback.md +721 -721
- package/.claude/commands/ship.md +212 -164
- package/.claude/commands/sonarcloud.md +152 -152
- package/.claude/commands/status.md +90 -48
- package/.claude/commands/validate.md +288 -282
- package/.claude/commands/verify.md +269 -221
- package/.claude/rules/greptile-review-process.md +285 -285
- package/.claude/rules/workflow.md +121 -105
- package/.claude/scripts/greptile-resolve.sh +558 -526
- package/.claude/scripts/load-env.sh +32 -32
- package/.cline/workflows/dev.md +342 -337
- package/.cline/workflows/plan.md +563 -518
- package/.cline/workflows/premerge.md +183 -173
- package/.cline/workflows/research.md +39 -39
- package/.cline/workflows/review.md +445 -439
- package/.cline/workflows/rollback.md +718 -718
- package/.cline/workflows/ship.md +209 -161
- package/.cline/workflows/sonarcloud.md +146 -146
- package/.cline/workflows/status.md +87 -45
- package/.cline/workflows/validate.md +285 -279
- package/.cline/workflows/verify.md +266 -218
- package/.codex/config.toml +11 -11
- package/.codex/skills/dev/SKILL.md +345 -340
- package/.codex/skills/plan/SKILL.md +566 -521
- package/.codex/skills/premerge/SKILL.md +186 -176
- package/.codex/skills/research/SKILL.md +42 -42
- package/.codex/skills/review/SKILL.md +448 -442
- package/.codex/skills/rollback/SKILL.md +721 -721
- package/.codex/skills/ship/SKILL.md +212 -164
- package/.codex/skills/sonarcloud/SKILL.md +149 -149
- package/.codex/skills/status/SKILL.md +90 -48
- package/.codex/skills/validate/SKILL.md +288 -282
- package/.codex/skills/verify/SKILL.md +269 -221
- package/.cursor/commands/dev.md +342 -337
- package/.cursor/commands/plan.md +563 -518
- package/.cursor/commands/premerge.md +183 -173
- package/.cursor/commands/research.md +39 -39
- package/.cursor/commands/review.md +445 -439
- package/.cursor/commands/rollback.md +718 -718
- package/.cursor/commands/ship.md +209 -161
- package/.cursor/commands/sonarcloud.md +146 -146
- package/.cursor/commands/status.md +87 -45
- package/.cursor/commands/validate.md +285 -279
- package/.cursor/commands/verify.md +266 -218
- package/.cursor/rules/permissions-guidance.mdc +37 -37
- package/.forge/hooks/check-tdd.js +240 -240
- package/.github/PLUGIN_TEMPLATE.json +32 -32
- package/.github/prompts/dev.prompt.md +347 -342
- package/.github/prompts/plan.prompt.md +568 -523
- package/.github/prompts/premerge.prompt.md +188 -178
- package/.github/prompts/research.prompt.md +44 -44
- package/.github/prompts/review.prompt.md +450 -444
- package/.github/prompts/rollback.prompt.md +723 -723
- package/.github/prompts/ship.prompt.md +214 -166
- package/.github/prompts/sonarcloud.prompt.md +151 -151
- package/.github/prompts/status.prompt.md +92 -50
- package/.github/prompts/validate.prompt.md +290 -284
- package/.github/prompts/verify.prompt.md +271 -223
- package/.github/workflows/beads-to-github.yml +56 -0
- package/.github/workflows/github-to-beads.yml +97 -0
- package/.kilocode/workflows/dev.md +346 -341
- package/.kilocode/workflows/plan.md +567 -522
- package/.kilocode/workflows/premerge.md +187 -177
- package/.kilocode/workflows/research.md +43 -43
- package/.kilocode/workflows/review.md +449 -443
- package/.kilocode/workflows/rollback.md +722 -722
- package/.kilocode/workflows/ship.md +213 -165
- package/.kilocode/workflows/sonarcloud.md +150 -150
- package/.kilocode/workflows/status.md +91 -49
- package/.kilocode/workflows/validate.md +289 -283
- package/.kilocode/workflows/verify.md +270 -222
- package/.mcp.json.example +12 -12
- package/.opencode/commands/dev.md +345 -340
- package/.opencode/commands/plan.md +566 -521
- package/.opencode/commands/premerge.md +186 -176
- package/.opencode/commands/research.md +42 -42
- package/.opencode/commands/review.md +448 -442
- package/.opencode/commands/rollback.md +721 -721
- package/.opencode/commands/ship.md +212 -164
- package/.opencode/commands/sonarcloud.md +149 -149
- package/.opencode/commands/status.md +90 -48
- package/.opencode/commands/validate.md +288 -282
- package/.opencode/commands/verify.md +269 -221
- package/.roo/commands/dev.md +346 -341
- package/.roo/commands/plan.md +567 -522
- package/.roo/commands/premerge.md +187 -177
- package/.roo/commands/research.md +43 -43
- package/.roo/commands/review.md +449 -443
- package/.roo/commands/rollback.md +722 -722
- package/.roo/commands/ship.md +213 -165
- package/.roo/commands/sonarcloud.md +150 -150
- package/.roo/commands/status.md +91 -49
- package/.roo/commands/validate.md +289 -283
- package/.roo/commands/verify.md +270 -222
- package/AGENTS.md +272 -175
- package/CLAUDE.md +110 -100
- package/README.md +429 -416
- package/bin/forge-cmd.js +317 -313
- package/bin/forge-preflight.js +322 -309
- package/bin/forge.js +4765 -4303
- package/docs/AGENT_INSTALL_PROMPT.md +342 -342
- package/docs/BEADS_GITHUB_SYNC.md +251 -251
- package/docs/ENHANCED_ONBOARDING.md +612 -602
- package/docs/EXAMPLES.md +482 -482
- package/docs/GREPTILE_SETUP.md +400 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
- package/docs/ROADMAP.md +359 -359
- package/docs/SETUP.md +663 -631
- package/docs/TOOLCHAIN.md +653 -630
- package/docs/VALIDATION.md +363 -363
- package/install.sh +40 -1056
- package/lefthook.yml +50 -39
- package/lib/agents/README.md +198 -198
- package/lib/agents/claude.plugin.json +28 -28
- package/lib/agents/cline.plugin.json +22 -22
- package/lib/agents/codex.plugin.json +19 -19
- package/lib/agents/copilot.plugin.json +24 -24
- package/lib/agents/cursor.plugin.json +25 -25
- package/lib/agents/kilocode.plugin.json +22 -22
- package/lib/agents/opencode.plugin.json +20 -20
- package/lib/agents/roo.plugin.json +23 -23
- package/lib/agents-config.js +2112 -2112
- package/lib/beads-health-check.js +143 -0
- package/lib/beads-setup.js +341 -0
- package/lib/beads-sync-scaffold.js +260 -0
- package/lib/commands/_registry.js +134 -0
- package/lib/commands/clean.js +181 -0
- package/lib/commands/dev.js +571 -513
- package/lib/commands/plan.js +692 -692
- package/lib/commands/push.js +196 -0
- package/lib/commands/recommend.js +119 -119
- package/lib/commands/ship.js +377 -377
- package/lib/commands/status.js +378 -378
- package/lib/commands/sync.js +55 -0
- package/lib/commands/team.js +37 -0
- package/lib/commands/test.js +207 -0
- package/lib/commands/validate.js +602 -602
- package/lib/commands/worktree.js +310 -0
- package/lib/context-merge.js +359 -359
- package/lib/dep-guard/analyzer.js +294 -294
- package/lib/dep-guard/behavior-detector.js +98 -98
- package/lib/dep-guard/contract-detector.js +162 -162
- package/lib/dep-guard/import-detector.js +498 -498
- package/lib/dep-guard/path-utils.js +13 -13
- package/lib/dep-guard/rubric.js +120 -120
- package/lib/dep-guard/task-parser.js +318 -318
- package/lib/detect-agent.js +191 -191
- package/lib/detect-worktree.js +47 -47
- package/lib/docs-command.js +51 -0
- package/lib/docs-copy.js +50 -0
- package/lib/file-hash.js +26 -26
- package/lib/freshness-token.js +148 -0
- package/lib/greptile-match.js +80 -0
- package/lib/husky-migration.js +450 -0
- package/lib/lefthook-check.js +65 -0
- package/lib/pat-setup.js +207 -0
- package/lib/plugin-catalog.js +350 -350
- package/lib/plugin-manager.js +166 -166
- package/lib/plugin-recommender.js +141 -141
- package/lib/project-discovery.js +491 -491
- package/lib/reset.js +309 -0
- package/lib/setup-action-log.js +139 -139
- package/lib/setup-summary-renderer.js +106 -106
- package/lib/setup-utils.js +96 -0
- package/lib/setup.js +192 -192
- package/lib/smart-merge.js +64 -0
- package/lib/symlink-utils.js +81 -0
- package/lib/task-ownership.js +117 -0
- package/lib/workflow-profiles.js +197 -197
- package/package.json +131 -128
- package/scripts/beads-context.sh +426 -0
- package/scripts/beads-context.test.js +567 -0
- package/scripts/behavioral-judge.sh +378 -0
- package/scripts/benchmark.js +85 -0
- package/scripts/branch-protection.js +183 -0
- package/scripts/check-agents.js +172 -0
- package/scripts/check-forge-token.js +98 -0
- package/scripts/commitlint.js +42 -0
- package/scripts/conflict-detect.sh +323 -0
- package/scripts/dep-guard-analyze.js +71 -0
- package/scripts/dep-guard.sh +789 -0
- package/scripts/eval_win.py +249 -0
- package/scripts/file-index.sh +493 -0
- package/scripts/forge-team/index.sh +86 -0
- package/scripts/forge-team/lib/agent-prompt.sh +52 -0
- package/scripts/forge-team/lib/claim.sh +256 -0
- package/scripts/forge-team/lib/dashboard.sh +341 -0
- package/scripts/forge-team/lib/epic.sh +332 -0
- package/scripts/forge-team/lib/hooks.sh +253 -0
- package/scripts/forge-team/lib/identity.sh +235 -0
- package/scripts/forge-team/lib/sync-github.sh +317 -0
- package/scripts/forge-team/lib/verify.sh +284 -0
- package/scripts/forge-team/lib/workload.sh +296 -0
- package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
- package/scripts/forge-team/tests/claim.test.sh +179 -0
- package/scripts/forge-team/tests/dashboard.test.sh +170 -0
- package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
- package/scripts/forge-team/tests/epic.test.sh +176 -0
- package/scripts/forge-team/tests/hooks.test.sh +239 -0
- package/scripts/forge-team/tests/identity.test.sh +176 -0
- package/scripts/forge-team/tests/integration.test.sh +371 -0
- package/scripts/forge-team/tests/sync-github.test.sh +209 -0
- package/scripts/forge-team/tests/verify.test.sh +314 -0
- package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
- package/scripts/forge-team/tests/workload.test.sh +209 -0
- package/scripts/github-beads-sync/comment.mjs +64 -0
- package/scripts/github-beads-sync/config.mjs +148 -0
- package/scripts/github-beads-sync/github-api.mjs +131 -0
- package/scripts/github-beads-sync/index.mjs +332 -0
- package/scripts/github-beads-sync/label-mapper.mjs +54 -0
- package/scripts/github-beads-sync/mapping.mjs +78 -0
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
- package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
- package/scripts/github-beads-sync/run-bd.mjs +159 -0
- package/scripts/github-beads-sync/sanitize.mjs +121 -0
- package/scripts/github-beads-sync.config.json +26 -0
- package/scripts/improve-command.js +375 -0
- package/scripts/lib/eval-runner.js +268 -0
- package/scripts/lib/eval-schema.js +135 -0
- package/scripts/lib/eval-storage.js +78 -0
- package/scripts/lib/grading.js +203 -0
- package/scripts/lib/jsonl-lock.sh +48 -0
- package/scripts/lib/sanitize.sh +116 -0
- package/scripts/lib/transcript-parser.js +63 -0
- package/scripts/lint.js +47 -0
- package/scripts/migrate-to-bun-test.js +412 -0
- package/scripts/pr-coordinator.sh +706 -0
- package/scripts/run-command-eval.js +236 -0
- package/scripts/smart-status.sh +809 -0
- package/scripts/sync-commands.js +571 -0
- package/scripts/sync-utils.sh +455 -0
- package/scripts/test-dashboard.js +123 -0
- package/scripts/test.js +46 -0
- package/scripts/validate.sh +94 -0
- package/skills/parallel-deep-research/SKILL.md +108 -108
- package/skills/parallel-deep-research/evals/README.md +27 -27
- package/skills/parallel-deep-research/evals/evals.json +62 -62
- package/skills/sonarcloud-analysis/SKILL.md +171 -171
- package/skills/sonarcloud-analysis/evals/README.md +27 -27
- package/skills/sonarcloud-analysis/evals/evals.json +50 -50
- package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
- package/.cursor/hooks/state/continual-learning-index.json +0 -19
- package/.cursor/hooks/state/continual-learning.json +0 -8
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# behavioral-judge.sh — Score /plan output using OpenRouter LLM judges
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# echo '{"plan_output": "..."}' | bash scripts/behavioral-judge.sh
|
|
6
|
+
# bash scripts/behavioral-judge.sh '{"plan_output": "..."}'
|
|
7
|
+
#
|
|
8
|
+
# Environment:
|
|
9
|
+
# OPENROUTER_API_KEY — Required (unless test mode)
|
|
10
|
+
# BEHAVIORAL_JUDGE_TEST_MODE — Set to 1 to skip real HTTP calls
|
|
11
|
+
# BEHAVIORAL_JUDGE_MOCK_SCORES — JSON scores for test mode, e.g. '{"security":4,"tdd":5,"design":4,"structural":3}'
|
|
12
|
+
# BEHAVIORAL_JUDGE_MOCK_PRIMARY_FAIL — Set to 1 to simulate GLM-5 failure
|
|
13
|
+
# BEHAVIORAL_JUDGE_MOCK_SECONDARY_FAIL — Set to 1 to simulate MiniMax failure
|
|
14
|
+
# BEHAVIORAL_JUDGE_MOCK_TERTIARY_FAIL — Set to 1 to simulate Kimi failure
|
|
15
|
+
#
|
|
16
|
+
# Output (stdout): JSON scoring result
|
|
17
|
+
# Scores are NOT written to stderr — only diagnostic messages go there
|
|
18
|
+
|
|
19
|
+
set -e
|
|
20
|
+
|
|
21
|
+
# ─── Subcommand: check-lock-sync ─────────────────────────────────────────────
|
|
22
|
+
# Usage: bash behavioral-judge.sh check-lock-sync <workflow.md>
|
|
23
|
+
# Compares engine.model in .md frontmatter against the compiled .lock.yml.
|
|
24
|
+
# Outputs: LOCK_IN_SYNC or LOCK_OUT_OF_SYNC (exit 0 in both cases)
|
|
25
|
+
if [ "$1" = "check-lock-sync" ]; then
|
|
26
|
+
MD_FILE="${2:-}"
|
|
27
|
+
if [ -z "$MD_FILE" ] || [ ! -f "$MD_FILE" ]; then
|
|
28
|
+
echo "LOCK_OUT_OF_SYNC"
|
|
29
|
+
>&2 echo '{"result":"LOCK_OUT_OF_SYNC","reason":"md_file_not_found"}'
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
LOCK_FILE="${MD_FILE%.md}.lock.yml"
|
|
33
|
+
if [ ! -f "$LOCK_FILE" ]; then
|
|
34
|
+
echo "LOCK_OUT_OF_SYNC"
|
|
35
|
+
exit 0
|
|
36
|
+
fi
|
|
37
|
+
MD_MODEL=$(grep -A2 'engine:' "$MD_FILE" | grep 'model:' | awk '{print $2}' | tr -d '"' | head -1)
|
|
38
|
+
LOCK_MODEL=$(grep -A2 'engine:' "$LOCK_FILE" | grep 'model:' | awk '{print $2}' | tr -d '"' | head -1)
|
|
39
|
+
if [ "$MD_MODEL" = "$LOCK_MODEL" ]; then
|
|
40
|
+
echo "LOCK_IN_SYNC"
|
|
41
|
+
else
|
|
42
|
+
echo "LOCK_OUT_OF_SYNC"
|
|
43
|
+
fi
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
OPENROUTER_BASE_URL="https://openrouter.ai/api/v1/chat/completions"
|
|
48
|
+
MODEL_PRIMARY="z-ai/glm-5"
|
|
49
|
+
MODEL_SECONDARY="minimax/minimax-m2.5"
|
|
50
|
+
MODEL_TERTIARY="moonshotai/kimi-k2.5"
|
|
51
|
+
|
|
52
|
+
# Weighted scoring: security×3, tdd×3, design×2, structural×1 — max 45
|
|
53
|
+
WEIGHT_SECURITY=3
|
|
54
|
+
WEIGHT_TDD=3
|
|
55
|
+
WEIGHT_DESIGN=2
|
|
56
|
+
WEIGHT_STRUCTURAL=1
|
|
57
|
+
|
|
58
|
+
# Classification thresholds
|
|
59
|
+
THRESHOLD_PASS=36
|
|
60
|
+
THRESHOLD_WEAK=27
|
|
61
|
+
|
|
62
|
+
# Read input from stdin or $1
|
|
63
|
+
if [ -n "$1" ]; then
|
|
64
|
+
INPUT="$1"
|
|
65
|
+
else
|
|
66
|
+
INPUT="$(cat)"
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Extract plan_output text if input is JSON, otherwise use raw input
|
|
70
|
+
PLAN_TEXT="$INPUT"
|
|
71
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
72
|
+
EXTRACTED=$(echo "$INPUT" | python3 -c "
|
|
73
|
+
import sys, json
|
|
74
|
+
try:
|
|
75
|
+
d = json.load(sys.stdin)
|
|
76
|
+
val = d.get('plan_output', '')
|
|
77
|
+
if val:
|
|
78
|
+
print(val)
|
|
79
|
+
except:
|
|
80
|
+
pass
|
|
81
|
+
" 2>/dev/null) || true
|
|
82
|
+
if [ -n "$EXTRACTED" ]; then
|
|
83
|
+
PLAN_TEXT="$EXTRACTED"
|
|
84
|
+
fi
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# ─── Judge prompt ────────────────────────────────────────────────────────────
|
|
88
|
+
build_prompt() {
|
|
89
|
+
local plan_output="$1"
|
|
90
|
+
cat <<PROMPT
|
|
91
|
+
You are a strict quality judge for an AI development workflow. Score the following /plan execution output.
|
|
92
|
+
|
|
93
|
+
Context: The forge /plan command has 3 phases:
|
|
94
|
+
- Phase 1: Design Intent (Q&A → design doc with success criteria, edge cases, ambiguity policy)
|
|
95
|
+
- Phase 2: Technical Research (OWASP analysis, TDD scenarios, library docs)
|
|
96
|
+
- Phase 3: Setup (branch, worktree, Beads issue, task list with TDD steps)
|
|
97
|
+
|
|
98
|
+
Scoring criteria (score each 0-5, where 5=excellent, 0=absent/unacceptable):
|
|
99
|
+
|
|
100
|
+
SECURITY (weight ×3): Does Phase 2 include OWASP Top 10 analysis? Are security test scenarios identified? Is injection risk addressed for external API calls?
|
|
101
|
+
|
|
102
|
+
TDD (weight ×3): Does Phase 3 task list have RED-GREEN-REFACTOR steps? Do >50% of tasks have explicit failing test step? Are test file paths specified?
|
|
103
|
+
|
|
104
|
+
DESIGN (weight ×2): Does Phase 1 produce a design doc with: success criteria, edge cases, out-of-scope, ambiguity policy? Is it specific (not generic)?
|
|
105
|
+
|
|
106
|
+
STRUCTURAL (weight ×1): Does Phase 3 include branch creation, worktree setup, Beads issue creation? Is a baseline test run included?
|
|
107
|
+
|
|
108
|
+
Output ONLY valid JSON with no explanation:
|
|
109
|
+
{"security": N, "tdd": N, "design": N, "structural": N}
|
|
110
|
+
|
|
111
|
+
Plan output to score:
|
|
112
|
+
${plan_output}
|
|
113
|
+
PROMPT
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# ─── Compute weighted scores and classification ───────────────────────────────
|
|
117
|
+
compute_result() {
|
|
118
|
+
local security="$1"
|
|
119
|
+
local tdd="$2"
|
|
120
|
+
local design="$3"
|
|
121
|
+
local structural="$4"
|
|
122
|
+
local judge_model="$5"
|
|
123
|
+
local judge_calls="$6"
|
|
124
|
+
|
|
125
|
+
local sec_weighted=$(( security * WEIGHT_SECURITY ))
|
|
126
|
+
local tdd_weighted=$(( tdd * WEIGHT_TDD ))
|
|
127
|
+
local des_weighted=$(( design * WEIGHT_DESIGN ))
|
|
128
|
+
local str_weighted=$(( structural * WEIGHT_STRUCTURAL ))
|
|
129
|
+
local total=$(( sec_weighted + tdd_weighted + des_weighted + str_weighted ))
|
|
130
|
+
|
|
131
|
+
local result
|
|
132
|
+
if [ "$total" -ge "$THRESHOLD_PASS" ]; then
|
|
133
|
+
result="PASS"
|
|
134
|
+
elif [ "$total" -ge "$THRESHOLD_WEAK" ]; then
|
|
135
|
+
result="WEAK"
|
|
136
|
+
else
|
|
137
|
+
result="FAIL"
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
cat <<JSON
|
|
141
|
+
{
|
|
142
|
+
"result": "${result}",
|
|
143
|
+
"total": ${total},
|
|
144
|
+
"dimensions": {
|
|
145
|
+
"security": {"raw": ${security}, "weighted": ${sec_weighted}},
|
|
146
|
+
"tdd": {"raw": ${tdd}, "weighted": ${tdd_weighted}},
|
|
147
|
+
"design": {"raw": ${design}, "weighted": ${des_weighted}},
|
|
148
|
+
"structural": {"raw": ${structural}, "weighted": ${str_weighted}}
|
|
149
|
+
},
|
|
150
|
+
"judge_model": "${judge_model}",
|
|
151
|
+
"judge_calls": ${judge_calls}
|
|
152
|
+
}
|
|
153
|
+
JSON
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# ─── Parse scores from LLM JSON response ─────────────────────────────────────
|
|
157
|
+
parse_scores() {
|
|
158
|
+
local response="$1"
|
|
159
|
+
# Extract the JSON object from the LLM response content
|
|
160
|
+
local content
|
|
161
|
+
content=$(echo "$response" | python3 -c "
|
|
162
|
+
import sys, json
|
|
163
|
+
try:
|
|
164
|
+
data = json.load(sys.stdin)
|
|
165
|
+
choices = data.get('choices', [])
|
|
166
|
+
if choices:
|
|
167
|
+
msg = choices[0].get('message', {})
|
|
168
|
+
content = msg.get('content', '')
|
|
169
|
+
print(content)
|
|
170
|
+
except Exception as e:
|
|
171
|
+
pass
|
|
172
|
+
" 2>/dev/null) || true
|
|
173
|
+
|
|
174
|
+
if [ -z "$content" ]; then
|
|
175
|
+
echo ""
|
|
176
|
+
return 1
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
# Parse scores from content JSON
|
|
180
|
+
local scores
|
|
181
|
+
scores=$(echo "$content" | python3 -c "
|
|
182
|
+
import sys, json, re
|
|
183
|
+
text = sys.stdin.read().strip()
|
|
184
|
+
# Try direct JSON parse first
|
|
185
|
+
try:
|
|
186
|
+
d = json.loads(text)
|
|
187
|
+
s = int(d.get('security', -1))
|
|
188
|
+
t = int(d.get('tdd', -1))
|
|
189
|
+
de = int(d.get('design', -1))
|
|
190
|
+
st = int(d.get('structural', -1))
|
|
191
|
+
if all(0 <= x <= 5 for x in [s, t, de, st]):
|
|
192
|
+
print(f'{s} {t} {de} {st}')
|
|
193
|
+
sys.exit(0)
|
|
194
|
+
except:
|
|
195
|
+
pass
|
|
196
|
+
# Try extracting JSON from surrounding text
|
|
197
|
+
m = re.search(r'\{[^}]+\}', text, re.DOTALL)
|
|
198
|
+
if m:
|
|
199
|
+
try:
|
|
200
|
+
d = json.loads(m.group(0))
|
|
201
|
+
s = int(d.get('security', -1))
|
|
202
|
+
t = int(d.get('tdd', -1))
|
|
203
|
+
de = int(d.get('design', -1))
|
|
204
|
+
st = int(d.get('structural', -1))
|
|
205
|
+
if all(0 <= x <= 5 for x in [s, t, de, st]):
|
|
206
|
+
print(f'{s} {t} {de} {st}')
|
|
207
|
+
sys.exit(0)
|
|
208
|
+
except:
|
|
209
|
+
pass
|
|
210
|
+
sys.exit(1)
|
|
211
|
+
" 2>/dev/null) || { echo ""; return 1; }
|
|
212
|
+
|
|
213
|
+
echo "$scores"
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# ─── Call OpenRouter with given model ────────────────────────────────────────
|
|
217
|
+
call_openrouter() {
|
|
218
|
+
local model="$1"
|
|
219
|
+
local prompt="$2"
|
|
220
|
+
local extra_params="$3"
|
|
221
|
+
|
|
222
|
+
# Escape prompt for JSON
|
|
223
|
+
local escaped_prompt
|
|
224
|
+
escaped_prompt=$(echo "$prompt" | python3 -c "
|
|
225
|
+
import sys, json
|
|
226
|
+
print(json.dumps(sys.stdin.read()))
|
|
227
|
+
" 2>/dev/null) || escaped_prompt='""'
|
|
228
|
+
|
|
229
|
+
local body
|
|
230
|
+
body=$(cat <<JSON
|
|
231
|
+
{
|
|
232
|
+
"model": "${model}",
|
|
233
|
+
"messages": [{"role": "user", "content": ${escaped_prompt}}],
|
|
234
|
+
"response_format": {"type": "json_object"},
|
|
235
|
+
"temperature": 0${extra_params}
|
|
236
|
+
}
|
|
237
|
+
JSON
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
local http_code
|
|
241
|
+
local response
|
|
242
|
+
response=$(curl -s -w "\n%{http_code}" \
|
|
243
|
+
-X POST "$OPENROUTER_BASE_URL" \
|
|
244
|
+
-H "Content-Type: application/json" \
|
|
245
|
+
-H "Authorization: Bearer ${OPENROUTER_API_KEY}" \
|
|
246
|
+
-H "HTTP-Referer: https://github.com/forge-cli/forge" \
|
|
247
|
+
-d "$body" \
|
|
248
|
+
--max-time 60 2>/dev/null) || { echo "CURL_FAILED"; return 1; }
|
|
249
|
+
|
|
250
|
+
http_code=$(echo "$response" | tail -n1)
|
|
251
|
+
local body_only
|
|
252
|
+
body_only=$(echo "$response" | sed '$d')
|
|
253
|
+
|
|
254
|
+
# Check for HTTP error codes indicating we should fall back
|
|
255
|
+
if [ "$http_code" = "429" ] || [[ "$http_code" =~ ^5 ]]; then
|
|
256
|
+
echo "HTTP_ERROR_${http_code}"
|
|
257
|
+
return 1
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
echo "$body_only"
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
# ─── Test mode: bypass real HTTP calls ───────────────────────────────────────
|
|
264
|
+
if [ "${BEHAVIORAL_JUDGE_TEST_MODE}" = "1" ]; then
|
|
265
|
+
# Determine which judge would succeed based on failure flags
|
|
266
|
+
judge_calls=0
|
|
267
|
+
judge_model=""
|
|
268
|
+
_default_scores='{"security":4,"tdd":4,"design":4,"structural":3}'
|
|
269
|
+
mock_scores="${BEHAVIORAL_JUDGE_MOCK_SCORES:-${_default_scores}}"
|
|
270
|
+
all_failed=false
|
|
271
|
+
|
|
272
|
+
# Attempt primary (GLM-5)
|
|
273
|
+
judge_calls=$(( judge_calls + 1 ))
|
|
274
|
+
if [ "${BEHAVIORAL_JUDGE_MOCK_PRIMARY_FAIL}" != "1" ]; then
|
|
275
|
+
judge_model="$MODEL_PRIMARY"
|
|
276
|
+
else
|
|
277
|
+
# Attempt secondary (MiniMax)
|
|
278
|
+
judge_calls=$(( judge_calls + 1 ))
|
|
279
|
+
if [ "${BEHAVIORAL_JUDGE_MOCK_SECONDARY_FAIL}" != "1" ]; then
|
|
280
|
+
judge_model="$MODEL_SECONDARY"
|
|
281
|
+
else
|
|
282
|
+
# Attempt tertiary (Kimi)
|
|
283
|
+
judge_calls=$(( judge_calls + 1 ))
|
|
284
|
+
if [ "${BEHAVIORAL_JUDGE_MOCK_TERTIARY_FAIL}" != "1" ]; then
|
|
285
|
+
judge_model="$MODEL_TERTIARY"
|
|
286
|
+
else
|
|
287
|
+
all_failed=true
|
|
288
|
+
fi
|
|
289
|
+
fi
|
|
290
|
+
fi
|
|
291
|
+
|
|
292
|
+
if [ "$all_failed" = "true" ]; then
|
|
293
|
+
echo '{"result": "INCONCLUSIVE", "reason": "all_judges_failed"}'
|
|
294
|
+
exit 0
|
|
295
|
+
fi
|
|
296
|
+
|
|
297
|
+
# Parse mock scores
|
|
298
|
+
scores=$(echo "$mock_scores" | python3 -c "
|
|
299
|
+
import sys, json
|
|
300
|
+
d = json.loads(sys.stdin.read())
|
|
301
|
+
s = int(d.get('security', 3))
|
|
302
|
+
t = int(d.get('tdd', 3))
|
|
303
|
+
de = int(d.get('design', 3))
|
|
304
|
+
st = int(d.get('structural', 3))
|
|
305
|
+
print(f'{s} {t} {de} {st}')
|
|
306
|
+
" 2>/dev/null) || scores="3 3 3 3"
|
|
307
|
+
|
|
308
|
+
read -r sec tdd_s des str <<< "$scores"
|
|
309
|
+
compute_result "$sec" "$tdd_s" "$des" "$str" "$judge_model" "$judge_calls"
|
|
310
|
+
exit 0
|
|
311
|
+
fi
|
|
312
|
+
|
|
313
|
+
# ─── Live mode: real API calls ───────────────────────────────────────────────
|
|
314
|
+
if [ -z "$OPENROUTER_API_KEY" ]; then
|
|
315
|
+
>&2 echo "ERROR: OPENROUTER_API_KEY is not set"
|
|
316
|
+
echo '{"result": "INCONCLUSIVE", "reason": "missing_api_key"}'
|
|
317
|
+
exit 0
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
PROMPT=$(build_prompt "$PLAN_TEXT")
|
|
321
|
+
judge_calls=0
|
|
322
|
+
|
|
323
|
+
# Primary: GLM-5 with reasoning disabled
|
|
324
|
+
judge_calls=$(( judge_calls + 1 ))
|
|
325
|
+
>&2 echo "Calling primary judge: $MODEL_PRIMARY"
|
|
326
|
+
GLM5_EXTRA=', "reasoning": {"enabled": false}'
|
|
327
|
+
response=$(call_openrouter "$MODEL_PRIMARY" "$PROMPT" "$GLM5_EXTRA" 2>/dev/null) || response="CALL_FAILED"
|
|
328
|
+
|
|
329
|
+
if [ "$response" != "CALL_FAILED" ] && [[ "$response" != HTTP_ERROR* ]] && [ -n "$response" ]; then
|
|
330
|
+
scores=$(parse_scores "$response") || scores=""
|
|
331
|
+
if [ -n "$scores" ]; then
|
|
332
|
+
read -r sec tdd_s des str <<< "$scores"
|
|
333
|
+
compute_result "$sec" "$tdd_s" "$des" "$str" "$MODEL_PRIMARY" "$judge_calls"
|
|
334
|
+
exit 0
|
|
335
|
+
fi
|
|
336
|
+
>&2 echo "Primary judge returned unparseable response, falling back"
|
|
337
|
+
else
|
|
338
|
+
>&2 echo "Primary judge failed (${response}), falling back to secondary"
|
|
339
|
+
fi
|
|
340
|
+
|
|
341
|
+
# Secondary: MiniMax M2.5 (no reasoning field)
|
|
342
|
+
judge_calls=$(( judge_calls + 1 ))
|
|
343
|
+
>&2 echo "Calling secondary judge: $MODEL_SECONDARY"
|
|
344
|
+
response=$(call_openrouter "$MODEL_SECONDARY" "$PROMPT" "" 2>/dev/null) || response="CALL_FAILED"
|
|
345
|
+
|
|
346
|
+
if [ "$response" != "CALL_FAILED" ] && [[ "$response" != HTTP_ERROR* ]] && [ -n "$response" ]; then
|
|
347
|
+
scores=$(parse_scores "$response") || scores=""
|
|
348
|
+
if [ -n "$scores" ]; then
|
|
349
|
+
read -r sec tdd_s des str <<< "$scores"
|
|
350
|
+
compute_result "$sec" "$tdd_s" "$des" "$str" "$MODEL_SECONDARY" "$judge_calls"
|
|
351
|
+
exit 0
|
|
352
|
+
fi
|
|
353
|
+
>&2 echo "Secondary judge returned unparseable response, falling back"
|
|
354
|
+
else
|
|
355
|
+
>&2 echo "Secondary judge failed (${response}), falling back to tertiary"
|
|
356
|
+
fi
|
|
357
|
+
|
|
358
|
+
# Tertiary: Kimi K2.5 (chat_template_kwargs thinking:false, no tool calling)
|
|
359
|
+
judge_calls=$(( judge_calls + 1 ))
|
|
360
|
+
>&2 echo "Calling tertiary judge: $MODEL_TERTIARY"
|
|
361
|
+
KIMI_EXTRA=', "chat_template_kwargs": {"thinking": false}'
|
|
362
|
+
response=$(call_openrouter "$MODEL_TERTIARY" "$PROMPT" "$KIMI_EXTRA" 2>/dev/null) || response="CALL_FAILED"
|
|
363
|
+
|
|
364
|
+
if [ "$response" != "CALL_FAILED" ] && [[ "$response" != HTTP_ERROR* ]] && [ -n "$response" ]; then
|
|
365
|
+
scores=$(parse_scores "$response") || scores=""
|
|
366
|
+
if [ -n "$scores" ]; then
|
|
367
|
+
read -r sec tdd_s des str <<< "$scores"
|
|
368
|
+
compute_result "$sec" "$tdd_s" "$des" "$str" "$MODEL_TERTIARY" "$judge_calls"
|
|
369
|
+
exit 0
|
|
370
|
+
fi
|
|
371
|
+
>&2 echo "Tertiary judge returned unparseable response"
|
|
372
|
+
else
|
|
373
|
+
>&2 echo "Tertiary judge failed (${response})"
|
|
374
|
+
fi
|
|
375
|
+
|
|
376
|
+
# All judges failed
|
|
377
|
+
>&2 echo "All judges failed — returning INCONCLUSIVE"
|
|
378
|
+
echo '{"result": "INCONCLUSIVE", "reason": "all_judges_failed"}'
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Performance benchmark script for Forge CLI.
|
|
5
|
+
* Measures CLI startup time, autoDetect, and detectFramework performance.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node scripts/benchmark.js # Human-readable output
|
|
9
|
+
* node scripts/benchmark.js --json # JSON output (for CI/tests)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { execFileSync } = require('node:child_process');
|
|
13
|
+
const { performance } = require('node:perf_hooks');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
const fs = require('node:fs');
|
|
16
|
+
|
|
17
|
+
const rootDir = path.join(__dirname, '..');
|
|
18
|
+
const ITERATIONS = 3;
|
|
19
|
+
|
|
20
|
+
function benchmarkCLIStartup() {
|
|
21
|
+
const times = [];
|
|
22
|
+
for (let i = 0; i < ITERATIONS; i++) {
|
|
23
|
+
const start = performance.now();
|
|
24
|
+
execFileSync('node', [path.join(rootDir, 'bin', 'forge.js'), '--help'], {
|
|
25
|
+
cwd: rootDir,
|
|
26
|
+
encoding: 'utf-8',
|
|
27
|
+
timeout: 10000,
|
|
28
|
+
stdio: 'pipe'
|
|
29
|
+
});
|
|
30
|
+
times.push(performance.now() - start);
|
|
31
|
+
}
|
|
32
|
+
const mean = times.reduce((a, b) => a + b, 0) / times.length;
|
|
33
|
+
return { name: 'CLI startup (--help)', mean: Math.round(mean), unit: 'ms', samples: ITERATIONS };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function benchmarkAutoDetect() {
|
|
37
|
+
const { autoDetect } = require(path.join(rootDir, 'lib', 'project-discovery.js'));
|
|
38
|
+
const times = [];
|
|
39
|
+
for (let i = 0; i < ITERATIONS; i++) {
|
|
40
|
+
const start = performance.now();
|
|
41
|
+
autoDetect(rootDir);
|
|
42
|
+
times.push(performance.now() - start);
|
|
43
|
+
}
|
|
44
|
+
const mean = times.reduce((a, b) => a + b, 0) / times.length;
|
|
45
|
+
return { name: 'autoDetect()', mean: Math.round(mean), unit: 'ms', samples: ITERATIONS };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function benchmarkDetectFramework() {
|
|
49
|
+
const { detectFramework } = require(path.join(rootDir, 'lib', 'project-discovery.js'));
|
|
50
|
+
const times = [];
|
|
51
|
+
for (let i = 0; i < ITERATIONS; i++) {
|
|
52
|
+
const start = performance.now();
|
|
53
|
+
detectFramework(rootDir);
|
|
54
|
+
times.push(performance.now() - start);
|
|
55
|
+
}
|
|
56
|
+
const mean = times.reduce((a, b) => a + b, 0) / times.length;
|
|
57
|
+
return { name: 'detectFramework()', mean: Math.round(mean), unit: 'ms', samples: ITERATIONS };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const results = [
|
|
61
|
+
benchmarkCLIStartup(),
|
|
62
|
+
benchmarkAutoDetect(),
|
|
63
|
+
benchmarkDetectFramework()
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const jsonMode = process.argv.includes('--json');
|
|
67
|
+
|
|
68
|
+
if (jsonMode) {
|
|
69
|
+
process.stdout.write(JSON.stringify(results));
|
|
70
|
+
} else {
|
|
71
|
+
console.log('\n Forge Performance Benchmarks');
|
|
72
|
+
console.log(' ===========================\n');
|
|
73
|
+
for (const r of results) {
|
|
74
|
+
const status = r.mean < 1000 ? 'PASS' : r.mean < 3000 ? 'WARN' : 'SLOW';
|
|
75
|
+
console.log(` ${status} ${r.name}: ${r.mean}${r.unit} (${r.samples} samples)`);
|
|
76
|
+
}
|
|
77
|
+
console.log('');
|
|
78
|
+
|
|
79
|
+
// Write results file
|
|
80
|
+
const outputPath = path.join(rootDir, 'benchmark-results.json');
|
|
81
|
+
fs.writeFileSync(outputPath, JSON.stringify(results, null, 2));
|
|
82
|
+
console.log(` Results saved to: benchmark-results.json\n`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = results;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Branch Protection Script (Cross-Platform)
|
|
4
|
+
*
|
|
5
|
+
* Prevents direct pushes to main/master branches.
|
|
6
|
+
* Uses Node.js for Windows compatibility (no shell-specific syntax).
|
|
7
|
+
*
|
|
8
|
+
* Exit codes:
|
|
9
|
+
* 0 - Push allowed
|
|
10
|
+
* 1 - Push blocked (protected branch)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
const { execFileSync } = require('node:child_process');
|
|
16
|
+
|
|
17
|
+
// ANSI color codes
|
|
18
|
+
const RED = '\x1b[31m';
|
|
19
|
+
const YELLOW = '\x1b[33m';
|
|
20
|
+
const RESET = '\x1b[0m';
|
|
21
|
+
|
|
22
|
+
/** Test-only: run mock-git.js via `node` so Windows does not need shell:true or git.exe shims */
|
|
23
|
+
const GIT_MOCK_JS = process.env.NODE_ENV === 'test'
|
|
24
|
+
? process.env.FORGE_GIT_MOCK_JS
|
|
25
|
+
: undefined;
|
|
26
|
+
|
|
27
|
+
const EXEC_OPTS = { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] };
|
|
28
|
+
|
|
29
|
+
function fileExistsSync(p) {
|
|
30
|
+
try {
|
|
31
|
+
return fs.statSync(p).isFile();
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve a real git binary (git.exe on Windows). Never uses shell — avoids injection via argv joining.
|
|
39
|
+
*/
|
|
40
|
+
function resolveGitBinary() {
|
|
41
|
+
const raw = (process.platform === 'win32'
|
|
42
|
+
? (process.env.Path || process.env.PATH || '')
|
|
43
|
+
: (process.env.PATH || ''));
|
|
44
|
+
const dirs = raw.split(path.delimiter).filter(Boolean);
|
|
45
|
+
if (process.platform === 'win32') {
|
|
46
|
+
for (const d of dirs) {
|
|
47
|
+
const exe = path.join(d, 'git.exe');
|
|
48
|
+
if (fileExistsSync(exe)) return exe;
|
|
49
|
+
}
|
|
50
|
+
return 'git.exe';
|
|
51
|
+
}
|
|
52
|
+
for (const d of dirs) {
|
|
53
|
+
const g = path.join(d, 'git');
|
|
54
|
+
if (fileExistsSync(g)) return g;
|
|
55
|
+
}
|
|
56
|
+
return 'git';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Narrow ref shape for env-provided branch names (used in diff ref ranges). */
|
|
60
|
+
function isSafeGitRefComponent(s) {
|
|
61
|
+
if (!s || s.length > 256) return false;
|
|
62
|
+
return /^[a-zA-Z0-9/._-]+$/.test(s);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function execGit(args) {
|
|
66
|
+
if (GIT_MOCK_JS) {
|
|
67
|
+
return execFileSync(process.execPath, [GIT_MOCK_JS, ...args], EXEC_OPTS);
|
|
68
|
+
}
|
|
69
|
+
return execFileSync(resolveGitBinary(), args, EXEC_OPTS);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Protected branches
|
|
73
|
+
const PROTECTED_BRANCHES = new Set(['main', 'master']);
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get the current branch name
|
|
77
|
+
* @returns {string} Current git branch name
|
|
78
|
+
*/
|
|
79
|
+
function getCurrentBranch() {
|
|
80
|
+
try {
|
|
81
|
+
if (process.env.LEFTHOOK_GIT_BRANCH) {
|
|
82
|
+
const b = process.env.LEFTHOOK_GIT_BRANCH.trim();
|
|
83
|
+
if (!isSafeGitRefComponent(b)) {
|
|
84
|
+
console.error(`${RED}✗ Error: Invalid LEFTHOOK_GIT_BRANCH value${RESET}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
return b;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const branch = execGit(['rev-parse', '--abbrev-ref', 'HEAD']).trim();
|
|
91
|
+
if (!isSafeGitRefComponent(branch)) {
|
|
92
|
+
console.error(`${RED}✗ Error: Invalid branch name from git${RESET}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
return branch;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error(`${RED}✗ Error: Could not determine current branch${RESET}`);
|
|
98
|
+
console.error(` ${error.message}`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check if branch is protected
|
|
105
|
+
* @param {string} branch - Branch name to check
|
|
106
|
+
* @returns {boolean} True if branch is protected
|
|
107
|
+
*/
|
|
108
|
+
function isProtectedBranch(branch) {
|
|
109
|
+
return PROTECTED_BRANCHES.has(branch);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Main function
|
|
114
|
+
*/
|
|
115
|
+
function main() {
|
|
116
|
+
// Handle --help flag
|
|
117
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
118
|
+
console.log('Branch Protection Script');
|
|
119
|
+
console.log('');
|
|
120
|
+
console.log('Prevents direct pushes to protected branches (main/master).');
|
|
121
|
+
console.log('');
|
|
122
|
+
console.log('Usage:');
|
|
123
|
+
console.log(' node scripts/branch-protection.js');
|
|
124
|
+
console.log('');
|
|
125
|
+
console.log('Exit codes:');
|
|
126
|
+
console.log(' 0 - Push allowed');
|
|
127
|
+
console.log(' 1 - Push blocked (protected branch)');
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const currentBranch = getCurrentBranch();
|
|
132
|
+
|
|
133
|
+
if (isProtectedBranch(currentBranch)) {
|
|
134
|
+
// Allow beads-only commits (issue tracking metadata) to push directly
|
|
135
|
+
try {
|
|
136
|
+
let upstream;
|
|
137
|
+
try {
|
|
138
|
+
upstream = execGit(['rev-parse', '--abbrev-ref', '@{u}']).trim();
|
|
139
|
+
} catch (_e) {
|
|
140
|
+
upstream = `origin/${currentBranch}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!isSafeGitRefComponent(upstream)) {
|
|
144
|
+
throw new Error('unsafe upstream ref');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const output = execGit(['diff', '--name-only', `${upstream}..HEAD`]).trim();
|
|
148
|
+
const changedFiles = output.split('\n').filter(Boolean);
|
|
149
|
+
|
|
150
|
+
if (changedFiles.length === 0) {
|
|
151
|
+
console.error(`${YELLOW}Note: no changed files detected — nothing to bypass${RESET}`);
|
|
152
|
+
} else if (changedFiles.every(f => f.startsWith('.beads/'))) {
|
|
153
|
+
console.error(`${YELLOW}Beads-only push to '${currentBranch}' — allowed${RESET}`);
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
} catch (_e) {
|
|
157
|
+
console.error(`${YELLOW}Note: could not detect beads-only push (upstream ref missing?) — blocking by default${RESET}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.error('');
|
|
161
|
+
console.error(`${RED}╔═══════════════════════════════════════════════════════════════╗${RESET}`);
|
|
162
|
+
console.error(`${RED}║ ⚠ PUSH BLOCKED ║${RESET}`);
|
|
163
|
+
console.error(`${RED}╚═══════════════════════════════════════════════════════════════╝${RESET}`);
|
|
164
|
+
console.error('');
|
|
165
|
+
console.error(`${RED}✗ Direct pushes to '${currentBranch}' are forbidden.${RESET}`);
|
|
166
|
+
console.error('');
|
|
167
|
+
console.error(`${YELLOW}To push your changes:${RESET}`);
|
|
168
|
+
console.error(` 1. Create a feature branch: ${YELLOW}git checkout -b feat/my-feature${RESET}`);
|
|
169
|
+
console.error(` 2. Push to the feature branch: ${YELLOW}git push -u origin feat/my-feature${RESET}`);
|
|
170
|
+
console.error(` 3. Create a pull request for review`);
|
|
171
|
+
console.error('');
|
|
172
|
+
console.error(`${YELLOW}Emergency hook bypass is human-only and must not appear in agent logs.${RESET}`);
|
|
173
|
+
console.error(` See ${YELLOW}CLAUDE.md${RESET} (Git Workflow) — AI agents must fix failing hooks, not bypass them.`);
|
|
174
|
+
console.error('');
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Push allowed
|
|
179
|
+
process.exit(0);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Run main function
|
|
183
|
+
main();
|