Skip to content

Commit 3341598

Browse files
committed
feat(shell): sync agent plans to pull requests
1 parent e69b841 commit 3341598

9 files changed

Lines changed: 277 additions & 13 deletions

File tree

packages/app/src/lib/core/templates-entrypoint/git-hooks.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const entrypointGitHooksTemplate = String
55
HOOKS_DIR="/opt/docker-git/hooks"
66
PRE_PUSH_HOOK="$HOOKS_DIR/pre-push"
77
POST_PUSH_ACTION="$HOOKS_DIR/post-push"
8+
PLAN_TO_GIT_CODEX_HOOK="$HOOKS_DIR/plan-to-git-codex-hook"
9+
CODEX_REQUIREMENTS_FILE="/etc/codex/requirements.toml"
810
mkdir -p "$HOOKS_DIR"
911
1012
cat <<'EOF' > "$PRE_PUSH_HOOK"
@@ -136,13 +138,24 @@ cat <<'EOF' > "$POST_PUSH_ACTION"
136138
#!/usr/bin/env bash
137139
set -euo pipefail
138140
139-
# 5) Run session backup after successful push
141+
# 5) Run plan sync and session backup after successful push
140142
REPO_ROOT="${"${"}DOCKER_GIT_POST_PUSH_REPO_ROOT:-}"
141143
if [[ -z "$REPO_ROOT" || ! -d "$REPO_ROOT" ]]; then
142144
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
143145
fi
144146
cd "$REPO_ROOT"
145147
148+
# CHANGE: sync captured Codex plans to the current branch PR after push.
149+
# WHY: issue #369 requires the agent plan to be uploaded to PR discussion.
150+
# REF: issue-369
151+
if [ "${"${"}DOCKER_GIT_SKIP_PLAN_TO_GIT:-}" != "1" ]; then
152+
if ! command -v plan-to-git >/dev/null 2>&1; then
153+
echo "[plan-to-git] Error: plan-to-git not found" >&2
154+
exit 1
155+
fi
156+
plan-to-git sync
157+
fi
158+
146159
# CHANGE: keep post-push backup logic in a reusable action script
147160
# WHY: git has no client-side post-push hook, so the global git wrapper
148161
# invokes this after a successful git push
@@ -161,6 +174,47 @@ fi
161174
EOF
162175
chmod 0755 "$POST_PUSH_ACTION"
163176
177+
cat <<'EOF' > "$PLAN_TO_GIT_CODEX_HOOK"
178+
#!/usr/bin/env bash
179+
set -euo pipefail
180+
181+
if [ "${"${"}DOCKER_GIT_SKIP_PLAN_TO_GIT:-}" = "1" ]; then
182+
exit 0
183+
fi
184+
185+
if ! command -v plan-to-git >/dev/null 2>&1; then
186+
echo "[plan-to-git] Error: plan-to-git not found" >&2
187+
exit 1
188+
fi
189+
190+
plan-to-git hook --source codex
191+
EOF
192+
chmod 0755 "$PLAN_TO_GIT_CODEX_HOOK"
193+
194+
mkdir -p "$(dirname "$CODEX_REQUIREMENTS_FILE")"
195+
cat <<'EOF' > "$CODEX_REQUIREMENTS_FILE"
196+
# docker-git managed Codex requirements
197+
198+
[features]
199+
hooks = true
200+
201+
[hooks]
202+
managed_dir = "/opt/docker-git/hooks"
203+
204+
[[hooks.UserPromptSubmit]]
205+
[[hooks.UserPromptSubmit.hooks]]
206+
type = "command"
207+
command = "/opt/docker-git/hooks/plan-to-git-codex-hook"
208+
statusMessage = "Capturing plan decision"
209+
210+
[[hooks.Stop]]
211+
[[hooks.Stop.hooks]]
212+
type = "command"
213+
command = "/opt/docker-git/hooks/plan-to-git-codex-hook"
214+
statusMessage = "Capturing agent plan"
215+
EOF
216+
chmod 0644 "$CODEX_REQUIREMENTS_FILE"
217+
164218
${renderEntrypointGitPostPushWrapperInstall()}
165219
166220
git config --system core.hooksPath "$HOOKS_DIR" || true

packages/app/src/lib/core/templates.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ scripts/
3939
4040
# Volatile Codex artifacts (do not commit)
4141
authorized_keys
42+
.agent-plan.json
4243
.orch/auth/codex/auth.json
4344
.orch/auth/claude/
4445
.orch/auth/codex/log/
@@ -50,6 +51,7 @@ authorized_keys
5051
const renderDockerignore = (): string =>
5152
`# docker-git build context
5253
authorized_keys
54+
.agent-plan.json
5355
.orch/env/
5456
.orch/auth/codex/
5557
.orch/auth/claude/

packages/app/src/lib/core/templates/dockerfile-prelude.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,33 @@ RUN cargo install --git https://github.com/ProverCoderAI/rust-browser-connection
8383
RUN printf "%s\\n" "ALL ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/zz-all \
8484
&& chmod 0440 /etc/sudoers.d/zz-all`
8585

86+
const planToGitRevision = "06fe8bdf1d2e48a1f5a0218a3bb7af19e63deb5e"
87+
88+
// CHANGE: install plan-to-git in generated project containers.
89+
// WHY: issue #369 requires agent plans to be captured and uploaded to pull requests.
90+
// QUOTE(ТЗ): "Надо что бы у нас план загружался в PR"
91+
// REF: issue-369
92+
// SOURCE: https://github.com/ProverCoderAI/plan-to-git/tree/v0.19.0
93+
// FORMAT THEOREM: image_build_success -> executable(/usr/local/bin/plan-to-git)
94+
// PURITY: SHELL
95+
// EFFECT: Docker build downloads and installs a pinned Rust CLI from GitHub.
96+
// INVARIANT: plan-to-git is available on PATH before Codex hooks or git post-push actions run.
97+
// COMPLEXITY: O(network + cargo_build)
98+
const renderDockerfilePlanToGit = (): string =>
99+
`# Install plan-to-git for Codex plan capture and PR sync (issue #369)
100+
RUN cargo install --git https://github.com/ProverCoderAI/plan-to-git --rev ${planToGitRevision} --locked --bins --root /usr/local \
101+
&& /usr/local/bin/plan-to-git --help >/dev/null`
102+
86103
/**
87-
* Renders the base image, package prelude, Rust toolchain, and browser module install.
104+
* Renders the base image, package prelude, Rust toolchain, browser module, and plan sync CLI install.
88105
*
89106
* @returns Dockerfile fragment that establishes the shared project container base.
90107
* @pure true
91108
* @effect none; CORE template renderer only constructs a string.
92-
* @invariant the returned fragment starts from the configured shared JS box image and installs the Rust browser lifecycle + MCP CLIs.
109+
* @invariant the returned fragment starts from the configured shared JS box image and installs the Rust browser lifecycle, MCP CLIs, and plan-to-git.
93110
* @precondition docker-git generated entrypoint remains the container entrypoint.
94-
* @postcondition the fragment keeps root available for setup and publishes both Rust browser binaries on PATH.
111+
* @postcondition the fragment keeps root available for setup and publishes Rust helper binaries on PATH.
95112
* @complexity O(1) time / O(1) space.
96113
*/
97114
export const renderDockerfilePrelude = (): string =>
98-
[renderDockerfileBase(), renderDockerfileRustBrowserConnection()].join("\n\n")
115+
[renderDockerfileBase(), renderDockerfileRustBrowserConnection(), renderDockerfilePlanToGit()].join("\n\n")

packages/app/tests/docker-git/core-templates.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ describe("app planFiles", () => {
6363
expect(dockerfile.contents).toContain(
6464
"cargo install --git https://github.com/ProverCoderAI/rust-browser-connection"
6565
)
66+
expect(dockerfile.contents).toContain(
67+
"cargo install --git https://github.com/ProverCoderAI/plan-to-git --rev 06fe8bdf1d2e48a1f5a0218a3bb7af19e63deb5e --locked --bins --root /usr/local"
68+
)
69+
expect(dockerfile.contents).toContain("/usr/local/bin/plan-to-git --help >/dev/null")
6670
expect(dockerfile.contents).toContain("make build-essential docker.io")
6771
expect(dockerfile.contents).toContain("/usr/local/bin/browser-connection --version")
6872
expect(dockerfile.contents).not.toContain("docker-git-playwright-mcp")
@@ -76,5 +80,21 @@ describe("app planFiles", () => {
7680
expect(entrypoint.contents).toContain(
7781
"args = [\"--project\", \"$DOCKER_GIT_BROWSER_PROJECT\", \"--network\", \"$DOCKER_GIT_BROWSER_NETWORK\"]"
7882
)
83+
expect(entrypoint.contents).toContain("plan-to-git sync")
84+
expect(entrypoint.contents).toContain("plan-to-git hook --source codex")
85+
expect(entrypoint.contents).toContain("CODEX_REQUIREMENTS_FILE=\"/etc/codex/requirements.toml\"")
86+
expect(entrypoint.contents).toContain("managed_dir = \"/opt/docker-git/hooks\"")
87+
expect(entrypoint.contents).toContain("[[hooks.UserPromptSubmit]]")
88+
expect(entrypoint.contents).toContain("[[hooks.Stop]]")
89+
expect(entrypoint.contents).toContain("command = \"/opt/docker-git/hooks/plan-to-git-codex-hook\"")
90+
})
91+
92+
it("keeps plan-to-git state out of generated git and docker contexts", () => {
93+
const files = planFiles(makeTemplateConfig())
94+
const gitignore = getGeneratedFile(files, ".gitignore")
95+
const dockerignore = getGeneratedFile(files, ".dockerignore")
96+
97+
expect(gitignore.contents).toContain(".agent-plan.json")
98+
expect(dockerignore.contents).toContain(".agent-plan.json")
7999
})
80100
})

packages/lib/src/core/templates-entrypoint/git-hooks.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const entrypointGitHooksTemplate = String
55
HOOKS_DIR="/opt/docker-git/hooks"
66
PRE_PUSH_HOOK="$HOOKS_DIR/pre-push"
77
POST_PUSH_ACTION="$HOOKS_DIR/post-push"
8+
PLAN_TO_GIT_CODEX_HOOK="$HOOKS_DIR/plan-to-git-codex-hook"
9+
CODEX_REQUIREMENTS_FILE="/etc/codex/requirements.toml"
810
mkdir -p "$HOOKS_DIR"
911
1012
cat <<'EOF' > "$PRE_PUSH_HOOK"
@@ -136,13 +138,24 @@ cat <<'EOF' > "$POST_PUSH_ACTION"
136138
#!/usr/bin/env bash
137139
set -euo pipefail
138140
139-
# 5) Run session backup after successful push
141+
# 5) Run plan sync and session backup after successful push
140142
REPO_ROOT="${"${"}DOCKER_GIT_POST_PUSH_REPO_ROOT:-}"
141143
if [[ -z "$REPO_ROOT" || ! -d "$REPO_ROOT" ]]; then
142144
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
143145
fi
144146
cd "$REPO_ROOT"
145147
148+
# CHANGE: sync captured Codex plans to the current branch PR after push.
149+
# WHY: issue #369 requires the agent plan to be uploaded to PR discussion.
150+
# REF: issue-369
151+
if [ "${"${"}DOCKER_GIT_SKIP_PLAN_TO_GIT:-}" != "1" ]; then
152+
if ! command -v plan-to-git >/dev/null 2>&1; then
153+
echo "[plan-to-git] Error: plan-to-git not found" >&2
154+
exit 1
155+
fi
156+
plan-to-git sync
157+
fi
158+
146159
# CHANGE: keep post-push backup logic in a reusable action script
147160
# WHY: git has no client-side post-push hook, so the global git wrapper
148161
# invokes this after a successful git push
@@ -161,6 +174,47 @@ fi
161174
EOF
162175
chmod 0755 "$POST_PUSH_ACTION"
163176
177+
cat <<'EOF' > "$PLAN_TO_GIT_CODEX_HOOK"
178+
#!/usr/bin/env bash
179+
set -euo pipefail
180+
181+
if [ "${"${"}DOCKER_GIT_SKIP_PLAN_TO_GIT:-}" = "1" ]; then
182+
exit 0
183+
fi
184+
185+
if ! command -v plan-to-git >/dev/null 2>&1; then
186+
echo "[plan-to-git] Error: plan-to-git not found" >&2
187+
exit 1
188+
fi
189+
190+
plan-to-git hook --source codex
191+
EOF
192+
chmod 0755 "$PLAN_TO_GIT_CODEX_HOOK"
193+
194+
mkdir -p "$(dirname "$CODEX_REQUIREMENTS_FILE")"
195+
cat <<'EOF' > "$CODEX_REQUIREMENTS_FILE"
196+
# docker-git managed Codex requirements
197+
198+
[features]
199+
hooks = true
200+
201+
[hooks]
202+
managed_dir = "/opt/docker-git/hooks"
203+
204+
[[hooks.UserPromptSubmit]]
205+
[[hooks.UserPromptSubmit.hooks]]
206+
type = "command"
207+
command = "/opt/docker-git/hooks/plan-to-git-codex-hook"
208+
statusMessage = "Capturing plan decision"
209+
210+
[[hooks.Stop]]
211+
[[hooks.Stop.hooks]]
212+
type = "command"
213+
command = "/opt/docker-git/hooks/plan-to-git-codex-hook"
214+
statusMessage = "Capturing agent plan"
215+
EOF
216+
chmod 0644 "$CODEX_REQUIREMENTS_FILE"
217+
164218
${renderEntrypointGitPostPushWrapperInstall()}
165219
166220
git config --system core.hooksPath "$HOOKS_DIR" || true

packages/lib/src/core/templates.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ scripts/
3838
3939
# Volatile Codex artifacts (do not commit)
4040
authorized_keys
41+
.agent-plan.json
4142
.orch/auth/codex/auth.json
4243
.orch/auth/claude/
4344
.orch/auth/codex/log/
@@ -49,6 +50,7 @@ authorized_keys
4950
const renderDockerignore = (): string =>
5051
`# docker-git build context
5152
authorized_keys
53+
.agent-plan.json
5254
.orch/env/
5355
.orch/auth/codex/
5456
.orch/auth/claude/

packages/lib/src/core/templates/dockerfile-prelude.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,33 @@ RUN cargo install --git https://github.com/ProverCoderAI/rust-browser-connection
8383
RUN printf "%s\\n" "ALL ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/zz-all \
8484
&& chmod 0440 /etc/sudoers.d/zz-all`
8585

86+
const planToGitRevision = "06fe8bdf1d2e48a1f5a0218a3bb7af19e63deb5e"
87+
88+
// CHANGE: install plan-to-git in generated project containers.
89+
// WHY: issue #369 requires agent plans to be captured and uploaded to pull requests.
90+
// QUOTE(ТЗ): "Надо что бы у нас план загружался в PR"
91+
// REF: issue-369
92+
// SOURCE: https://github.com/ProverCoderAI/plan-to-git/tree/v0.19.0
93+
// FORMAT THEOREM: image_build_success -> executable(/usr/local/bin/plan-to-git)
94+
// PURITY: SHELL
95+
// EFFECT: Docker build downloads and installs a pinned Rust CLI from GitHub.
96+
// INVARIANT: plan-to-git is available on PATH before Codex hooks or git post-push actions run.
97+
// COMPLEXITY: O(network + cargo_build)
98+
const renderDockerfilePlanToGit = (): string =>
99+
`# Install plan-to-git for Codex plan capture and PR sync (issue #369)
100+
RUN cargo install --git https://github.com/ProverCoderAI/plan-to-git --rev ${planToGitRevision} --locked --bins --root /usr/local \
101+
&& /usr/local/bin/plan-to-git --help >/dev/null`
102+
86103
/**
87-
* Renders the base image, package prelude, Rust toolchain, and browser module install.
104+
* Renders the base image, package prelude, Rust toolchain, browser module, and plan sync CLI install.
88105
*
89106
* @returns Dockerfile fragment that establishes the shared project container base.
90107
* @pure true
91108
* @effect none; CORE template renderer only constructs a string.
92-
* @invariant the returned fragment starts from the configured shared JS box image and installs the Rust browser lifecycle + MCP CLIs.
109+
* @invariant the returned fragment starts from the configured shared JS box image and installs the Rust browser lifecycle, MCP CLIs, and plan-to-git.
93110
* @precondition docker-git generated entrypoint remains the container entrypoint.
94-
* @postcondition the fragment keeps root available for setup and publishes both Rust browser binaries on PATH.
111+
* @postcondition the fragment keeps root available for setup and publishes Rust helper binaries on PATH.
95112
* @complexity O(1) time / O(1) space.
96113
*/
97114
export const renderDockerfilePrelude = (): string =>
98-
[renderDockerfileBase(), renderDockerfileRustBrowserConnection()].join("\n\n")
115+
[renderDockerfileBase(), renderDockerfileRustBrowserConnection(), renderDockerfilePlanToGit()].join("\n\n")

0 commit comments

Comments
 (0)