Skip to content

Commit 814b1e2

Browse files
committed
fix: main 동기화 push에서 hook 예외를 분기 처리
- main과 develop tip이 같을 때 pre-push에서 동기화 publish를 허용 - 관련 hook 문서와 workflow 규칙을 branch-aware 정책으로 정리 - CLI 테스트와 workflow task evidence를 추가하고 closeout을 기록
1 parent 6b24ffc commit 814b1e2

13 files changed

Lines changed: 287 additions & 2 deletions

File tree

.codex/skills/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- 내부 런타임 구현은 `scripts/workflow_runtime/`이 가진다.
1212
- skill은 위 규칙을 복제하지 않고, 한 단계의 handoff와 CLI 진입점만 안내한다.
1313
- `python3 scripts/workflow.py init`는 runtime surface를 source 기준으로 다시 동기화하고, `doctor`는 drift를 실패로 보고한다.
14+
- `main` 브랜치의 `develop` 동기화 publish 예외처럼 branch-specific hook 정책은 `docs/hooks.md``.githooks/`가 소유하고, skill은 그 정책을 다시 정의하지 않는다.
1415

1516
## Active Skill Set
1617

.githooks/pre-push

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ set -eu
44
python3 scripts/workflow.py doctor
55
python3 scripts/workflow.py hook pre_command --command-text "git push $*"
66

7+
current_branch="$(git branch --show-current 2>/dev/null || true)"
8+
if [ "$current_branch" = "main" ]; then
9+
main_tip="$(git rev-parse HEAD 2>/dev/null || true)"
10+
develop_tip="$(git rev-parse --verify refs/heads/develop 2>/dev/null || true)"
11+
if [ -n "$main_tip" ] && [ "$main_tip" = "$develop_tip" ]; then
12+
exit 0
13+
fi
14+
fi
15+
716
task_id="${WORKFLOW_TASK_ID:-}"
817
if [ -n "$task_id" ]; then
918
phase_id="${WORKFLOW_PHASE_ID:-}"

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ NEVER: control plane 문서를 앱 source-of-truth보다 우선시하지 않는
4747
9. 모든 phase가 완료되면 `review`가 review readiness를 기록한다.
4848
10. `review --close --user-validation-note ...`만 최종 완료를 닫을 수 있다.
4949
11. 실패, block, 추가 요구사항이 생기면 `reopen`으로 target phase를 `pending`으로 되돌리고 repair loop를 재개한다.
50+
12. `pre_push`의 branch-aware 예외는 현재 브랜치가 `main`이고 로컬 `main` tip이 로컬 `develop` tip과 같을 때의 동기화 publish에만 허용한다.
5051

5152
## TDD Contract
5253

docs/hooks.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838

3939
- `pre_review`에서는 active phase에 대한 latest passed verification을 요구한다.
4040
- `pre_push`에서는 unpushed diff를 기준으로 task를 먼저 해석하고, 그 scope와 겹칠 때 latest passed verification을 요구한다.
41+
- 단, `.githooks/pre-push`는 현재 브랜치가 `main`이고 로컬 `main` tip이 로컬 `develop` tip과 같으면 task 추론 전에 동기화 publish로 간주하고 통과시킨다.
4142
- active task가 없더라도 unpushed diff가 정확히 하나의 completed task scope에 매핑되면 그 task를 push gate context로 재사용한다.
4243
- unpushed diff가 어느 task에도 단일하게 매핑되지 않으면 `pre_push`는 fail-closed 한다.
4344

@@ -64,4 +65,4 @@ git hook만으로는 충분하지 않다. 로컬 CLI와 phase runner가 같은 h
6465
`init``.githooks/pre-commit`, `.githooks/pre-push`, `workflows/system/hooks.json`을 source 기준으로 다시 동기화한다.
6566
`doctor`는 위 runtime surface가 source와 drift하면 실패하고 `python3 scripts/workflow.py init`를 다시 요구한다.
6667
`pre_commit`은 active task가 하나로 추론되지 않으면 fail-closed 한다. 이때는 `WORKFLOW_TASK_ID` 또는 `--task-id`로 task binding을 명시해야 한다.
67-
`pre_push`는 먼저 unpushed diff를 task scope에 매핑한다. scope가 단일 task로 정해지지 않으면 역시 fail-closed 한다.
68+
`pre_push`는 먼저 현재 브랜치가 `main`인지와 로컬 `main`/`develop` tip 일치를 확인한다. 이 동기화 publish가 아니면 기존처럼 unpushed diff를 task scope에 매핑하고, scope가 단일 task로 정해지지 않으면 fail-closed 한다.

