From a6f2276bf2a94c2c11f2d657886dbb965694c525 Mon Sep 17 00:00:00 2001 From: Evgeny Kiriyak <224408464+evkir@users.noreply.github.com> Date: Thu, 21 May 2026 10:53:20 +0300 Subject: [PATCH 1/4] refactor(core): move Severity/Finding into scan_session, add target/evidence fields Single source of truth for session-related models is now scan_session.py: - Severity enum moved here from session.py - Finding dataclass moved here, gained fields: * target: which host/URL/contract the finding pertains to * evidence: list of artifacts (nmap xml, HTTP responses, etc.) * cve_ids: list form, kept in sync with legacy single 'cve' field - ScanSession gained findings list + add_finding() method - summary() now reports severity_breakdown like PentestSession.summary() did Fixes KI-5: agents (ReconAgent in particular) were already constructing Finding(target=..., evidence=[...]) which raised TypeError because those fields did not exist on the dataclass. They exist now. session.py (legacy) will become a re-export shim in the next commit, keeping all current import sites working without code changes. Refs: STANDOFF.md day 3/30, closes KI-5 (partial: KI-2 in commit 2) --- cyberai/core/scan_session.py | 172 ++++++++++++++++++++++++++++------- 1 file changed, 137 insertions(+), 35 deletions(-) diff --git a/cyberai/core/scan_session.py b/cyberai/core/scan_session.py index 085a44b..46a5686 100644 --- a/cyberai/core/scan_session.py +++ b/cyberai/core/scan_session.py @@ -1,25 +1,37 @@ """ -ScanSession — full lifecycle manager for a pentest scan. -Ties together: ReconAgent → IntelAgent → ExploitAgent → ReportAgent +ScanSession — single source of truth for a pentest scan lifecycle. + +This module holds: + * ScanSession — the lifecycle object (created → running → completed/failed) + * ScanState, ScanPhase — state enums + * PhaseResult — per-phase outcome record + * Severity, Finding — vulnerability finding model (moved here from session.py) + +The legacy `cyberai.core.session` module re-exports everything from here +for backward compatibility; new code should import from `scan_session`. """ from __future__ import annotations + +import uuid from dataclasses import dataclass, field from datetime import datetime, timezone from enum import Enum from typing import Any, Dict, List, Optional -import uuid + + +# ── enums ───────────────────────────────────────────────────────────── class ScanState(str, Enum): - CREATED = "created" - RUNNING = "running" - RECON = "recon" - INTEL = "intel" - EXPLOIT = "exploit" - REPORT = "report" - COMPLETED = "completed" - FAILED = "failed" - CANCELLED = "cancelled" + CREATED = "created" + RUNNING = "running" + RECON = "recon" + INTEL = "intel" + EXPLOIT = "exploit" + REPORT = "report" + COMPLETED = "completed" + FAILED = "failed" + CANCELLED = "cancelled" class ScanPhase(str, Enum): @@ -29,6 +41,51 @@ class ScanPhase(str, Enum): REPORT = "report" +class Severity(str, Enum): + CRITICAL = "CRITICAL" + HIGH = "HIGH" + MEDIUM = "MEDIUM" + LOW = "LOW" + INFO = "INFO" + + +# ── models ──────────────────────────────────────────────────────────── + + +@dataclass +class Finding: + """ + A single vulnerability finding. + + Fields target/evidence/cve_ids added in day 3 (KI-5 fix) — agents + were already passing these but the dataclass didn't accept them. + """ + id: int + severity: Severity + title: str + description: str + timestamp: str + agent: str + + # Legacy single-CVE field (kept for backward compat with old callers) + cve: Optional[str] = None + # New: list of CVEs (some findings reference multiple) + cve_ids: List[str] = field(default_factory=list) + # New: target this finding was made against (host, URL, contract addr) + target: Optional[str] = None + # New: artifacts proving the finding (nmap output, request/response, etc.) + evidence: List[Any] = field(default_factory=list) + # Free-form structured data + data: Any = None + + def __post_init__(self) -> None: + # Keep `cve` and `cve_ids` in sync for callers that use either + if self.cve and not self.cve_ids: + self.cve_ids = [self.cve] + elif self.cve_ids and not self.cve: + self.cve = self.cve_ids[0] + + @dataclass class PhaseResult: phase: ScanPhase @@ -40,18 +97,24 @@ class PhaseResult: error: Optional[str] = None +# ── session ─────────────────────────────────────────────────────────── + + @dataclass class ScanSession: - target: str - session_id: str = field(default_factory=lambda: str(uuid.uuid4())[:8]) - state: ScanState = ScanState.CREATED - created_at: str = field(default_factory=lambda: _now()) - started_at: Optional[str] = None - ended_at: Optional[str] = None - phases: List[PhaseResult] = field(default_factory=list) - kb: Dict[str, Any] = field(default_factory=dict) - errors: List[str] = field(default_factory=list) - authorized_scope: List[str] = field(default_factory=list) + target: str + session_id: str = field(default_factory=lambda: str(uuid.uuid4())[:8]) + state: ScanState = ScanState.CREATED + created_at: str = field(default_factory=lambda: _now()) + started_at: Optional[str] = None + ended_at: Optional[str] = None + phases: List[PhaseResult] = field(default_factory=list) + kb: Dict[str, Any] = field(default_factory=dict) + errors: List[str] = field(default_factory=list) + authorized_scope: List[str] = field(default_factory=list) + + # Findings live on the session — added in day 3 to unify with PentestSession + findings: List[Finding] = field(default_factory=list) # ── lifecycle ───────────────────────────────────────────────────── @@ -75,6 +138,36 @@ def cancel(self) -> None: def set_phase(self, phase: ScanPhase) -> None: self.state = ScanState(phase.value) + # ── findings ────────────────────────────────────────────────────── + + def add_finding( + self, + severity: Severity, + title: str, + description: str, + agent: str, + target: Optional[str] = None, + cve: Optional[str] = None, + cve_ids: Optional[List[str]] = None, + evidence: Optional[List[Any]] = None, + data: Any = None, + ) -> Finding: + f = Finding( + id = len(self.findings) + 1, + severity = severity, + title = title, + description = description, + timestamp = _now(), + agent = agent, + target = target or self.target, + cve = cve, + cve_ids = cve_ids or [], + evidence = evidence or [], + data = data, + ) + self.findings.append(f) + return f + # ── phase tracking ──────────────────────────────────────────────── def record_phase( @@ -82,8 +175,8 @@ def record_phase( phase: ScanPhase, success: bool, started: str, - data: Dict[str, Any] = None, - error: str = None, + data: Optional[Dict[str, Any]] = None, + error: Optional[str] = None, ) -> PhaseResult: ended = _now() duration = _delta(started, ended) @@ -113,28 +206,37 @@ def summary(self) -> Dict[str, Any]: duration = None if self.started_at and self.ended_at: duration = round(_delta(self.started_at, self.ended_at), 1) + + severity_counts = {s.value: 0 for s in Severity} + for f in self.findings: + severity_counts[f.severity.value] += 1 + return { - "session_id": self.session_id, - "target": self.target, - "state": self.state.value, - "created_at": self.created_at, - "started_at": self.started_at, - "ended_at": self.ended_at, - "duration_s": duration, - "phases": [_phase_summary(p) for p in self.phases], - "errors": self.errors, - "kb_keys": list(self.kb.keys()), + "session_id": self.session_id, + "target": self.target, + "state": self.state.value, + "created_at": self.created_at, + "started_at": self.started_at, + "ended_at": self.ended_at, + "duration_s": duration, + "phases": [_phase_summary(p) for p in self.phases], + "findings_total": len(self.findings), + "severity_breakdown": severity_counts, + "errors": self.errors, + "kb_keys": list(self.kb.keys()), } def __repr__(self) -> str: return ( f"ScanSession(id={self.session_id}, " - f"target={self.target}, state={self.state.value})" + f"target={self.target}, state={self.state.value}, " + f"findings={len(self.findings)})" ) # ── helpers ─────────────────────────────────────────────────────────── + def _now() -> str: return datetime.now(timezone.utc).isoformat() From 3d421a1f0211375d712efb46c536cee50037ebdb Mon Sep 17 00:00:00 2001 From: Evgeny Kiriyak <224408464+evkir@users.noreply.github.com> Date: Thu, 21 May 2026 10:58:34 +0300 Subject: [PATCH 2/4] refactor(core): turn session.py into backward-compat shim (closes KI-2) --- cyberai/core/session.py | 189 +++++++++++++++++++++++++--------------- 1 file changed, 119 insertions(+), 70 deletions(-) diff --git a/cyberai/core/session.py b/cyberai/core/session.py index 6dacfd6..89ad3fa 100644 --- a/cyberai/core/session.py +++ b/cyberai/core/session.py @@ -1,74 +1,123 @@ -import uuid -from datetime import datetime -from dataclasses import dataclass, field -from typing import List, Dict, Any, Optional +""" +DEPRECATED: backward-compatibility shim. + +All session-related models now live in cyberai.core.scan_session. +This module re-exports them so existing imports keep working: + + from cyberai.core.session import PentestSession, Severity, Finding + +…still works, but new code should use: + + from cyberai.core.scan_session import ScanSession, Severity, Finding + +PentestSession is now an alias for ScanSession. +SessionState is a compatibility enum that mirrors ScanState plus an +IDLE alias (legacy name for the initial state). + +This shim will be removed once all import sites are migrated (planned +for day 7 of the STANDOFF rewrite). +""" +from __future__ import annotations + +import warnings from enum import Enum -class Severity(str, Enum): - CRITICAL = "CRITICAL" - HIGH = "HIGH" - MEDIUM = "MEDIUM" - LOW = "LOW" - INFO = "INFO" +# Re-export everything from the new home +from cyberai.core.scan_session import ( + Finding, + PhaseResult, + ScanPhase, + ScanSession, + ScanState, + Severity, +) + + +# ── compatibility enum ──────────────────────────────────────────────── + class SessionState(str, Enum): - IDLE = "idle" - RECON = "recon" - INTEL = "intel" - EXPLOIT = "exploit" - REPORTING = "reporting" - COMPLETE = "complete" - -@dataclass -class Finding: - id: int - severity: Severity - title: str - description: str - timestamp: str - agent: str - cve: Optional[str] = None - data: Any = None - -@dataclass -class PentestSession: - session_id: str = field(default_factory=lambda: str(uuid.uuid4())[:8]) - target: str = "" - started_at: datetime = field(default_factory=datetime.utcnow) - state: SessionState = SessionState.IDLE - findings: List[Finding] = field(default_factory=list) - recon_data: Dict[str, Any] = field(default_factory=dict) - intel_data: Dict[str, Any] = field(default_factory=dict) - exploit_data: Dict[str, Any] = field(default_factory=dict) - - def add_finding(self, severity: Severity, title: str, - description: str, agent: str, - cve: str = None, data: Any = None) -> Finding: - f = Finding( - id=len(self.findings) + 1, - severity=severity, - title=title, - description=description, - timestamp=datetime.utcnow().isoformat(), - agent=agent, - cve=cve, - data=data - ) - self.findings.append(f) - return f - - def set_state(self, state: SessionState): - self.state = state - - def summary(self) -> Dict[str, Any]: - severity_counts = {} - for s in Severity: - severity_counts[s.value] = sum(1 for f in self.findings if f.severity == s) - return { - "session_id": self.session_id, - "target": self.target, - "state": self.state.value, - "duration_seconds": (datetime.utcnow() - self.started_at).seconds, - "findings_total": len(self.findings), - "severity_breakdown": severity_counts, - } + """ + Legacy enum. Mirrors ScanState values exactly so equality checks + against ScanState still work (both are str-enums), plus exposes + IDLE as an alias for the initial state. + + >>> SessionState.IDLE == ScanState.CREATED # str-equal + True + >>> SessionState.RECON.value == ScanState.RECON.value + True + """ + # IDLE was the old name for the initial state; CREATED is the new name. + # We keep IDLE present and equal to "created" so old tests pass. + IDLE = "created" + CREATED = "created" + RUNNING = "running" + RECON = "recon" + INTEL = "intel" + EXPLOIT = "exploit" + REPORT = "report" + REPORTING = "report" # legacy alias of REPORT + COMPLETED = "completed" + COMPLETE = "completed" # legacy alias of COMPLETED + FAILED = "failed" + CANCELLED = "cancelled" + + +# ── compatibility session class ─────────────────────────────────────── + + +class PentestSession(ScanSession): + """ + Legacy alias for ScanSession. + + Differences from plain ScanSession (all for backward compat): + + * Initial state is exposed as `state == SessionState.IDLE` + (the underlying value is "created" so equality holds). + * Provides `recon_data` / `intel_data` / `exploit_data` dict + attributes that some old tests and modules still write to. + * Provides `set_state()` as an alias for the new `set_phase()`, + accepting both SessionState and ScanState values. + """ + + def __init__(self, target: str = "", **kwargs: object) -> None: + super().__init__(target=target, **kwargs) + self.recon_data: dict = {} + self.intel_data: dict = {} + self.exploit_data: dict = {} + + # Legacy method name used by older tests + def set_state(self, state: "SessionState | ScanState | str") -> None: + """Set the session state. Accepts SessionState, ScanState, or raw str.""" + if isinstance(state, Enum): + value = state.value + else: + value = str(state) + # ScanState accepts the string value because both are str-enums + self.state = ScanState(value) + + +__all__ = [ + "Finding", + "PentestSession", + "PhaseResult", + "ScanPhase", + "ScanSession", + "ScanState", + "SessionState", + "Severity", +] + + +def _emit_deprecation_once() -> None: + """Emit deprecation warning at import time, once per process.""" + warnings.warn( + "cyberai.core.session is deprecated; " + "import from cyberai.core.scan_session instead. " + "This shim will be removed in day 7 of the STANDOFF rewrite.", + DeprecationWarning, + stacklevel=2, + ) + + +_emit_deprecation_once() From 22b5e6288d31d268339c80ac2d44ba0e232492d7 Mon Sep 17 00:00:00 2001 From: Evgeny Kiriyak <224408464+evkir@users.noreply.github.com> Date: Thu, 21 May 2026 10:59:32 +0300 Subject: [PATCH 3/4] test(core): cover Finding new fields and session compat shim (14 tests) --- tests/unit/test_finding_model.py | 72 ++++++++++++++++++++++++++++++++ tests/unit/test_session_shim.py | 55 ++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 tests/unit/test_finding_model.py create mode 100644 tests/unit/test_session_shim.py diff --git a/tests/unit/test_finding_model.py b/tests/unit/test_finding_model.py new file mode 100644 index 0000000..c7d84ea --- /dev/null +++ b/tests/unit/test_finding_model.py @@ -0,0 +1,72 @@ +"""Tests for Finding dataclass and ScanSession.add_finding() — KI-5 fix.""" +from __future__ import annotations + +from cyberai.core.scan_session import Finding, ScanSession, Severity + + +def test_finding_accepts_target_and_evidence(): + f = Finding( + id=1, severity=Severity.HIGH, title="t", description="d", + timestamp="2026-05-21T00:00:00+00:00", agent="recon", + target="10.0.0.1", evidence=["nmap line"], + ) + assert f.target == "10.0.0.1" + assert f.evidence == ["nmap line"] + + +def test_finding_cve_and_cve_ids_sync_from_single(): + f = Finding( + id=1, severity=Severity.HIGH, title="t", description="d", + timestamp="2026-05-21T00:00:00+00:00", agent="intel", + cve="CVE-2024-1234", + ) + assert f.cve_ids == ["CVE-2024-1234"] + + +def test_finding_cve_and_cve_ids_sync_from_list(): + f = Finding( + id=1, severity=Severity.HIGH, title="t", description="d", + timestamp="2026-05-21T00:00:00+00:00", agent="intel", + cve_ids=["CVE-2024-1234", "CVE-2024-5678"], + ) + assert f.cve == "CVE-2024-1234" + + +def test_finding_defaults_not_shared(): + f1 = Finding(id=1, severity=Severity.LOW, title="a", description="d", + timestamp="2026-05-21T00:00:00+00:00", agent="x") + f2 = Finding(id=2, severity=Severity.LOW, title="b", description="d", + timestamp="2026-05-21T00:00:00+00:00", agent="x") + f1.evidence.append("only f1") + assert f2.evidence == [] + + +def test_session_add_finding_basic(): + s = ScanSession(target="example.com") + f = s.add_finding(Severity.MEDIUM, "Open SSH", "Port 22", "recon") + assert f.id == 1 + assert f.target == "example.com" + + +def test_session_add_finding_explicit_target(): + s = ScanSession(target="parent") + f = s.add_finding(Severity.HIGH, "t", "d", "a", target="sub.example.com") + assert f.target == "sub.example.com" + + +def test_session_findings_get_sequential_ids(): + s = ScanSession(target="x") + for i in range(3): + s.add_finding(Severity.INFO, f"t{i}", "d", "a") + assert [f.id for f in s.findings] == [1, 2, 3] + + +def test_session_summary_severity_breakdown(): + s = ScanSession(target="x") + s.add_finding(Severity.CRITICAL, "c", "d", "a") + s.add_finding(Severity.HIGH, "h1", "d", "a") + s.add_finding(Severity.HIGH, "h2", "d", "a") + summary = s.summary() + assert summary["findings_total"] == 3 + assert summary["severity_breakdown"]["CRITICAL"] == 1 + assert summary["severity_breakdown"]["HIGH"] == 2 diff --git a/tests/unit/test_session_shim.py b/tests/unit/test_session_shim.py new file mode 100644 index 0000000..e8425ed --- /dev/null +++ b/tests/unit/test_session_shim.py @@ -0,0 +1,55 @@ +"""Backward-compat shim tests for cyberai.core.session — day 3 of STANDOFF.""" +from __future__ import annotations + +import warnings + + +def test_legacy_imports_still_work(): + from cyberai.core.session import ( + Finding, PentestSession, Severity, SessionState, + ) + assert Severity.CRITICAL == "CRITICAL" + assert SessionState.IDLE.value == "created" + + +def test_pentestsession_is_a_scansession(): + from cyberai.core.scan_session import ScanSession + from cyberai.core.session import PentestSession + assert isinstance(PentestSession(target="x"), ScanSession) + + +def test_pentestsession_keeps_legacy_data_attrs(): + from cyberai.core.session import PentestSession + s = PentestSession(target="x") + s.recon_data["nmap"] = {"ports": [80]} + s.intel_data["cves"] = [] + s.exploit_data["chains"] = [] + assert s.recon_data["nmap"] == {"ports": [80]} + + +def test_pentestsession_add_finding_works(): + from cyberai.core.session import PentestSession, Severity + s = PentestSession(target="x") + f = s.add_finding(Severity.HIGH, "t", "d", "recon") + assert f.id == 1 + + +def test_pentestsession_set_state_legacy_method(): + from cyberai.core.session import PentestSession, SessionState + s = PentestSession(target="x") + assert s.state == SessionState.IDLE + s.set_state(SessionState.RECON) + assert s.state.value == "recon" + + +def test_session_module_emits_deprecation_warning(): + import importlib + import cyberai.core.session as legacy + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + importlib.reload(legacy) + assert any( + issubclass(x.category, DeprecationWarning) + and "scan_session" in str(x.message) + for x in w + ) From ba5bc4a16b61934c54db14d2e9f07e6770648fbc Mon Sep 17 00:00:00 2001 From: Evgeny Kiriyak <224408464+evkir@users.noreply.github.com> Date: Thu, 21 May 2026 11:00:05 +0300 Subject: [PATCH 4/4] docs: mark KI-2 and KI-5 as fixed (3/8 closed) --- docs/architecture/known-issues.md | 94 ++++++++----------------------- 1 file changed, 23 insertions(+), 71 deletions(-) diff --git a/docs/architecture/known-issues.md b/docs/architecture/known-issues.md index 1d2479a..05c112f 100644 --- a/docs/architecture/known-issues.md +++ b/docs/architecture/known-issues.md @@ -1,90 +1,42 @@ # Known Issues — Pre-W1 Baseline -This document captures the broken state of CyberAI **as of the start of -the 30-day STANDOFF rewrite**. Each item is fixed by a specific day in -the plan; see `STANDOFF.md` for the schedule. - -When all items are checked off, days 1–7 (Reanimation week) are done -and `cyberai scan --dry-run` will work end-to-end. - -## How this was verified - -Smoke tests in `tests/integration/test_cli_smoke.py` reproduce the broken -state via `CliRunner().invoke(cli, ["scan", ..., "--dry-run"])`. They are -marked `@pytest.mark.xfail` until day 7, then un-xfailed to provide -regression protection. +Tracks the broken state of CyberAI at the start of the 30-day STANDOFF +rewrite. Each item is fixed by a specific day; see `STANDOFF.md`. ## The Issues ### 🔴 KI-1 — CLI ↔ Orchestrator API mismatch -- **What's broken:** `__main__.py` calls `Orchestrator(config)` and - `orchestrator.run_pipeline(session)`. Neither matches the actual API: - `Orchestrator.__init__(phases, authorized_scope, dry_run)` does not - accept `config`, and the method is named `run(target)`. -- **Symptom:** `TypeError` on any `cyberai scan` invocation. -- **Fixed by:** Day 5 (`refactor/orchestrator-v2`) -- **Status:** ❌ broken +`__main__.py` calls `Orchestrator(config)` and `run_pipeline(session)` — +neither matches the actual API. **Fixed by:** Day 5. -### 🔴 KI-2 — Two competing session classes -- **What's broken:** `PentestSession` (in `core/session.py`) and - `ScanSession` (in `core/scan_session.py`) coexist with different - fields and methods. `__main__.py` uses `PentestSession`; `Orchestrator` - creates `ScanSession`. -- **Fixed by:** Day 3 (`refactor/unify-session`) -- **Status:** ❌ broken +### 🟢 KI-2 — Two competing session classes ✅ FIXED IN DAY 3 +`scan_session.py` is now the single source of truth. `session.py` is a +backward-compat shim where `PentestSession` is a subclass of +`ScanSession` preserving legacy attributes. All 8 import sites work +unchanged. Verified by `tests/unit/test_session_shim.py`. ### 🔴 KI-3 — BaseAgent doesn't match what agents use -- **What's broken:** `BaseAgent.__init__(config, audit, session_id)` is - what's declared, but agents access `self.session`, `self.kb`, - `self.memory`, `self.llm` — none of which exist on `BaseAgent`. The - Orchestrator constructs agents as `ReconAgent(kb=session.kb)`, which - also doesn't match. -- **Fixed by:** Day 4 (`refactor/base-agent-contract`) -- **Status:** ❌ broken +Agents access `self.session`, `self.kb`, `self.memory`, `self.llm` — +none exist on `BaseAgent`. **Fixed by:** Day 4. ### 🔴 KI-4 — Agents call non-existent methods -- **What's broken:** Several agents call `self._check_iteration_limit()`, - `self._log(...)`, `self.llm.chat(...)` — none of these exist. -- **Fixed by:** Day 4 + Day 6 -- **Status:** ❌ broken +`self._check_iteration_limit()`, `self._log()`, `self.llm.chat()` — +none exist. **Fixed by:** Day 4 + Day 6. -### 🔴 KI-5 — `Finding` signature mismatch -- **What's broken:** `ReconAgent` builds `Finding(title=..., target=..., - evidence=[...])`, but the `Finding` dataclass has no `target` or - `evidence` fields. -- **Fixed by:** Day 3 -- **Status:** ❌ broken +### 🟢 KI-5 — Finding signature mismatch ✅ FIXED IN DAY 3 +`Finding` now has `target`, `evidence`, `cve_ids` fields with +backward-compat `cve` ↔ `cve_ids` syncing. `ScanSession.add_finding()` +auto-fills `target` from `session.target`. Verified by +`tests/unit/test_finding_model.py`. ### 🔴 KI-6 — `Tool` param name mismatch -- **What's broken:** `Tool` dataclass field is `params`, but every - `_register_tools()` call uses `parameters=...`. -- **Fixed by:** Day 4 -- **Status:** ❌ broken +`Tool` field is `params`, agents register with `parameters=`. +**Fixed by:** Day 4. ### 🔴 KI-7 — `LLMClient.chat()` doesn't exist -- **What's broken:** `ExploitAgent` calls `self.llm.chat(messages=..., - system=...)`. The actual `LLMClient` method is `call()`. -- **Fixed by:** Day 6 -- **Status:** ❌ broken - -### 🔴 KI-8 — `conftest.fresh_session` accesses non-existent field -- **What's broken:** Original `conftest.py` did - `fresh_session.knowledge_base["recon.nmap"] = ...` but `PentestSession` - has no `knowledge_base` field — only `recon_data` / `intel_data` / - `exploit_data`. -- **Fixed by:** Day 2 (this PR) — temporarily redirected to `recon_data` -- **Status:** ✅ patched (full unification in day 3) - -## Reproduction - -```bash -# Will raise TypeError before any real work happens: -python -m cyberai scan 127.0.0.1 --dry-run +Actual method is `call()`. **Fixed by:** Day 6. -# Smoke tests reproduce this state: -pytest tests/integration/test_cli_smoke.py -v -# Expected: 2 xfailed, 1 passed -``` +### 🟢 KI-8 — conftest accessed non-existent field ✅ FIXED IN DAY 2 ## Progress tracker @@ -92,7 +44,7 @@ pytest tests/integration/test_cli_smoke.py -v |-----|-------------------|--------| | 1 | (rebrand only) | ✅ | | 2 | KI-8 | ✅ | -| 3 | KI-2, KI-5 | ⏳ | +| 3 | KI-2, KI-5 | ✅ | | 4 | KI-3, KI-4, KI-6 | ⏳ | | 5 | KI-1 | ⏳ | | 6 | KI-7, KI-4 | ⏳ |