feat: auto-delete containers whose issue or PR is closed#394
Conversation
Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: ProverCoderAI#117
📝 WalkthroughSummary by CodeRabbitВыпуск
ОбзорPR реализует опциональный механизм фонового автоудаления Docker-контейнеров проектов, исходные GitHub issues/pull requests которых находятся в закрытом состоянии. Удаление блокируется консервативной политикой: если состояние источника открыто/неизвестно или есть активные агенты/интерактивные сессии, проект сохраняется. ИзмененияАвтоудаление закрытого источника
Оценка сложности рецензирования🎯 3 (Умеренная) | ⏱️ ~25 минут Возможно связанные PR
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 error)
✅ Passed checks (6 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Working session summaryThe "Check" workflow is the remaining one. I'll wait for the background monitor ( Meanwhile, here's a summary of where things stand: Issue #117 — auto-delete containers for closed issues/PRs — implementation complete:
Verification: lib 300/300 + api 192/192 tests pass; new tests added (19 + 7 cases); lib CI so far: Final Build ✅, Checking Dependencies ✅, Snapshot ✅ — only "Check" still running. PR #394 is updated (title + description) and marked ready for review. This summary was automatically extracted from the AI working session output. |
🤖 Solution Draft LogThis log file contains the complete execution trace of the AI solution draft process. 💰 Cost: $7.013169📊 Context and tokens usage:Claude Opus 4.8: (2 sub-sessions)
Total: (16.1K new + 225.0K cache writes + 8.1M cache reads) input tokens, 58.4K output tokens, $7.013169 cost 🤖 Models used:
📎 Log file uploaded as Gist (3117KB)Now working session is ended, feel free to review and add any feedback on the solution draft. |
Cancelled CI/CD Requires ReviewHive Mind detected cancelled or stale CI/CD checks and cannot get them running automatically. Commit: 2e4**********************************45e Cancelled checks
Workflow runs inspected
Automatic re-run result
Action required
If workflow/job Hive Mind is stopping because continuing to poll the same cancelled/stale check would not change the mergeability result. |
This reverts commit adacb6a.
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/api/src/services/project-activity.ts`:
- Around line 27-32: The code masks readContainerTaskSnapshot failures by
catching all errors and returning false (via Effect.catchAll(() =>
Effect.succeed(false))), which makes the caller think "no active agent" and can
cause unsafe auto-deletion; change the behavior in the pipeline that uses
readContainerTaskSnapshot and activeAgents so errors are propagated instead of
swallowed — remove or modify the Effect.catchAll on the
readContainerTaskSnapshot pipeline (or replace it with an explicit
failure/unknown/keep result) so that failures from readContainerTaskSnapshot
surface to the caller (or return an explicit "unknown"/"keep" sentinel) and let
the higher-level evaluator decide rather than forcing false.
In `@packages/api/tests/project-closed-source-policy.test.ts`:
- Around line 1-57: Add a fast-check property test that exhaustively verifies
the decideProjectClosedSourceAction invariant by generating arbitrary
ProjectClosedSourcePolicyInput values (use fast-check) and asserting the policy
rules: when sourceState === "closed" && !hasActiveAgent &&
!hasLiveInteractiveSession result._tag === "Delete"; when sourceState === "open"
|| sourceState === "unknown" result._tag === "Keep" with reason
"source-open-or-unknown"; when hasActiveAgent result._tag === "Keep" with reason
"active-agent" (and this should take precedence over hasLiveInteractiveSession
which yields "live-interactive-session"); import and use
decideProjectClosedSourceAction and the ProjectClosedSourcePolicyInput type in
the test file and keep the existing example tests.
In `@packages/lib/src/core/project-source-ref.ts`:
- Around line 20-21: The ProjectSourceRef contract for GitLab is inconsistent:
the docstring states owner is the namespace (e.g. "group/subgroup") but the code
sets owner to gitlab.projectPath (namespace/repo). Update ProjectSourceRef so
owner consistently carries only the namespace and the repository name goes into
a separate field (e.g., repo or name), or adjust the docstring to match the
current behavior. Locate the ProjectSourceRef type/constructor and the place
assigning gitlab.projectPath (reference gitlab.projectPath and the owner
property) and change the assignment and/or documentation so owner = namespace
only and repository = repoName, ensuring any code consumers are updated to use
the new repo field.
In `@packages/lib/src/usecases/project-source-state.ts`:
- Around line 19-20: Инвариант в строках 19‑20 должен быть уточнён: замените
формулировку "never throws on API/HTTP failure — unknown is returned so deletion
is skipped" на указание, что fetchGithubSourceState может завершаться ошибкой в
error‑channel (PlatformError) и не выполняет локального преобразования ошибок в
"unknown"; свёртка в "unknown" происходит в оркестраторе (например в проекте
AutoDelete через Effect.catchAll(() => Effect.succeed("unknown"))). Ссылаться в
комментарии явно на fetchGithubSourceState и на поведение обработчика в
project-auto-delete (Effect.catchAll), чтобы контракт был корректно понят при
чтении кода.
In `@packages/lib/tests/core/project-source-ref.test.ts`:
- Around line 9-115: Add fast-check property-based tests for
parseProjectSourceRef, normalizeProjectSourceState, and
shouldDeleteForSourceState: write fc.property cases that (1) assert trimming
whitespace around ref does not change the parsed .number for
parseProjectSourceRef and that known GitHub/GitLab ref patterns produce the
expected object shape ({provider, owner, repo, kind, number}) while branch-like
or unknown patterns (including a GitHub repo with refs/merge-requests/... )
return null; (2) assert normalizeProjectSourceState is idempotent, case- and
whitespace-insensitive, maps "open"/"opened"→"open",
"closed"/"merged"/"locked"→"closed", and null/undefined/other→"unknown"; and (3)
assert shouldDeleteForSourceState(state) === (normalizeProjectSourceState(state)
=== "closed") for arbitrary string/null/undefined inputs; use
parseProjectSourceRef, normalizeProjectSourceState, and
shouldDeleteForSourceState identifiers and fc.property with appropriate
generators (fc.string, fc.option, fc.oneof, and custom generators for valid ref
patterns) to implement these invariants.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: b8711bdb-0097-4739-913f-4a74be709e7f
📒 Files selected for processing (11)
.changeset/auto-delete-closed-source.mdpackages/api/src/program.tspackages/api/src/services/project-activity.tspackages/api/src/services/project-auto-delete.tspackages/api/src/services/project-auto-suspend.tspackages/api/src/services/project-closed-source-policy.tspackages/api/tests/project-closed-source-policy.test.tspackages/lib/src/core/domain.tspackages/lib/src/core/project-source-ref.tspackages/lib/src/usecases/project-source-state.tspackages/lib/tests/core/project-source-ref.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
- GitHub Check: E2E (OpenCode)
- GitHub Check: E2E (Clone cache)
- GitHub Check: Lint
- GitHub Check: Test
- GitHub Check: E2E (Runtime volumes + SSH)
- GitHub Check: E2E (Login context)
- GitHub Check: E2E (Clone auto-open SSH)
- GitHub Check: E2E (Browser command)
- GitHub Check: Final build (windows-latest)
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Implement Functional Core, Imperative Shell (FCIS) pattern: CORE layer contains only pure functions with immutable data and mathematical operations; SHELL layer isolates all effects (IO, network, database). Strict dependency direction: SHELL → CORE (never reverse).
Never useany,unknown,eslint-disable,ts-ignore, orastype assertions (except in rigorously justified cases with documentation). Always use exhaustive union type analysis through.exhaustive()pattern matching.
All external dependencies must be wrapped through typed interfaces and injected via Effect-TS Layer pattern. Never call external services directly from CORE functions.
Use monadic composition with Effect-TS for all effects:Effect<Success, Error, Requirements>. Compose effects throughpipe()andEffect.flatMap(). Implement dependency injection via Layer pattern. Handle errors without try/catch blocks.
All functions must be pure in the CORE layer: no side effects (logging, console output, IO operations, mutations). Separate all side effects into the SHELL layer.
Use exhaustive pattern matching with Effect.Match instead of switch statements. Example:Match.value(item).pipe(Match.when(...), Match.exhaustive).
Document all functions with comprehensive TSDoc including:@pure(true/false),@effect(required services),@invariant(mathematical invariants),@precondition,@postcondition,@complexity(time and space),@throwsNever (errors must be typed in Effect).
Use functional comment markers for code clarity: CHANGE (brief description), WHY (mathematical/architectural justification), QUOTE(ТЗ) (requirement citation), REF (RTM or message ID), SOURCE (external source with quote), FORMAT THEOREM (∀x ∈ Domain: P(x) → Q(f(x))), PURITY (CORE|SHELL), EFFECT (Effect type signature), INVARIANT (mathematical invariant), COMPLEXITY (time/space).
Define all external service dependencies as Context.Tag classes with fully typed methods returning Effect types. Example: `class Da...
Files:
packages/api/src/services/project-closed-source-policy.tspackages/lib/src/core/domain.tspackages/api/src/services/project-activity.tspackages/api/tests/project-closed-source-policy.test.tspackages/api/src/program.tspackages/lib/src/usecases/project-source-state.tspackages/api/src/services/project-auto-delete.tspackages/lib/src/core/project-source-ref.tspackages/lib/tests/core/project-source-ref.test.tspackages/api/src/services/project-auto-suspend.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx,js,jsx}: Forbidden constructs in CORE code:any,eslint-disable,ts-ignore,async/await, raw Promise chains (then/catch),Promise.all,try/catchfor logic control,console.*, switch statements (use Match with .exhaustive() instead)
All functions must use Effect-TS for composing effects:Effect<Success, Error, Requirements>. No direct async/await, Promise chains, or try/catch in product logic.
Functional comments must include: CHANGE, WHY, QUOTE(ТЗ) or n/a, REF, SOURCE or n/a, FORMAT THEOREM, PURITY (CORE|SHELL), EFFECT signature for SHELL functions, INVARIANT, and COMPLEXITY.
All data mutations must use immutable patterns (ReadonlyArray, readonly properties, Object.freeze); mutation in SHELL only when absolutely necessary and documented.
Files:
packages/api/src/services/project-closed-source-policy.tspackages/lib/src/core/domain.tspackages/api/src/services/project-activity.tspackages/api/tests/project-closed-source-policy.test.tspackages/api/src/program.tspackages/lib/src/usecases/project-source-state.tspackages/api/src/services/project-auto-delete.tspackages/lib/src/core/project-source-ref.tspackages/lib/tests/core/project-source-ref.test.tspackages/api/src/services/project-auto-suspend.ts
**/*.{sh,bash,py,js,ts,jsx,tsx,go,java,rb,php}
📄 CodeRabbit inference engine (Custom checks)
Fail if changed files introduce command injection or unsafe shell/process execution with user-controlled input
Files:
packages/api/src/services/project-closed-source-policy.tspackages/lib/src/core/domain.tspackages/api/src/services/project-activity.tspackages/api/tests/project-closed-source-policy.test.tspackages/api/src/program.tspackages/lib/src/usecases/project-source-state.tspackages/api/src/services/project-auto-delete.tspackages/lib/src/core/project-source-ref.tspackages/lib/tests/core/project-source-ref.test.tspackages/api/src/services/project-auto-suspend.ts
**/*.{py,js,ts,jsx,tsx,go,java,rb,php,sh,bash,c,cpp}
📄 CodeRabbit inference engine (Custom checks)
Fail if changed files introduce path traversal or writes outside intended project/container state directories
Files:
packages/api/src/services/project-closed-source-policy.tspackages/lib/src/core/domain.tspackages/api/src/services/project-activity.tspackages/api/tests/project-closed-source-policy.test.tspackages/api/src/program.tspackages/lib/src/usecases/project-source-state.tspackages/api/src/services/project-auto-delete.tspackages/lib/src/core/project-source-ref.tspackages/lib/tests/core/project-source-ref.test.tspackages/api/src/services/project-auto-suspend.ts
**/*.{js,ts,jsx,tsx,py,java,go,rb,php,sh,bash,yml,yaml,json,env*,toml,cfg,config,dockerfile,dockerignore}
📄 CodeRabbit inference engine (Custom checks)
Fail if changed files expose credentials, tokens, private-keys, or PII in source, generated config, logs, or CI output
Files:
packages/api/src/services/project-closed-source-policy.tspackages/lib/src/core/domain.tspackages/api/src/services/project-activity.tspackages/api/tests/project-closed-source-policy.test.tspackages/api/src/program.tspackages/lib/src/usecases/project-source-state.tspackages/api/src/services/project-auto-delete.tspackages/lib/src/core/project-source-ref.tspackages/lib/tests/core/project-source-ref.test.tspackages/api/src/services/project-auto-suspend.ts
**/*
⚙️ CodeRabbit configuration file
**/*: Ты строгий ревьюер SPEC DRIVEN DEVELOPMENT.Перед выводами изучи README.md, другие *.md файлы, linked issues,
PR description, PR comments/discussion и релевантную кодовую базу.Сверь изменения с исходным ТЗ/спекой и обсуждением. Флагай любой уход
от спеки, недокументированное изменение поведения, отсутствие тестов
для заявленного поведения и security-риск. Если спека не видна,
попроси автора добавить ее в issue или PR description.Проверь решение с точки зрения формальной верификации: какие инварианты,
предусловия и постусловия можно доказать математически, а где доказуемость
слабая. Оцени решение с точки зрения теории игр: устойчивы ли стимулы,
нет ли выгодного обхода правил, и какое решение было бы сильнее.
Files:
packages/api/src/services/project-closed-source-policy.tspackages/lib/src/core/domain.tspackages/api/src/services/project-activity.tspackages/api/tests/project-closed-source-policy.test.tspackages/api/src/program.tspackages/lib/src/usecases/project-source-state.tspackages/api/src/services/project-auto-delete.tspackages/lib/src/core/project-source-ref.tspackages/lib/tests/core/project-source-ref.test.tspackages/api/src/services/project-auto-suspend.ts
**
⚙️ CodeRabbit configuration file
**: РОЛЬ: Математик-программист, специализирующийся на формально верифицируемой функциональной архитектуре.ЦЕЛЬ: Создавать математически доказуемые решения через функциональную парадигму с полным разделением чистых вычислений и контролируемых эффектов.
МОДЕЛЬ РАССУЖДЕНИЯ:
- Не выдавать “личные мнения”. Формировать вывод как результат симуляции профессионального обсуждения релевантных ролей
(архитектор Effect/FP, ревьюер типов, страж CORE↔SHELL, тест-инженер).- Если запрос сформулирован как “что думаешь”, отвечать в терминах аргументов ролей и выбирать решение
по критериям инвариантов, типовой безопасности и тестируемости (если пользователь явно просит выбор — выбрать и обосновать).ПРАВИЛО ПРОЦЕССА (НЕ ФОРМАТ ОТВЕТА):
В начале работы (внутренне) формулировать Deep Research вопрос:
"I am looking for code that does , is there existing code that can do this?"
Далее:
- если доступен проект/код — сперва искать и переиспользовать существующие паттерны (минимальный корректный diff),
- если проект недоступен — опираться на предоставленный контекст и явно фиксировать допущения,
- код писать только после формального понимания задачи (типы/инварианты → архитектура → код → тесты),
- источники указывать только если реально использован внешний материал; иначе
SOURCE: n/a.ИНСТРУМЕНТАЛЬНОЕ ПОВЕДЕНИЕ (ОБЯЗАТЕЛЬНО, НЕ ФОРМАТ ОТВЕТА):
- Агент всегда использует доступные инструменты среды (терминал, поиск по проекту, запуск тестов/скриптов, анализ сборки, web-ресёрч при необходимости)
для ресёрча, проверки гипотез и выполнения действий. Приоритет: проверяемость, воспроизводимость, минимальный риск.- Агент не предлагает “гайд” как замену действия. Если действие возможно выполнить инструментами — агент выполняет его сам,
затем сообщает, что было сделано и как повторить.- Любые инструкции (команды/процедуры) агент даёт только после собственной проверки на доступной среде.
Если проверить невозможно — явно фиксирует ограничение и перечисляе...
Files:
packages/api/src/services/project-closed-source-policy.tspackages/lib/src/core/domain.tspackages/api/src/services/project-activity.tspackages/api/tests/project-closed-source-policy.test.tspackages/api/src/program.tspackages/lib/src/usecases/project-source-state.tspackages/api/src/services/project-auto-delete.tspackages/lib/src/core/project-source-ref.tspackages/lib/tests/core/project-source-ref.test.tspackages/api/src/services/project-auto-suspend.ts
**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.test.{ts,tsx}: Implement property-based testing using fast-check for mathematical properties and invariants. Example:fc.property(fc.array(messageArbitrary), (messages) => isChronologicallySorted(sortMessagesByTimestamp(messages))).
Mock external dependencies in unit tests using Effect's testing utilities. Run tests without Effect runtime for speed. Example:Effect.provide(MockService), Effect.runPromise.
Files:
packages/api/tests/project-closed-source-policy.test.tspackages/lib/tests/core/project-source-ref.test.ts
**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Property-based tests (fast-check) must verify mathematical invariants; unit tests must use Effect test utilities without async/await.
Files:
packages/api/tests/project-closed-source-policy.test.tspackages/lib/tests/core/project-source-ref.test.ts
🧠 Learnings (1)
📚 Learning: 2026-05-18T07:43:38.131Z
Learnt from: skulidropek
Repo: ProverCoderAI/docker-git PR: 313
File: packages/api/src/services/panel-cloudflare-tunnel.ts:123-135
Timestamp: 2026-05-18T07:43:38.131Z
Learning: In this repo’s ProverCoderAI/docker-git architecture, it is intentional for service modules under packages/api/src/services/ to call Node.js APIs directly (e.g., fs, child_process, fetch). Do not treat direct Node API usage in these service modules as a dependency-injection (DI) violation. This codebase provides the NodeContext at the program boundary (entry point) rather than injecting Context.Tag/Layer services into individual service modules; reviewers should only flag DI/context issues if the expected boundary setup is missing.
Applied to files:
packages/api/src/services/project-closed-source-policy.tspackages/api/src/services/project-activity.tspackages/api/src/services/project-auto-delete.tspackages/api/src/services/project-auto-suspend.ts
🪛 markdownlint-cli2 (0.22.1)
.changeset/auto-delete-closed-source.md
[warning] 5-5: First line in a file should be a top-level heading
(MD041, first-line-heading, first-line-h1)
🪛 OpenGrep (1.22.0)
packages/lib/src/core/project-source-ref.ts
[ERROR] 43-43: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.
(coderabbit.command-injection.exec-js)
[ERROR] 47-47: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.
(coderabbit.command-injection.exec-js)
[ERROR] 55-55: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.
(coderabbit.command-injection.exec-js)
[ERROR] 59-59: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.
(coderabbit.command-injection.exec-js)
🔇 Additional comments (2)
packages/lib/src/core/domain.ts (1)
29-39: LGTM!.changeset/auto-delete-closed-source.md (1)
1-5: LGTM!
| readContainerTaskSnapshot(project.projectDir, false).pipe( | ||
| Effect.map((snapshot) => | ||
| activeAgents(snapshot.agents).length > 0 || snapshot.tasks.some((task) => task.kind === "agent") | ||
| ), | ||
| Effect.catchAll(() => Effect.succeed(false)) | ||
| ) |
There was a problem hiding this comment.
Не маскируйте сбой чтения снапшота как false для признака активного агента.
На Line 31 ошибка чтения состояния контейнера принудительно превращается в false. В контуре авто-удаления это даёт ложный вывод «агента нет» и может привести к удалению проекта при недоступности проверки. Для безопасного поведения ошибку нужно пробрасывать вверх (или явно трактовать как keep/unknown), чтобы оценка проекта завершалась без удаления.
💡 Proposed fix
export const projectHasActiveAgent = (
project: ProjectItem
) =>
readContainerTaskSnapshot(project.projectDir, false).pipe(
- Effect.map((snapshot) =>
- activeAgents(snapshot.agents).length > 0 || snapshot.tasks.some((task) => task.kind === "agent")
- ),
- Effect.catchAll(() => Effect.succeed(false))
+ Effect.map(
+ (snapshot) =>
+ activeAgents(snapshot.agents).length > 0 || snapshot.tasks.some((task) => task.kind === "agent")
+ )
)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/api/src/services/project-activity.ts` around lines 27 - 32, The code
masks readContainerTaskSnapshot failures by catching all errors and returning
false (via Effect.catchAll(() => Effect.succeed(false))), which makes the caller
think "no active agent" and can cause unsafe auto-deletion; change the behavior
in the pipeline that uses readContainerTaskSnapshot and activeAgents so errors
are propagated instead of swallowed — remove or modify the Effect.catchAll on
the readContainerTaskSnapshot pipeline (or replace it with an explicit
failure/unknown/keep result) so that failures from readContainerTaskSnapshot
surface to the caller (or return an explicit "unknown"/"keep" sentinel) and let
the higher-level evaluator decide rather than forcing false.
| import { describe, expect, it } from "vitest" | ||
|
|
||
| import { | ||
| decideProjectClosedSourceAction, | ||
| type ProjectClosedSourcePolicyInput | ||
| } from "../src/services/project-closed-source-policy.js" | ||
|
|
||
| const makeInput = (overrides: Partial<ProjectClosedSourcePolicyInput> = {}): ProjectClosedSourcePolicyInput => ({ | ||
| sourceState: "closed", | ||
| hasActiveAgent: false, | ||
| hasLiveInteractiveSession: false, | ||
| ...overrides | ||
| }) | ||
|
|
||
| describe("project closed-source policy", () => { | ||
| it("deletes a closed-source project with no active work", () => { | ||
| expect(decideProjectClosedSourceAction(makeInput())).toEqual({ _tag: "Delete" }) | ||
| }) | ||
|
|
||
| it("keeps an open-source project", () => { | ||
| expect(decideProjectClosedSourceAction(makeInput({ sourceState: "open" }))).toEqual({ | ||
| _tag: "Keep", | ||
| reason: "source-open-or-unknown" | ||
| }) | ||
| }) | ||
|
|
||
| it("keeps a project whose source state is unknown", () => { | ||
| expect(decideProjectClosedSourceAction(makeInput({ sourceState: "unknown" }))).toEqual({ | ||
| _tag: "Keep", | ||
| reason: "source-open-or-unknown" | ||
| }) | ||
| }) | ||
|
|
||
| it("keeps a closed-source project while an agent is active", () => { | ||
| expect(decideProjectClosedSourceAction(makeInput({ hasActiveAgent: true }))).toEqual({ | ||
| _tag: "Keep", | ||
| reason: "active-agent" | ||
| }) | ||
| }) | ||
|
|
||
| it("keeps a closed-source project while an interactive session is live", () => { | ||
| expect(decideProjectClosedSourceAction(makeInput({ hasLiveInteractiveSession: true }))).toEqual({ | ||
| _tag: "Keep", | ||
| reason: "live-interactive-session" | ||
| }) | ||
| }) | ||
|
|
||
| it("prefers the active-agent reason over a live interactive session", () => { | ||
| expect( | ||
| decideProjectClosedSourceAction(makeInput({ hasActiveAgent: true, hasLiveInteractiveSession: true })) | ||
| ).toEqual({ _tag: "Keep", reason: "active-agent" }) | ||
| }) | ||
|
|
||
| it("never deletes an open project even with no active work", () => { | ||
| expect(decideProjectClosedSourceAction(makeInput({ sourceState: "open" }))._tag).toBe("Keep") | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Добавьте property-based тест инварианта для политики удаления.
На Line 15-57 есть только примерные сценарии. Для .test.ts здесь нужен тест инварианта через fast-check, чтобы проверить правило удаления на всём пространстве входов, а не только на фиксированных кейсах.
💡 Proposed fix
import { describe, expect, it } from "vitest"
+import fc from "fast-check"
@@
describe("project closed-source policy", () => {
@@
it("never deletes an open project even with no active work", () => {
expect(decideProjectClosedSourceAction(makeInput({ sourceState: "open" }))._tag).toBe("Keep")
})
+
+ it("preserves deletion invariant for all input combinations", () => {
+ fc.assert(
+ fc.property(
+ fc.constantFrom<"open" | "closed" | "unknown">("open", "closed", "unknown"),
+ fc.boolean(),
+ fc.boolean(),
+ (sourceState, hasActiveAgent, hasLiveInteractiveSession) => {
+ const result = decideProjectClosedSourceAction({
+ sourceState,
+ hasActiveAgent,
+ hasLiveInteractiveSession
+ })
+
+ return result._tag === "Delete"
+ ? sourceState === "closed" && !hasActiveAgent && !hasLiveInteractiveSession
+ : true
+ }
+ )
+ )
+ })
})As per coding guidelines **/*.test.{ts,tsx} requires fast-check property tests for invariants, and **/*.{test,spec}.{ts,tsx} requires invariant verification via fast-check.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/api/tests/project-closed-source-policy.test.ts` around lines 1 - 57,
Add a fast-check property test that exhaustively verifies the
decideProjectClosedSourceAction invariant by generating arbitrary
ProjectClosedSourcePolicyInput values (use fast-check) and asserting the policy
rules: when sourceState === "closed" && !hasActiveAgent &&
!hasLiveInteractiveSession result._tag === "Delete"; when sourceState === "open"
|| sourceState === "unknown" result._tag === "Keep" with reason
"source-open-or-unknown"; when hasActiveAgent result._tag === "Keep" with reason
"active-agent" (and this should take precedence over hasLiveInteractiveSession
which yields "live-interactive-session"); import and use
decideProjectClosedSourceAction and the ProjectClosedSourcePolicyInput type in
the test file and keep the existing example tests.
Source: Coding guidelines
| * For GitLab projects `owner` carries the full namespace path (e.g. `group/subgroup`). | ||
| */ |
There was a problem hiding this comment.
Синхронизируйте контракт поля owner для GitLab.
На Line 20-21 задокументирован owner как namespace (пример group/subgroup), но на Line 105 возвращается gitlab.projectPath (namespace/repo). Это создаёт неоднозначный публичный контракт ProjectSourceRef для GitLab.
Предлагаемое исправление
- owner: gitlab.projectPath,
+ owner: gitlab.namespace,Also applies to: 104-106
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/lib/src/core/project-source-ref.ts` around lines 20 - 21, The
ProjectSourceRef contract for GitLab is inconsistent: the docstring states owner
is the namespace (e.g. "group/subgroup") but the code sets owner to
gitlab.projectPath (namespace/repo). Update ProjectSourceRef so owner
consistently carries only the namespace and the repository name goes into a
separate field (e.g., repo or name), or adjust the docstring to match the
current behavior. Locate the ProjectSourceRef type/constructor and the place
assigning gitlab.projectPath (reference gitlab.projectPath and the owner
property) and change the assignment and/or documentation so owner = namespace
only and repository = repoName, ensuring any code consumers are updated to use
the new repo field.
| // INVARIANT: never throws on API/HTTP failure — unknown is returned so deletion is skipped | ||
| // COMPLEXITY: O(1) API call |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Проверка локальной реализации usecase:"
rg -n --type=ts 'fetchGithubSourceState|fetchProjectSourceState|PlatformError|catchAll|normalizeProjectSourceState' \
packages/lib/src/usecases/project-source-state.ts
echo
echo "Проверка места, где делается collapse в unknown:"
rg -n --type=ts 'fetchProjectSourceState|catchAll\\(\\) => Effect\\.succeed\\("unknown"' \
packages/api/src/services/project-auto-delete.tsRepository: ProverCoderAI/docker-git
Length of output: 914
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== project-source-state.ts (lines 1-120) ==="
nl -ba packages/lib/src/usecases/project-source-state.ts | sed -n '1,120p'
echo
echo "=== project-auto-delete.ts: occurrences of unknown and catchAll ==="
rg -n --type=ts 'unknown|catchAll' packages/api/src/services/project-auto-delete.ts
echo
echo "=== project-auto-delete.ts (show around first unknown occurrence) ==="
UNKNOWN_LINE=$(rg -n --type=ts 'unknown' packages/api/src/services/project-auto-delete.ts | head -n 1 | cut -d: -f1)
if [ -n "$UNKNOWN_LINE" ]; then
nl -ba packages/api/src/services/project-auto-delete.ts | sed -n "$((UNKNOWN_LINE-20)),$((UNKNOWN_LINE+40))p"
else
echo "No 'unknown' occurrence found."
fi
echo
echo "=== project-auto-delete.ts: show catchAll blocks context ==="
# Print a small window around each catchAll
rg -n --type=ts 'catchAll\(' packages/api/src/services/project-auto-delete.ts | cut -d: -f1 | sort -u | while read -r ln; do
echo "--- catchAll near line $ln ---"
nl -ba packages/api/src/services/project-auto-delete.ts | sed -n "$((ln-20)),$((ln+40))p" | sed 's/^/ /'
doneRepository: ProverCoderAI/docker-git
Length of output: 175
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== project-source-state.ts (lines 1-120) ==="
nl -ba packages/lib/src/usecases/project-source-state.ts | sed -n '1,120p'
echo
echo "=== project-auto-delete.ts: occurrences of unknown and catchAll ==="
rg -n --type=ts 'unknown|catchAll' packages/api/src/services/project-auto-delete.ts
echo
echo "=== project-auto-delete.ts (show around first unknown occurrence) ==="
UNKNOWN_LINE=$(rg -n --type=ts 'unknown' packages/api/src/services/project-auto-delete.ts | head -n 1 | cut -d: -f1)
if [ -n "${UNKNOWN_LINE:-}" ]; then
nl -ba packages/api/src/services/project-auto-delete.ts | sed -n "$((UNKNOWN_LINE-20)),$((UNKNOWN_LINE+40))p"
else
echo "No 'unknown' occurrence found."
fi
echo
echo "=== project-auto-delete.ts: show catchAll blocks context ==="
rg -n --type=ts 'catchAll\(' packages/api/src/services/project-auto-delete.ts | cut -d: -f1 | sort -u | while read -r ln; do
echo "--- catchAll near line $ln ---"
nl -ba packages/api/src/services/project-auto-delete.ts | sed -n "$((ln-20)),$((ln+40))p"
doneRepository: ProverCoderAI/docker-git
Length of output: 175
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== project-source-state.ts: show around line ~15-80 ==="
sed -n '1,120p' packages/lib/src/usecases/project-source-state.ts | cat -n
echo
echo "=== project-auto-delete.ts: find unknown/catchAll ==="
rg -n --type=ts 'unknown|catchAll' packages/api/src/services/project-auto-delete.ts
echo
echo "=== project-auto-delete.ts: show around first unknown occurrence ==="
UNKNOWN_LINE=$(rg -n --type=ts 'unknown' packages/api/src/services/project-auto-delete.ts | head -n 1 | cut -d: -f1 || true)
if [ -n "${UNKNOWN_LINE:-}" ]; then
START=$((UNKNOWN_LINE-30)); END=$((UNKNOWN_LINE+60))
sed -n "${START},${END}p" packages/api/src/services/project-auto-delete.ts | cat -n
else
echo "No 'unknown' occurrence found."
fi
echo
echo "=== project-auto-delete.ts: show around each catchAll occurrence ==="
while read -r ln; do
START=$((ln-30)); END=$((ln+60))
echo "--- catchAll window near line $ln ---"
sed -n "${START},${END}p" packages/api/src/services/project-auto-delete.ts | cat -n
done < <(rg -n --type=ts 'catchAll\(' packages/api/src/services/project-auto-delete.ts | cut -d: -f1 | sort -u)Repository: ProverCoderAI/docker-git
Length of output: 7061
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== project-auto-delete.ts: show around catchAll line 70-120 ==="
sed -n '60,120p' packages/api/src/services/project-auto-delete.ts | cat -n
echo
echo "=== project-auto-delete.ts: show around catchAll line 130-190 ==="
sed -n '130,190p' packages/api/src/services/project-auto-delete.ts | cat -n
echo
echo "=== Find runGhApiNullable implementation ==="
rg -n --type=ts 'function runGhApiNullable|const runGhApiNullable|runGhApiNullable\s*=' packages/lib/src/usecases/github-api-helpers.ts packages/lib/src/usecases/github-api-helpers.* 2>/dev/null || true
# Try likely path
if [ -f packages/lib/src/usecases/github-api-helpers.ts ]; then
sed -n '1,220p' packages/lib/src/usecases/github-api-helpers.ts | cat -n
fi
echo
echo "=== Find normalizeProjectSourceState implementation ==="
rg -n --type=ts 'function normalizeProjectSourceState|const normalizeProjectSourceState|normalizeProjectSourceState\s*=' packages/lib/src/core/project-source-ref.ts packages/lib/src/core/project-source-ref.* 2>/dev/null || true
# Try likely path
if [ -f packages/lib/src/core/project-source-ref.ts ]; then
sed -n '1,260p' packages/lib/src/core/project-source-ref.ts | cat -n
fiRepository: ProverCoderAI/docker-git
Length of output: 12917
Уточнить инвариант в fetchGithubSourceState, чтобы он соответствовал контракту PlatformError
Инвариант never throws … unknown is returned в packages/lib/src/usecases/project-source-state.ts (стр. 19-20) шире реального поведения: fetchGithubSourceState всё ещё может завершиться ошибкой в error-channel (PlatformError), т.к. внутри нет Effect.catchAll (тип сигнатуры на это указывает). Коллапс к "unknown" происходит в packages/api/src/services/project-auto-delete.ts через Effect.catchAll(() => Effect.succeed("unknown" as const)), поэтому инвариант стоит переформулировать с учётом того, что "unknown" — это поведение оркестратора, а не самой usecase-функции.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/lib/src/usecases/project-source-state.ts` around lines 19 - 20,
Инвариант в строках 19‑20 должен быть уточнён: замените формулировку "never
throws on API/HTTP failure — unknown is returned so deletion is skipped" на
указание, что fetchGithubSourceState может завершаться ошибкой в error‑channel
(PlatformError) и не выполняет локального преобразования ошибок в "unknown";
свёртка в "unknown" происходит в оркестраторе (например в проекте AutoDelete
через Effect.catchAll(() => Effect.succeed("unknown"))). Ссылаться в комментарии
явно на fetchGithubSourceState и на поведение обработчика в project-auto-delete
(Effect.catchAll), чтобы контракт был корректно понят при чтении кода.
| describe("parseProjectSourceRef", () => { | ||
| it("recovers a GitHub issue identity from issue-<n>", () => { | ||
| expect(parseProjectSourceRef("https://github.com/owner/repo.git", "issue-42")).toEqual({ | ||
| provider: "github", | ||
| owner: "owner", | ||
| repo: "repo", | ||
| kind: "issue", | ||
| number: "42" | ||
| }) | ||
| }) | ||
|
|
||
| it("recovers a GitHub pull-request identity from refs/pull/<n>/head", () => { | ||
| expect(parseProjectSourceRef("https://github.com/owner/repo.git", "refs/pull/7/head")).toEqual({ | ||
| provider: "github", | ||
| owner: "owner", | ||
| repo: "repo", | ||
| kind: "pull", | ||
| number: "7" | ||
| }) | ||
| }) | ||
|
|
||
| it("recovers a GitLab merge-request identity from refs/merge-requests/<n>/head", () => { | ||
| expect(parseProjectSourceRef("https://gitlab.com/group/repo.git", "refs/merge-requests/9/head")).toEqual({ | ||
| provider: "gitlab", | ||
| owner: "group/repo", | ||
| repo: "repo", | ||
| kind: "pull", | ||
| number: "9" | ||
| }) | ||
| }) | ||
|
|
||
| it("recovers a GitLab issue identity from issue-<n>", () => { | ||
| expect(parseProjectSourceRef("https://gitlab.com/group/sub/repo.git", "issue-3")).toEqual({ | ||
| provider: "gitlab", | ||
| owner: "group/sub/repo", | ||
| repo: "repo", | ||
| kind: "issue", | ||
| number: "3" | ||
| }) | ||
| }) | ||
|
|
||
| it("trims surrounding whitespace from the ref", () => { | ||
| expect(parseProjectSourceRef("https://github.com/owner/repo.git", " issue-1 ")?.number).toBe("1") | ||
| }) | ||
|
|
||
| it("returns null for a plain branch ref", () => { | ||
| expect(parseProjectSourceRef("https://github.com/owner/repo.git", "main")).toBeNull() | ||
| }) | ||
|
|
||
| it("returns null for an empty ref", () => { | ||
| expect(parseProjectSourceRef("https://github.com/owner/repo.git", " ")).toBeNull() | ||
| }) | ||
|
|
||
| it("returns null when the repo URL is not a known provider", () => { | ||
| expect(parseProjectSourceRef("https://example.com/owner/repo.git", "issue-1")).toBeNull() | ||
| }) | ||
|
|
||
| it("does not treat a GitHub merge-request style ref as a pull request", () => { | ||
| expect(parseProjectSourceRef("https://github.com/owner/repo.git", "refs/merge-requests/9/head")).toBeNull() | ||
| }) | ||
| }) | ||
|
|
||
| describe("normalizeProjectSourceState", () => { | ||
| it("maps GitHub open to open", () => { | ||
| expect(normalizeProjectSourceState("open")).toBe("open") | ||
| }) | ||
|
|
||
| it("maps GitLab opened to open", () => { | ||
| expect(normalizeProjectSourceState("opened")).toBe("open") | ||
| }) | ||
|
|
||
| it("maps closed to closed", () => { | ||
| expect(normalizeProjectSourceState("closed")).toBe("closed") | ||
| }) | ||
|
|
||
| it("maps GitLab merged to closed", () => { | ||
| expect(normalizeProjectSourceState("merged")).toBe("closed") | ||
| }) | ||
|
|
||
| it("maps GitLab locked to closed", () => { | ||
| expect(normalizeProjectSourceState("locked")).toBe("closed") | ||
| }) | ||
|
|
||
| it("is case- and whitespace-insensitive", () => { | ||
| expect(normalizeProjectSourceState(" CLOSED ")).toBe("closed") | ||
| }) | ||
|
|
||
| it("maps unrecognized values to unknown", () => { | ||
| expect(normalizeProjectSourceState("draft")).toBe("unknown") | ||
| }) | ||
|
|
||
| it("maps null and undefined to unknown", () => { | ||
| expect(normalizeProjectSourceState(null)).toBe("unknown") | ||
| expect(normalizeProjectSourceState(undefined)).toBe("unknown") | ||
| }) | ||
| }) | ||
|
|
||
| describe("shouldDeleteForSourceState", () => { | ||
| it("deletes only when the source is closed", () => { | ||
| expect(shouldDeleteForSourceState("closed")).toBe(true) | ||
| }) | ||
|
|
||
| it("never deletes for open or unknown", () => { | ||
| expect(shouldDeleteForSourceState("open")).toBe(false) | ||
| expect(shouldDeleteForSourceState("unknown")).toBe(false) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "1) Проверка использования fast-check в тестах:"
rg -n --type=ts 'from "fast-check"|fc\.property|fc\.assert'
echo
echo "2) Проверка данного файла на property-based проверки:"
rg -n --type=ts 'fc\.property|fc\.assert' packages/lib/tests/core/project-source-ref.test.ts
echo
echo "3) Проверка, что зависимость fast-check объявлена:"
fd -i 'package.json' -x rg -n '"fast-check"'Repository: ProverCoderAI/docker-git
Length of output: 17894
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="packages/lib/tests/core/project-source-ref.test.ts"
echo "1) fast-check usage in target file:"
rg -n --type=ts 'from "fast-check"|fc\.property|fc\.assert' "$FILE" || true
echo
echo "2) Show file header imports (first 40 lines):"
head -n 50 "$FILE"
echo
echo "3) fast-check declared in any package.json:"
fd -i 'package.json' . -x sh -c 'echo "---- $1"; rg -n "\"fast-check\"" "$1" || true' sh {} \
| head -n 200Repository: ProverCoderAI/docker-git
Length of output: 2010
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="packages/lib/tests/core/project-source-ref.test.ts"
echo "1) fast-check usage in target file:"
rg -n --type=ts 'from "fast-check"|fc\.property|fc\.assert' "$FILE" || true
echo
echo "2) Show file header imports (first 40 lines):"
head -n 50 "$FILE"
echo
echo "3) fast-check declared in any package.json:"
fd -i 'package.json' . -x sh -c 'echo "---- $1"; rg -n "\"fast-check\"" "$1" || true' sh {} \
| head -n 200Repository: ProverCoderAI/docker-git
Length of output: 2010
Добавить fast-check property-based инварианты для project-source-ref
В packages/lib/tests/core/project-source-ref.test.ts используются только примерные vitest-тесты (нет fast-check/fc.property), при этом fast-check уже подключён в packages/lib. Добавить property-based проверки инвариантов для:
parseProjectSourceRef: пробелы вокругrefне меняютnumber; валидные паттерны GitHub/GitLab дают соответствующий{ provider, owner, repo, kind, number }; ветки/неизвестные паттерны (в т.ч. GitHub merge-request style для GitHub) даютnull.normalizeProjectSourceState: идемпотентность; нечувствительность к регистру/whitespace; маппинг{open, opened}→open,{closed, merged, locked}→closed,{null, undefined, прочее}→unknown.shouldDeleteForSourceState:trueтогда и только тогда, когдаnormalizeProjectSourceState(state) === "closed".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/lib/tests/core/project-source-ref.test.ts` around lines 9 - 115, Add
fast-check property-based tests for parseProjectSourceRef,
normalizeProjectSourceState, and shouldDeleteForSourceState: write fc.property
cases that (1) assert trimming whitespace around ref does not change the parsed
.number for parseProjectSourceRef and that known GitHub/GitLab ref patterns
produce the expected object shape ({provider, owner, repo, kind, number}) while
branch-like or unknown patterns (including a GitHub repo with
refs/merge-requests/... ) return null; (2) assert normalizeProjectSourceState is
idempotent, case- and whitespace-insensitive, maps "open"/"opened"→"open",
"closed"/"merged"/"locked"→"closed", and null/undefined/other→"unknown"; and (3)
assert shouldDeleteForSourceState(state) === (normalizeProjectSourceState(state)
=== "closed") for arbitrary string/null/undefined inputs; use
parseProjectSourceRef, normalizeProjectSourceState, and
shouldDeleteForSourceState identifiers and fc.property with appropriate
generators (fc.string, fc.option, fc.oneof, and custom generators for valid ref
patterns) to implement these invariants.
Source: Coding guidelines
What & why
Closes #117 — «Сделать возможность автоматического удаления контейнера issues или PR которого уже закрылся» (automatically delete a container whose issue or PR has already been closed).
A closed issue/PR leaves its docker-git container running with no purpose, and these pile up over time. This adds an opt-in background loop that periodically checks each project's originating GitHub issue/PR and removes the container once that source is closed — modeled directly on the existing auto-suspend loop.
How it works
packages/lib/src/core/project-source-ref.ts, pure): inverts the clone-time normalization to map a project'srepoUrl+repoRefback to{provider, owner, repo, kind: issue|pull, number}(issue-<n>,refs/pull/<n>/head,refs/merge-requests/<n>/head). Returnsnullfor plain-branch projects so they are never touched.packages/lib/src/usecases/project-source-state.ts, shell): callsgh api /repos/{owner}/{repo}/issues/{n} --jq .stateinside the gh-auth container and normalizes toopen | closed | unknown(GitLab →unknownuntil first-class support exists).packages/api/src/services/project-closed-source-policy.ts, pure): delete only when the source is definitivelyclosedand no agent is active and no live interactive session (ssh/terminal/browser/skiller) is attached.open/unknownalways keep the project, so a transient API failure never destroys live work.packages/api/src/services/project-auto-delete.ts, shell): disabled by default; forked fromprogram.tsnext to the auto-suspend loop.Shared activity predicates (
projectHasActiveAgent,projectHasLiveInteractiveSession) were extracted intoproject-activity.tsand reused by both the auto-suspend and auto-delete loops to avoid duplicated logic.Configuration (opt-in)
DOCKER_GIT_AUTO_DELETE_CLOSEDfalse1/true/on/yes). Deletion is irreversible, so it is off unless explicitly enabled.DOCKER_GIT_AUTO_DELETE_SCAN_INTERVAL_SECONDS300Safety invariants
openorunknownstate.unknown→ keep.Tests
packages/lib/tests/core/project-source-ref.test.ts— 19 cases covering issue/PR/MR parsing for GitHub & GitLab, branch/empty/non-provider rejection, and state normalization.packages/api/tests/project-closed-source-policy.test.ts— 7 cases covering every Keep/Delete branch and reason precedence.Full suites green: lib 300/300, api 192/192. Lib (
vibecode-linter) and api (eslint) lint clean; lib/api typecheck clean. Changeset added (minorfor@prover-coder-ai/docker-git).