docs/runbook.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,5 @@ python3 scripts/workflow.py hook pre_push --command-text "git push origin featur
7272
```
7373

7474
`pre_commit`은 active task가 여러 개라면 자동 추론을 중단하고 실패한다. 이 경우 `WORKFLOW_TASK_ID` 또는 `--task-id`로 명시적으로 task binding을 넣는다.
75-
`pre_push`는 먼저 unpushed diff를 task scope에 매핑한다. active task가 없어도 completed task scope 하나로 매핑되면 그 task를 재사용하고, 단일 task로 정해지지 않으면 실패한다.
75+
`pre_push`는 먼저 현재 브랜치가 `main`인지와 로컬 `main`/`develop` tip이 같은지 확인한다. 이 조건을 만족하면 `develop` 동기화 publish로 보고 task-bound guard를 건너뛴다.
76+
그 외 경우에는 기존처럼 unpushed diff를 task scope에 매핑한다. active task가 없어도 completed task scope 하나로 매핑되면 그 task를 재사용하고, 단일 task로 정해지지 않으면 실패한다.

tests/test_workflow_cli.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,41 @@ def test_githook_pre_push_blocks_force_push(self) -> None:
696696
result = self.run_hook_script(root, "pre-push", "origin", "feature", "--force", expected=1)
697697
self.assertIn("blocked command", result.stdout)
698698

699+
def test_githook_pre_push_passes_for_main_sync_publish_when_head_matches_develop(self) -> None:
700+
with tempfile.TemporaryDirectory() as tmp:
701+
root = Path(tmp)
702+
self.init_git_repo(root)
703+
self.install_runtime_surface(root)
704+
self.run_cli(root, "init")
705+
706+
self.git_add(root, ".")
707+
self.git_commit_no_verify(root, "bootstrap runtime surface")
708+
subprocess.run(["git", "branch", "-M", "main"], cwd=root, capture_output=True, text=True, check=True)
709+
subprocess.run(["git", "branch", "develop", "HEAD"], cwd=root, capture_output=True, text=True, check=True)
710+
711+
result = self.run_hook_script(root, "pre-push", "origin", "main")
712+
self.assertIn("command allowed", result.stdout)
713+
self.assertNotIn("requires explicit --task-id", result.stderr)
714+
715+
def test_githook_pre_push_keeps_task_guard_when_main_differs_from_develop(self) -> None:
716+
with tempfile.TemporaryDirectory() as tmp:
717+
root = Path(tmp)
718+
self.init_git_repo(root)
719+
self.install_runtime_surface(root)
720+
self.run_cli(root, "init")
721+
722+
self.git_add(root, ".")
723+
self.git_commit_no_verify(root, "bootstrap runtime surface")
724+
subprocess.run(["git", "branch", "-M", "main"], cwd=root, capture_output=True, text=True, check=True)
725+
subprocess.run(["git", "branch", "develop", "HEAD"], cwd=root, capture_output=True, text=True, check=True)
726+
727+
(root / "notes.txt").write_text("main diverged from develop\n", encoding="utf-8")
728+
self.git_add(root, "notes.txt")
729+
self.git_commit_no_verify(root, "main diverged")
730+
731+
result = self.run_hook_script(root, "pre-push", "origin", "main", expected=1)
732+
self.assertIn("do not map to a single task", result.stderr)
733+
699734
def test_githook_pre_push_requires_verification_for_unpushed_scope_changes(self) -> None:
700735
with tempfile.TemporaryDirectory() as tmp:
701736
root = Path(tmp)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"task_id": "task-main-branch-hook-exemption",
3+
"generated_at": "2026-04-16T04:36:44+00:00",
4+
"phases": [
5+
{
6+
"id": "phase-1",
7+
"title": "branch-aware-main-publish-hook",
8+
"goal": "Allow main sync publishes to bypass task-scoped pre-push enforcement only when main tip matches develop tip, while keeping other hook behavior unchanged.",
9+
"inputs": [
10+
"workflows/tasks/task-main-branch-hook-exemption/spec.md",
11+
"docs/hooks.md",
12+
"tests/test_workflow_cli.py"
13+
],
14+
"allowed_write_paths": [
15+
".githooks/",
16+
"docs/",
17+
"tests/",
18+
".codex/skills/",
19+
"AGENTS.md"
20+
],
21+
"acceptance": {
22+
"commands": [
23+
"python3 -m unittest tests.test_workflow_cli -v",
24+
"python3 scripts/workflow.py doctor"
25+
]
26+
},
27+
"test_policy": {
28+
"mode": "require_tests",
29+
"evidence": []
30+
},
31+
"order": 1,
32+
"status": "completed",
33+
"retry_count": 0
34+
}
35+
]
36+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"id": "20260416T044014-921f70aa",
3+
"task_id": "task-main-branch-hook-exemption",
4+
"phase_id": "phase-1",
5+
"event": "phase_completion",
6+
"commands": [
7+
{
8+
"command": "phase completion",
9+
"status": "passed",
10+
"output": ".codex/skills/README.md, .githooks/pre-push, AGENTS.md, docs/hooks.md, docs/runbook.md, tests/test_workflow_cli.py, workflows/tasks/task-main-branch-hook-exemption/phases.json, workflows/tasks/task-main-branch-hook-exemption/spec.md, workflows/tasks/task-main-branch-hook-exemption/task.json"
11+
}
12+
],
13+
"result": "passed",
14+
"evidence": [],
15+
"error_fingerprint": null,
16+
"next_action": "verify",
17+
"timestamp": "2026-04-16T04:40:14+00:00"
18+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"id": "20260416T044031-46f9f315",
3+
"task_id": "task-main-branch-hook-exemption",
4+
"phase_id": "phase-1",
5+
"event": "verification",
6+
"commands": [
7+
{
8+
"command": "python3 -m unittest tests.test_workflow_cli -v",
9+
"status": "passed",
10+
"output": "test_approve_requires_structured_socratic_log_and_locks_intake (tests.test_workflow_cli.WorkflowCliTest.test_approve_requires_structured_socratic_log_and_locks_intake) ... ok\ntest_check_fails_when_approved_task_points_to_completed_phase (tests.test_workflow_cli.WorkflowCliTest.test_check_fails_when_approved_task_points_to_completed_phase) ... ok\ntest_circuit_breaker_triggers_from_verification_failures (tests.test_workflow_cli.WorkflowCliTest.test_circuit_breaker_triggers_from_verification_failures) ... ok\ntest_dangerous_command_guard_blocks_short_force_push (tests.test_workflow_cli.WorkflowCli..."
11+
},
12+
{
13+
"command": "python3 scripts/workflow.py doctor",
14+
"status": "passed",
15+
"output": "{\n \"status\": \"passed\",\n \"checks\": [\n \"workflow system hook config is readable\",\n \"git core.hooksPath points to .githooks\",\n \"task artifacts are internally consistent\",\n \"runtime surface matches source\",\n \"AGENTS constitution is complete\",\n \"no stale legacy references remain in control-plane files\"\n ],\n \"errors\": []\n}"
16+
}
17+
],
18+
"result": "passed",
19+
"evidence": [],
20+
"error_fingerprint": null,
21+
"next_action": "review",
22+
"timestamp": "2026-04-16T04:40:31+00:00"
23+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"id": "20260416T044034-57b90731",
3+
"task_id": "task-main-branch-hook-exemption",
4+
"phase_id": "phase-1",
5+
"event": "review_ready",
6+
"commands": [
7+
{
8+
"command": "review gate",
9+
"status": "passed",
10+
"output": "latest passed verification: 20260416T044031-46f9f315"
11+
}
12+
],
13+
"result": "passed",
14+
"evidence": [
15+
"main==develop 동기화 publish 예외 검토 완료"
16+
],
17+
"error_fingerprint": null,
18+
"next_action": "user validation",
19+
"timestamp": "2026-04-16T04:40:34+00:00"
20+
}

0 commit comments

Comments
 (0)