Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,12 +373,3 @@ describe("Message invariants", () => {
Каждый эффект — это контролируемое взаимодействие с реальным миром.

ПРИНЦИП: Сначала формализуем, потом программируем.

<!-- docker-git:issue-managed:start -->
Issue workspace: #61
Issue URL: https://github.com/ProverCoderAI/docker-git/issues/61
Workspace path: /home/dev/provercoderai/docker-git/issue-61

Работай только над этим issue, если пользователь не попросил другое.
Если нужен первоисточник требований, открой Issue URL.
<!-- docker-git:issue-managed:end -->
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ Force modes:

Agent context for issue workspaces:
- Global `${CODEX_HOME}/AGENTS.md` includes workspace path + issue/PR context.
- For `issue-*` workspaces, docker-git creates `${TARGET_DIR}/AGENTS.md` (if missing) with issue context and auto-adds it to `.git/info/exclude`.

## Projects Root Layout

Expand Down
15 changes: 12 additions & 3 deletions packages/docker-git/tests/core/templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ describe("planFiles", () => {
"GIT_CREDENTIAL_HELPER_PATH=\"/usr/local/bin/docker-git-credential-helper\""
)
expect(entrypointSpec.contents).toContain("token=\"$GITHUB_TOKEN\"")
expect(entrypointSpec.contents).toContain("issue_managed_start='<!-- docker-git:issue-managed:start -->'")
expect(entrypointSpec.contents).toContain("check_issue_managed_block_range")
expect(entrypointSpec.contents).toContain(
"push contains commit updating managed issue block in AGENTS.md"
)
expect(entrypointSpec.contents).toContain("CACHE_ROOT=\"/home/dev/.docker-git/.cache/git-mirrors\"")
expect(entrypointSpec.contents).toContain("PACKAGE_CACHE_ROOT=\"/home/dev/.docker-git/.cache/packages\"")
expect(entrypointSpec.contents).toContain("npm_config_store_dir")
Expand Down Expand Up @@ -149,13 +154,17 @@ describe("planFiles", () => {
if (entrypointSpec && entrypointSpec._tag === "File") {
expect(entrypointSpec.contents).toContain("Доступные workspace пути:")
expect(entrypointSpec.contents).toContain("Контекст workspace:")
expect(entrypointSpec.contents).toContain("Issue AGENTS.md:")
expect(entrypointSpec.contents).toContain("ISSUE_AGENTS_PATH=\"$TARGET_DIR/AGENTS.md\"")
expect(entrypointSpec.contents).toContain("grep -qx \"AGENTS.md\" \"$EXCLUDE_PATH\"")
expect(entrypointSpec.contents).toContain("docker_git_workspace_context_line()")
expect(entrypointSpec.contents).toContain("REPO_REF_VALUE=\"${REPO_REF:-issue-5}\"")
expect(entrypointSpec.contents).toContain("REPO_URL_VALUE=\"${REPO_URL:-https://github.com/org/repo.git}\"")
expect(entrypointSpec.contents).toContain("Контекст workspace: issue #$ISSUE_ID_VALUE ($ISSUE_URL_VALUE)")
expect(entrypointSpec.contents).not.toContain("ISSUE_AGENTS_HINT_LINE=")
expect(entrypointSpec.contents).not.toContain("Issue AGENTS.md: __TARGET_DIR__/AGENTS.md")
expect(entrypointSpec.contents).not.toContain("ISSUE_AGENTS_PATH=\"$TARGET_DIR/AGENTS.md\"")
expect(entrypointSpec.contents).not.toContain(
"ISSUE_MANAGED_START=\"<!-- docker-git:issue-managed:start -->\""
)
expect(entrypointSpec.contents).not.toContain("grep -qx \"AGENTS.md\" \"$EXCLUDE_PATH\"")
}
}))

Expand Down
4 changes: 0 additions & 4 deletions packages/lib/src/core/templates-entrypoint/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,6 @@ PROJECT_LINE="Рабочая папка проекта (git clone): __TARGET_DIR
WORKSPACES_LINE="Доступные workspace пути: __TARGET_DIR__"
WORKSPACE_INFO_LINE="Контекст workspace: repository"
FOCUS_LINE="Фокус задачи: работай только в workspace, который запрашивает пользователь. Текущий workspace: __TARGET_DIR__"
ISSUE_AGENTS_HINT_LINE="Issue AGENTS.md: n/a"
INTERNET_LINE="Доступ к интернету: есть. Если чего-то не знаешь — ищи в интернете или по кодовой базе."
if [[ "$REPO_REF" == issue-* ]]; then
ISSUE_ID="$(printf "%s" "$REPO_REF" | sed -E 's#^issue-##')"
Expand All @@ -222,7 +221,6 @@ if [[ "$REPO_REF" == issue-* ]]; then
else
WORKSPACE_INFO_LINE="Контекст workspace: issue #$ISSUE_ID"
fi
ISSUE_AGENTS_HINT_LINE="Issue AGENTS.md: __TARGET_DIR__/AGENTS.md"
elif [[ "$REPO_REF" == refs/pull/*/head ]]; then
PR_ID="$(printf "%s" "$REPO_REF" | sed -nE 's#^refs/pull/([0-9]+)/head$#\1#p')"
PR_URL=""
Expand All @@ -249,7 +247,6 @@ $PROJECT_LINE
$WORKSPACES_LINE
$WORKSPACE_INFO_LINE
$FOCUS_LINE
$ISSUE_AGENTS_HINT_LINE
$INTERNET_LINE
$MANAGED_END
EOF
Expand All @@ -270,7 +267,6 @@ $PROJECT_LINE
$WORKSPACES_LINE
$WORKSPACE_INFO_LINE
$FOCUS_LINE
$ISSUE_AGENTS_HINT_LINE
$INTERNET_LINE
$MANAGED_END
EOF
Expand Down
112 changes: 105 additions & 7 deletions packages/lib/src/core/templates-entrypoint/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,18 +81,112 @@ export const renderEntrypointGitConfig = (config: TemplateConfig): string =>
renderEntrypointGitIdentity(config)
].join("\n\n")

export const renderEntrypointGitHooks = (): string =>
String.raw`# 3) Install global git hooks to protect main/master
const entrypointGitHooksTemplate = String
.raw`# 3) Install global git hooks to protect main/master + managed AGENTS context
HOOKS_DIR="/opt/docker-git/hooks"
PRE_PUSH_HOOK="$HOOKS_DIR/pre-push"
mkdir -p "$HOOKS_DIR"
if [[ ! -f "$PRE_PUSH_HOOK" ]]; then
cat <<'EOF' > "$PRE_PUSH_HOOK"

cat <<'EOF' > "$PRE_PUSH_HOOK"
#!/usr/bin/env bash
set -euo pipefail

protected_branches=("refs/heads/main" "refs/heads/master")
allow_delete="${"${"}DOCKER_GIT_ALLOW_DELETE:-}"
zero_sha="0000000000000000000000000000000000000000"
issue_managed_start='<!-- docker-git:issue-managed:start -->'
issue_managed_end='<!-- docker-git:issue-managed:end -->'

extract_issue_block() {
local ref="$1"

if ! git cat-file -e "$ref" 2>/dev/null; then
return 0
fi

local awk_status=0
if ! git cat-file -p "$ref" | awk -v start="$issue_managed_start" -v end="$issue_managed_end" '
BEGIN { in_block = 0; found = 0 }
$0 == start { in_block = 1; found = 1 }
in_block == 1 { print }
$0 == end && in_block == 1 { in_block = 0; exit }
END {
if (found == 0) exit 3
if (in_block == 1) exit 2
}
'; then
awk_status=$?
if [[ "$awk_status" -eq 3 ]]; then
return 0
fi
return "$awk_status"
fi
}

commit_changes_issue_block() {
local commit="$1"
local parent=""
local commit_block=""
local parent_block=""

if ! git diff-tree --no-commit-id --name-only -r "$commit" -- AGENTS.md | grep -qx "AGENTS.md"; then
return 1
fi

if ! commit_block="$(extract_issue_block "$commit:AGENTS.md")"; then
return 2
fi

parent="$(git rev-list --parents -n 1 "$commit" | awk '{print $2}')"
if [[ -n "$parent" ]]; then
if ! parent_block="$(extract_issue_block "$parent:AGENTS.md")"; then
return 2
fi
fi

if [[ "$commit_block" != "$parent_block" ]]; then
return 0
fi
return 1
}

check_issue_managed_block_range() {
local local_sha="$1"
local remote_sha="$2"
local commits=""
local commit=""
local guard_status=0

if [[ "$local_sha" == "$zero_sha" ]]; then
return 0
fi

if [[ "$remote_sha" == "$zero_sha" ]]; then
commits="$(git rev-list "$local_sha" --not --remotes 2>/dev/null || true)"
if [[ -z "$commits" ]]; then
commits="$local_sha"
fi
else
commits="$(git rev-list "$remote_sha..$local_sha" 2>/dev/null || true)"
fi

for commit in $commits; do
commit_changes_issue_block "$commit"
guard_status=$?
if [[ "$guard_status" -eq 0 ]]; then
echo "docker-git: push contains commit updating managed issue block in AGENTS.md: $commit"
echo "docker-git: this block is runtime context and must stay outside repository history."
return 1
fi
if [[ "$guard_status" -eq 2 ]]; then
echo "docker-git: failed to parse managed issue block in AGENTS.md for commit $commit"
echo "docker-git: push blocked to prevent committing runtime workspace metadata."
return 1
fi
done

return 0
}

while read -r local_ref local_sha remote_ref remote_sha; do
if [[ -z "$remote_ref" ]]; then
Expand All @@ -105,15 +199,19 @@ while read -r local_ref local_sha remote_ref remote_sha; do
exit 1
fi
done
if [[ "$local_sha" == "0000000000000000000000000000000000000000" && "$remote_ref" == refs/heads/* ]]; then
if ! check_issue_managed_block_range "$local_sha" "$remote_sha"; then
exit 1
fi
if [[ "$local_sha" == "$zero_sha" && "$remote_ref" == refs/heads/* ]]; then
if [[ "$allow_delete" != "1" ]]; then
echo "docker-git: deleting remote branches is disabled (set DOCKER_GIT_ALLOW_DELETE=1 to override)."
exit 1
fi
fi
done
EOF
chmod 0755 "$PRE_PUSH_HOOK"
fi
chmod 0755 "$PRE_PUSH_HOOK"
git config --system core.hooksPath "$HOOKS_DIR" || true
git config --global core.hooksPath "$HOOKS_DIR" || true`

export const renderEntrypointGitHooks = (): string => entrypointGitHooksTemplate
87 changes: 1 addition & 86 deletions packages/lib/src/core/templates-entrypoint/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,89 +133,6 @@ const renderCloneCacheFinalize = (config: TemplateConfig): string =>
fi
fi`

const renderIssueWorkspaceAgentsResolve = (): string =>
`ISSUE_ID="$(printf "%s" "$REPO_REF" | sed -E 's#^issue-##')"
ISSUE_URL=""
if [[ "$REPO_URL" == https://github.com/* ]]; then
ISSUE_REPO="$(printf "%s" "$REPO_URL" | sed -E 's#^https://github.com/##; s#[.]git$##; s#/*$##')"
if [[ -n "$ISSUE_REPO" ]]; then
ISSUE_URL="https://github.com/$ISSUE_REPO/issues/$ISSUE_ID"
fi
fi
if [[ -z "$ISSUE_URL" ]]; then
ISSUE_URL="n/a"
fi`

const renderIssueWorkspaceAgentsManagedBlock = (): string =>
`ISSUE_AGENTS_PATH="$TARGET_DIR/AGENTS.md"
ISSUE_MANAGED_START="<!-- docker-git:issue-managed:start -->"
ISSUE_MANAGED_END="<!-- docker-git:issue-managed:end -->"
ISSUE_MANAGED_BLOCK="$(cat <<EOF
$ISSUE_MANAGED_START
Issue workspace: #$ISSUE_ID
Issue URL: $ISSUE_URL
Workspace path: $TARGET_DIR

Работай только над этим issue, если пользователь не попросил другое.
Если нужен первоисточник требований, открой Issue URL.
$ISSUE_MANAGED_END
EOF
)"`

const renderIssueWorkspaceAgentsWrite = (): string =>
`if [[ ! -e "$ISSUE_AGENTS_PATH" ]]; then
printf "%s\n" "$ISSUE_MANAGED_BLOCK" > "$ISSUE_AGENTS_PATH"
else
TMP_ISSUE_AGENTS_PATH="$(mktemp)"
if grep -qF "$ISSUE_MANAGED_START" "$ISSUE_AGENTS_PATH" && grep -qF "$ISSUE_MANAGED_END" "$ISSUE_AGENTS_PATH"; then
awk -v start="$ISSUE_MANAGED_START" -v end="$ISSUE_MANAGED_END" -v repl="$ISSUE_MANAGED_BLOCK" '
BEGIN { in_block = 0 }
$0 == start { print repl; in_block = 1; next }
$0 == end { in_block = 0; next }
in_block == 0 { print }
' "$ISSUE_AGENTS_PATH" > "$TMP_ISSUE_AGENTS_PATH"
else
sed \
-e '/^# docker-git issue workspace$/d' \
-e '/^Issue workspace: #/d' \
-e '/^Issue URL: /d' \
-e '/^Workspace path: /d' \
-e '/^Работай только над этим issue, если пользователь не попросил другое[.]$/d' \
-e '/^Если нужен первоисточник требований, открой Issue URL[.]$/d' \
"$ISSUE_AGENTS_PATH" > "$TMP_ISSUE_AGENTS_PATH"
if [[ -s "$TMP_ISSUE_AGENTS_PATH" ]]; then
printf "\n" >> "$TMP_ISSUE_AGENTS_PATH"
fi
printf "%s\n" "$ISSUE_MANAGED_BLOCK" >> "$TMP_ISSUE_AGENTS_PATH"
fi
mv "$TMP_ISSUE_AGENTS_PATH" "$ISSUE_AGENTS_PATH"
fi
if [[ -e "$ISSUE_AGENTS_PATH" ]]; then
chown 1000:1000 "$ISSUE_AGENTS_PATH" || true
fi`

const renderIssueWorkspaceAgentsExclude = (): string =>
`EXCLUDE_PATH="$TARGET_DIR/.git/info/exclude"
if [[ -f "$ISSUE_AGENTS_PATH" ]]; then
touch "$EXCLUDE_PATH"
if ! grep -qx "AGENTS.md" "$EXCLUDE_PATH"; then
printf "%s\n" "AGENTS.md" >> "$EXCLUDE_PATH"
fi
fi`

const renderIssueWorkspaceAgents = (): string =>
[
`if [[ "$CLONE_OK" -eq 1 && "$REPO_REF" == issue-* && -d "$TARGET_DIR/.git" ]]; then`,
renderIssueWorkspaceAgentsResolve(),
"",
renderIssueWorkspaceAgentsManagedBlock(),
"",
renderIssueWorkspaceAgentsWrite(),
"",
renderIssueWorkspaceAgentsExclude(),
"fi"
].join("\n")

const renderCloneBody = (config: TemplateConfig): string =>
[
renderCloneBodyStart(config),
Expand All @@ -224,9 +141,7 @@ const renderCloneBody = (config: TemplateConfig): string =>
"",
renderCloneRemotes(config),
"",
renderCloneCacheFinalize(config),
"",
renderIssueWorkspaceAgents()
renderCloneCacheFinalize(config)
].join("\n")

const renderCloneFinalize = (): string =>
Expand Down
Loading