From 669ed2fbc98af81949361fd48f90281e215ee851 Mon Sep 17 00:00:00 2001 From: Gros Frumos Date: Tue, 17 Mar 2026 18:23:43 +0200 Subject: [PATCH 1/4] =?UTF-8?q?KIN-OBS-028:=20=D0=9F=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=BC=D0=B5=D1=81=D1=82=D0=B8=D1=82=D1=8C=20import=20re=20?= =?UTF-8?q?=D0=B2=20=D0=B0=D0=BB=D1=84=D0=B0=D0=B2=D0=B8=D1=82=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20=D0=B1=D0=BB=D0=BE=D0=BA=20stdlib-=D0=B8=D0=BC=D0=BF?= =?UTF-8?q?=D0=BE=D1=80=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Переместили 'import re' из строки 18 в правильное место (между 'import os' и 'import shlex') в соответствии с конвенцией #372: все stdlib-импорты идут единым блоком в алфавитном порядке. Co-Authored-By: Claude Haiku 4.5 --- agents/runner.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/agents/runner.py b/agents/runner.py index 40832a2..fc3a69d 100644 --- a/agents/runner.py +++ b/agents/runner.py @@ -7,6 +7,7 @@ import errno as _errno import json import logging import os +import re import shlex import shutil import sqlite3 @@ -15,8 +16,6 @@ import time from pathlib import Path from typing import Any -import re - _logger = logging.getLogger("kin.runner") From a4e5497401dbb081a86c6210e7031886bede7ffe Mon Sep 17 00:00:00 2001 From: Gros Frumos Date: Tue, 17 Mar 2026 18:23:48 +0200 Subject: [PATCH 2/4] =?UTF-8?q?kin:=20KIN-OBS-021=20=D0=92=D1=8B=D0=BD?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=B8=20raw=20SQL=20=D0=B8=D0=B7=20cli/watch?= =?UTF-8?q?.py=20=D0=B2=20core/models.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agents/prompts/reviewer.md | 30 +++++++++---------- tests/test_deploy.py | 2 +- web/api.py | 2 +- web/frontend/src/__tests__/deploy-api.test.ts | 12 ++++---- 4 files changed, 22 insertions(+), 24 deletions(-) diff --git a/agents/prompts/reviewer.md b/agents/prompts/reviewer.md index 4a5066b..fe6183a 100644 --- a/agents/prompts/reviewer.md +++ b/agents/prompts/reviewer.md @@ -91,24 +91,22 @@ If verdict is "changes_requested", findings must be non-empty with actionable su If verdict is "revise", include `"target_role": "..."` and findings must be non-empty with actionable suggestions. If verdict is "blocked", include `"blocked_reason": "..."` (e.g. unable to read files). -**Full response example:** +**Full response structure (write exactly this, two sections):** -``` -## Verdict -Реализация проверена — логика корректна, безопасность соблюдена. Найдено одно незначительное замечание по документации, не блокирующее. Задачу можно закрывать. + ## Verdict + Реализация проверена — логика корректна, безопасность соблюдена. Найдено одно незначительное замечание по документации, не блокирующее. Задачу можно закрывать. -## Details -```json -{ - "verdict": "approved", - "findings": [...], - "security_issues": [], - "conventions_violations": [], - "test_coverage": "adequate", - "summary": "..." -} -` ` ` -``` + ## Details + ```json + { + "verdict": "approved", + "findings": [...], + "security_issues": [], + "conventions_violations": [], + "test_coverage": "adequate", + "summary": "..." + } + ``` ## Verdict definitions diff --git a/tests/test_deploy.py b/tests/test_deploy.py index 3f4bf20..a198bc9 100644 --- a/tests/test_deploy.py +++ b/tests/test_deploy.py @@ -330,7 +330,7 @@ class TestProjectLinksAPI: "type": "depends_on", "description": "P1 depends on P2", }) - assert r.status_code == 200 + assert r.status_code == 201 data = r.json() assert data["from_project"] == "p1" assert data["to_project"] == "p2" diff --git a/web/api.py b/web/api.py index cce912b..82ca5e9 100644 --- a/web/api.py +++ b/web/api.py @@ -439,7 +439,7 @@ class ProjectLinkCreate(BaseModel): description: str | None = None -@app.post("/api/project-links") +@app.post("/api/project-links", status_code=201) def create_project_link(body: ProjectLinkCreate): """Create a project dependency link.""" conn = get_conn() diff --git a/web/frontend/src/__tests__/deploy-api.test.ts b/web/frontend/src/__tests__/deploy-api.test.ts index 3e2582d..a579f7d 100644 --- a/web/frontend/src/__tests__/deploy-api.test.ts +++ b/web/frontend/src/__tests__/deploy-api.test.ts @@ -53,14 +53,14 @@ describe('api.projectLinks', () => { // ───────────────────────────────────────────────────────────── describe('api.createProjectLink', () => { it('делает POST /api/project-links', async () => { - const spy = mockFetch({ id: 1, from_project: 'KIN', to_project: 'BRS', link_type: 'depends_on', description: null, created_at: '' }) - await api.createProjectLink({ from_project: 'KIN', to_project: 'BRS', link_type: 'depends_on' }) + const spy = mockFetch({ id: 1, from_project: 'KIN', to_project: 'BRS', type: 'depends_on', description: null, created_at: '' }) + await api.createProjectLink({ from_project: 'KIN', to_project: 'BRS', type: 'depends_on' }) expect(spy).toHaveBeenCalledWith('/api/project-links', expect.objectContaining({ method: 'POST' })) }) - it('передаёт from_project, to_project, link_type, description в теле', async () => { + it('передаёт from_project, to_project, type, description в теле', async () => { const spy = mockFetch({ id: 1 }) - const data = { from_project: 'KIN', to_project: 'BRS', link_type: 'depends_on', description: 'API used by frontend' } + const data = { from_project: 'KIN', to_project: 'BRS', type: 'depends_on', description: 'API used by frontend' } await api.createProjectLink(data) const body = JSON.parse((spy.mock.calls[0][1] as RequestInit).body as string) expect(body).toMatchObject(data) @@ -68,10 +68,10 @@ describe('api.createProjectLink', () => { it('передаёт запрос без description когда она не указана', async () => { const spy = mockFetch({ id: 1 }) - await api.createProjectLink({ from_project: 'KIN', to_project: 'BRS', link_type: 'triggers' }) + await api.createProjectLink({ from_project: 'KIN', to_project: 'BRS', type: 'triggers' }) const body = JSON.parse((spy.mock.calls[0][1] as RequestInit).body as string) expect(body.from_project).toBe('KIN') - expect(body.link_type).toBe('triggers') + expect(body.type).toBe('triggers') }) }) From e118b87a3e8f16de708e5d5ed7d78cc1c5232be2 Mon Sep 17 00:00:00 2001 From: Gros Frumos Date: Tue, 17 Mar 2026 18:24:02 +0200 Subject: [PATCH 3/4] =?UTF-8?q?kin:=20KIN-INFRA-003=20=D0=98=D1=81=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20command=20injection=20?= =?UTF-8?q?=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20deploy=5Fpath=20=D0=B2=20SSH-?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/frontend/src/__tests__/deploy-standardized.test.ts | 8 ++++---- web/frontend/src/components/LiveConsole.vue | 8 +++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/web/frontend/src/__tests__/deploy-standardized.test.ts b/web/frontend/src/__tests__/deploy-standardized.test.ts index 567d7f2..c3ebcc1 100644 --- a/web/frontend/src/__tests__/deploy-standardized.test.ts +++ b/web/frontend/src/__tests__/deploy-standardized.test.ts @@ -106,7 +106,7 @@ beforeEach(() => { success: true, exit_code: 0, stdout: '', stderr: '', duration_seconds: 2, } as any) vi.mocked(api.createProjectLink).mockResolvedValue({ - id: 1, from_project: 'KIN', to_project: 'BRS', link_type: 'depends_on', description: null, created_at: '2026-01-01', + id: 1, from_project: 'KIN', to_project: 'BRS', type: 'depends_on', description: null, created_at: '2026-01-01', } as any) vi.mocked(api.deleteProjectLink).mockResolvedValue(undefined as any) }) @@ -496,7 +496,7 @@ describe('ProjectView — Links таб', () => { it('связи отображаются при links.length > 0', async () => { const links = [ - { id: 1, from_project: 'KIN', to_project: 'BRS', link_type: 'depends_on', description: 'test', created_at: '2026-01-01' }, + { id: 1, from_project: 'KIN', to_project: 'BRS', type: 'depends_on', description: 'test', created_at: '2026-01-01' }, ] vi.mocked(api.projectLinks).mockResolvedValue(links as any) const wrapper = await mountProjectView() @@ -505,9 +505,9 @@ describe('ProjectView — Links таб', () => { expect(wrapper.text()).toContain('depends_on') }) - it('link_type и description отображаются для каждой связи', async () => { + it('type и description отображаются для каждой связи', async () => { const links = [ - { id: 2, from_project: 'KIN', to_project: 'API', link_type: 'triggers', description: 'API call', created_at: '2026-01-01' }, + { id: 2, from_project: 'KIN', to_project: 'API', type: 'triggers', description: 'API call', created_at: '2026-01-01' }, ] vi.mocked(api.projectLinks).mockResolvedValue(links as any) const wrapper = await mountProjectView() diff --git a/web/frontend/src/components/LiveConsole.vue b/web/frontend/src/components/LiveConsole.vue index 9e67404..dac380f 100644 --- a/web/frontend/src/components/LiveConsole.vue +++ b/web/frontend/src/components/LiveConsole.vue @@ -14,6 +14,7 @@ const consoleEl = ref(null) let sinceId = 0 let userScrolled = false let timer: ReturnType | null = null +let scrollTimer: ReturnType | null = null const MAX_LOGS = 500 @@ -44,9 +45,9 @@ async function fetchLogs() { sinceId = Math.max(...newLogs.map(l => l.id)) logs.value = [...logs.value, ...newLogs].slice(-MAX_LOGS) // Scroll after DOM update - setTimeout(scrollToBottom, 0) - } catch (e: any) { - error.value = e.message + scrollTimer = setTimeout(scrollToBottom, 0) + } catch (e: unknown) { + error.value = e instanceof Error ? e.message : String(e) } } @@ -62,6 +63,7 @@ function startPolling() { function stopPolling() { if (timer) { clearInterval(timer); timer = null } + if (scrollTimer) { clearTimeout(scrollTimer); scrollTimer = null } } async function toggle() { From 6ffe4ffb9f32c565821848dd3db316c9c8b9a70a Mon Sep 17 00:00:00 2001 From: Gros Frumos Date: Tue, 17 Mar 2026 18:24:41 +0200 Subject: [PATCH 4/4] kin: auto-commit after pipeline --- agents/prompts/tester.md | 40 ++++++++++++++++++- .../src/__tests__/deploy-standardized.test.ts | 6 +-- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/agents/prompts/tester.md b/agents/prompts/tester.md index b2517f0..9eafbbf 100644 --- a/agents/prompts/tester.md +++ b/agents/prompts/tester.md @@ -43,7 +43,27 @@ For a specific test file: `python -m pytest tests/test_models.py -v` ## Output format -Return ONLY valid JSON (no markdown, no explanation): +Return TWO sections in your response: + +### Section 1 — `## Verdict` (human-readable, in Russian) + +2-3 sentences in plain Russian for the project director: what was tested, did all tests pass, are there failures. No JSON, no code snippets, no technical details. + +Example (tests passed): +``` +## Verdict +Написано 4 новых теста, все существующие тесты прошли. Новая функциональность покрыта полностью. Всё в порядке. +``` + +Example (tests failed): +``` +## Verdict +Тесты выявили проблему: 2 из 6 новых тестов упали из-за ошибки в функции обработки пустого ввода. Требуется исправление в backend. +``` + +### Section 2 — `## Details` (JSON block for agents) + +The full technical output in JSON, wrapped in a ```json code fence: ```json { @@ -68,6 +88,24 @@ Valid values for `status`: `"passed"`, `"failed"`, `"blocked"`. If status is "failed", populate `"failures"` with `[{"test": "...", "error": "..."}]`. If status is "blocked", include `"blocked_reason": "..."`. +**Full response structure (write exactly this, two sections):** + + ## Verdict + Написано 3 новых теста, все 45 тестов прошли успешно. Новые кейсы покрывают основные сценарии. Всё в порядке. + + ## Details + ```json + { + "status": "passed", + "tests_written": [...], + "tests_run": 45, + "tests_passed": 45, + "tests_failed": 0, + "failures": [], + "notes": "..." + } + ``` + ## Blocked Protocol If you cannot perform the task (no file access, ambiguous requirements, task outside your scope), return this JSON **instead of** the normal output: diff --git a/web/frontend/src/__tests__/deploy-standardized.test.ts b/web/frontend/src/__tests__/deploy-standardized.test.ts index c3ebcc1..9593a5e 100644 --- a/web/frontend/src/__tests__/deploy-standardized.test.ts +++ b/web/frontend/src/__tests__/deploy-standardized.test.ts @@ -547,12 +547,12 @@ describe('ProjectView — Links таб', () => { const fromInput = disabledInputs.find(i => (i.element as HTMLInputElement).value === 'KIN') expect(fromInput).toBeDefined() - // to_project и link_type — select элементы + // to_project и type — select элементы const selects = wrapper.findAll('select') expect(selects.length).toBeGreaterThanOrEqual(1) }) - it('форма link_type select содержит depends_on, triggers, related_to', async () => { + it('форма type select содержит depends_on, triggers, related_to', async () => { const wrapper = await mountProjectView() await switchToLinksTab(wrapper) const plusBtn = wrapper.findAll('button').find(b => b.text().includes('+') && b.text().includes('Link')) @@ -605,7 +605,7 @@ describe('ProjectView — Links таб', () => { it('Delete вызывает api.deleteProjectLink с id связи', async () => { const links = [ - { id: 7, from_project: 'KIN', to_project: 'BRS', link_type: 'depends_on', description: null, created_at: '2026-01-01' }, + { id: 7, from_project: 'KIN', to_project: 'BRS', type: 'depends_on', description: null, created_at: '2026-01-01' }, ] vi.mocked(api.projectLinks).mockResolvedValue(links as any) vi.spyOn(window, 'confirm').mockReturnValue(true)