@@ -6,8 +6,11 @@ const entrypointGitHooksTemplate = String
66HOOKS_DIR="/opt/docker-git/hooks"
77PRE_PUSH_HOOK="$HOOKS_DIR/pre-push"
88POST_PUSH_ACTION="$HOOKS_DIR/post-push"
9+ PLAN_TO_GIT_SYNC_HELPER="$HOOKS_DIR/plan-to-git-sync"
910PLAN_TO_GIT_CODEX_HOOK="$HOOKS_DIR/plan-to-git-codex-hook"
11+ PLAN_TO_GIT_CLAUDE_HOOK="$HOOKS_DIR/plan-to-git-claude-hook"
1012CODEX_REQUIREMENTS_FILE="/etc/codex/requirements.toml"
13+ CLAUDE_PLAN_TO_GIT_SETTINGS_FILE="$CLAUDE_CONFIG_DIR/settings.json"
1114mkdir -p "$HOOKS_DIR"
1215
1316cat <<'EOF' > "$PRE_PUSH_HOOK"
@@ -135,6 +138,75 @@ done
135138EOF
136139chmod 0755 "$PRE_PUSH_HOOK"
137140
141+ cat <<'EOF' > "$PLAN_TO_GIT_SYNC_HELPER"
142+ #!/usr/bin/env bash
143+ set -euo pipefail
144+
145+ if [ "${ "${" } DOCKER_GIT_SKIP_PLAN_TO_GIT:-}" = "1" ]; then
146+ exit 0
147+ fi
148+
149+ if ! command -v plan-to-git >/dev/null 2>&1; then
150+ echo "[plan-to-git] Error: plan-to-git not found" >&2
151+ exit 1
152+ fi
153+
154+ export PLAN_TO_GIT_STATE_DIR="${ "${" } PLAN_TO_GIT_STATE_DIR:-/tmp/plan-to-git}"
155+
156+ docker_git_plan_to_git_explicit_pr_supported() {
157+ plan-to-git sync --help 2>/dev/null | grep -q -- "--pr <PR>"
158+ }
159+
160+ docker_git_plan_to_git_resolve_pr_number() {
161+ local candidate=""
162+ local key=""
163+ for key in DOCKER_GIT_PR_NUMBER PR_NUMBER GITHUB_PR_NUMBER; do
164+ candidate="${ "${" } !key:-}"
165+ if [[ "$candidate" =~ ^[0-9]+$ ]]; then
166+ printf "%s\n" "$candidate"
167+ return 0
168+ fi
169+ done
170+
171+ candidate="${ "${" } REPO_REF:-}"
172+ if [[ "$candidate" =~ ^refs/pull/([0-9]+)/head$ ]]; then
173+ printf "%s\n" "${ "${" } BASH_REMATCH[1]}"
174+ return 0
175+ fi
176+ if [[ "$candidate" =~ ^pull/([0-9]+)$ ]]; then
177+ printf "%s\n" "${ "${" } BASH_REMATCH[1]}"
178+ return 0
179+ fi
180+
181+ if command -v gh >/dev/null 2>&1; then
182+ candidate="$(gh pr view --json number --jq .number 2>/dev/null || true)"
183+ if [[ "$candidate" =~ ^[0-9]+$ ]]; then
184+ printf "%s\n" "$candidate"
185+ return 0
186+ fi
187+ fi
188+
189+ return 0
190+ }
191+
192+ docker_git_plan_to_git_sync() {
193+ local pr_number=""
194+ pr_number="$(docker_git_plan_to_git_resolve_pr_number || true)"
195+
196+ if [[ -n "$pr_number" ]] && docker_git_plan_to_git_explicit_pr_supported; then
197+ echo "[plan-to-git] Syncing queued agent plans to PR #$pr_number"
198+ plan-to-git sync --pr "$pr_number"
199+ return 0
200+ fi
201+
202+ echo "[plan-to-git] Syncing queued agent plans via current branch discovery"
203+ plan-to-git sync
204+ }
205+
206+ docker_git_plan_to_git_sync
207+ EOF
208+ chmod 0755 "$PLAN_TO_GIT_SYNC_HELPER"
209+
138210cat <<'EOF' > "$POST_PUSH_ACTION"
139211#!/usr/bin/env bash
140212set -euo pipefail
@@ -148,16 +220,24 @@ cd "$REPO_ROOT"
148220
149221${ renderPostPushPrEnsure ( ) }
150222
151- # CHANGE: backfill Codex session plans before syncing the current branch PR.
152- # WHY: live Codex hooks can be unavailable in already-running sessions; session logs are the durable fallback.
153- # REF: issue-375
223+ # CHANGE: backfill agent session plans before syncing the current branch or explicit PR.
224+ # WHY: live agent hooks can be unavailable in already-running sessions; session logs are the durable fallback.
225+ # QUOTE(ТЗ): "что бы всё уходило на гитхаб автоматически"
226+ # REF: issue-397
154227if [ "${ "${" } DOCKER_GIT_SKIP_PLAN_TO_GIT:-}" != "1" ]; then
155228 if ! command -v plan-to-git >/dev/null 2>&1; then
156229 echo "[plan-to-git] Error: plan-to-git not found" >&2
157230 exit 1
158231 fi
159232 plan-to-git import-codex --no-sync
160- plan-to-git sync
233+ plan-to-git import-claude --no-sync
234+ PLAN_TO_GIT_SYNC_HELPER="${ "${" } DOCKER_GIT_PLAN_TO_GIT_SYNC_HELPER:-/opt/docker-git/hooks/plan-to-git-sync}"
235+ if [[ -x "$PLAN_TO_GIT_SYNC_HELPER" ]]; then
236+ "$PLAN_TO_GIT_SYNC_HELPER"
237+ else
238+ echo "[plan-to-git] Sync helper not found; falling back to current branch discovery" >&2
239+ plan-to-git sync
240+ fi
161241fi
162242
163243# CHANGE: keep post-push backup logic in a reusable action script
@@ -191,10 +271,33 @@ if ! command -v plan-to-git >/dev/null 2>&1; then
191271 exit 1
192272fi
193273
274+ export PLAN_TO_GIT_STATE_DIR="${ "${" } PLAN_TO_GIT_STATE_DIR:-/tmp/plan-to-git}"
194275plan-to-git hook --source codex
276+ PLAN_TO_GIT_SYNC_HELPER="${ "${" } DOCKER_GIT_PLAN_TO_GIT_SYNC_HELPER:-/opt/docker-git/hooks/plan-to-git-sync}"
277+ "$PLAN_TO_GIT_SYNC_HELPER" >&2 || true
195278EOF
196279chmod 0755 "$PLAN_TO_GIT_CODEX_HOOK"
197280
281+ cat <<'EOF' > "$PLAN_TO_GIT_CLAUDE_HOOK"
282+ #!/usr/bin/env bash
283+ set -euo pipefail
284+
285+ if [ "${ "${" } DOCKER_GIT_SKIP_PLAN_TO_GIT:-}" = "1" ]; then
286+ exit 0
287+ fi
288+
289+ if ! command -v plan-to-git >/dev/null 2>&1; then
290+ echo "[plan-to-git] Error: plan-to-git not found" >&2
291+ exit 1
292+ fi
293+
294+ export PLAN_TO_GIT_STATE_DIR="${ "${" } PLAN_TO_GIT_STATE_DIR:-/tmp/plan-to-git}"
295+ plan-to-git hook --source claude
296+ PLAN_TO_GIT_SYNC_HELPER="${ "${" } DOCKER_GIT_PLAN_TO_GIT_SYNC_HELPER:-/opt/docker-git/hooks/plan-to-git-sync}"
297+ "$PLAN_TO_GIT_SYNC_HELPER" >&2 || true
298+ EOF
299+ chmod 0755 "$PLAN_TO_GIT_CLAUDE_HOOK"
300+
198301mkdir -p "$(dirname "$CODEX_REQUIREMENTS_FILE")"
199302cat <<'EOF' > "$CODEX_REQUIREMENTS_FILE"
200303# docker-git managed Codex requirements
@@ -219,6 +322,62 @@ statusMessage = "Capturing agent plan"
219322EOF
220323chmod 0644 "$CODEX_REQUIREMENTS_FILE"
221324
325+ docker_git_install_claude_plan_to_git_hooks() {
326+ if [ "${ "${" } DOCKER_GIT_SKIP_PLAN_TO_GIT:-}" = "1" ]; then
327+ return 0
328+ fi
329+
330+ CLAUDE_PLAN_TO_GIT_SETTINGS_FILE="${ "${" } CLAUDE_PLAN_TO_GIT_SETTINGS_FILE:-${ "${" } CLAUDE_CONFIG_DIR:-/home/dev/.claude}/settings.json}"
331+ CLAUDE_PLAN_TO_GIT_SETTINGS_FILE="$CLAUDE_PLAN_TO_GIT_SETTINGS_FILE" PLAN_TO_GIT_CLAUDE_HOOK="$PLAN_TO_GIT_CLAUDE_HOOK" node - <<'NODE'
332+ const fs = require("node:fs")
333+ const path = require("node:path")
334+
335+ const settingsPath = process.env.CLAUDE_PLAN_TO_GIT_SETTINGS_FILE
336+ const hookCommand = process.env.PLAN_TO_GIT_CLAUDE_HOOK || "/opt/docker-git/hooks/plan-to-git-claude-hook"
337+ if (typeof settingsPath !== "string" || settingsPath.length === 0) {
338+ process.exit(0)
339+ }
340+
341+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value)
342+
343+ let settings = {}
344+ try {
345+ const parsed = JSON.parse(fs.readFileSync(settingsPath, "utf8"))
346+ settings = isRecord(parsed) ? parsed : {}
347+ } catch {
348+ settings = {}
349+ }
350+
351+ const currentHooks = isRecord(settings.hooks) ? settings.hooks : {}
352+ const nextHooks = { ...currentHooks }
353+ const managedHook = { type: "command", command: hookCommand }
354+ const ensureEventHook = (eventName) => {
355+ const currentEventHooks = Array.isArray(nextHooks[eventName]) ? nextHooks[eventName] : []
356+ const alreadyInstalled = currentEventHooks.some((entry) =>
357+ isRecord(entry) &&
358+ Array.isArray(entry.hooks) &&
359+ entry.hooks.some((hook) => isRecord(hook) && hook.type === "command" && hook.command === hookCommand)
360+ )
361+ nextHooks[eventName] = alreadyInstalled ? currentEventHooks : [...currentEventHooks, { hooks: [managedHook] }]
362+ }
363+
364+ ensureEventHook("UserPromptSubmit")
365+ ensureEventHook("Stop")
366+
367+ const nextSettings = { ...settings, hooks: nextHooks }
368+ if (JSON.stringify(settings) === JSON.stringify(nextSettings)) {
369+ process.exit(0)
370+ }
371+
372+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true })
373+ fs.writeFileSync(settingsPath, JSON.stringify(nextSettings, null, 2) + "\n", { mode: 0o600 })
374+ NODE
375+ chmod 0600 "$CLAUDE_PLAN_TO_GIT_SETTINGS_FILE" 2>/dev/null || true
376+ chown 1000:1000 "$CLAUDE_PLAN_TO_GIT_SETTINGS_FILE" 2>/dev/null || true
377+ }
378+
379+ docker_git_install_claude_plan_to_git_hooks
380+
222381${ renderEntrypointGitPostPushWrapperInstall ( ) }
223382
224383git config --system core.hooksPath "$HOOKS_DIR" || true
0 commit comments