Skip to content

fix(api): reject blank document fields before indexing - #46

Merged
johnnyhuy merged 1 commit into
mainfrom
agent/issue-45-document-validation
Nov 14, 2026
Merged

johnnyhuy merged 1 commit into
mainfrom
agent/issue-45-document-validation

Conversation

@johnnyhuy

@johnnyhuy johnnyhuy commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

Reject empty and whitespace-only document titles, content and sources with HTTP 422 before storage or embedding. Trim titles and sources while preserving valid content exactly, including code indentation and trailing newlines.

Part of #5. Closes #45.

Verified with the repository gates and local API capture.

  • UV_PYTHON=3.13 UV_FROZEN=1 mise --no-env run install passed. The first unrestricted attempt selected Python 3.14, which has no wheel for locked onnxruntime 1.23.2. Python 3.13 matches mise.toml; the frozen lockfile avoids unrelated dependency changes.
  • mise run test-backend passed, 172 tests including 19 new document creation cases. Existing dependencies emit 731 deprecation warnings.
  • mise run test-frontend passed, 15 tests.
  • mise run dev-backend -- --host 127.0.0.1 --port 18745 reached application startup complete, served the curl capture, and shut down cleanly.
  • git diff --check passed before the commit.

The checks used a clean environment with MISE_NO_ENV=1, UV_PYTHON=3.13, UV_FROZEN=1, fixture Slack credentials and a temporary Python guard that blocks Slack API calls, Socket Mode and non-loopback socket connections. No real credentials or .env files were read. The API proof used temporary local SQLite and Chroma storage with fixed embeddings. The shared RAG and vector-store singletons and their real indexing path were retained.

The curl transcript shows all three blank fields returning 422, document and retrieval lists unchanged after rejection, and a valid code document stored and retrieved with its formatting intact. Fixed-vector distance 0 demonstrates indexing wiring only, not semantic relevance. The proof comment contains the full transcript, new test names and capture setup.

Backend capture method (a) succeeded, with an additional fixture retrieval transcript. No frontend UI changed, so a screenshot was not applicable. No capture method was blocked. No migration or live Slack verification is needed for this slice. Existing documents are unchanged; this validation applies to new API submissions.

Validate document title, source and content before persistence. Preserve valid content formatting and cover rejected input, source defaults and successful indexing with offline tests.

Co-authored-by: OpenAI Codex <noreply@openai.com>
@johnnyhuy

Copy link
Copy Markdown
Contributor Author

Captured offline against commit 16d58bd on 14 November 2026. All assertions in the curl capture passed.

The backend gate listed these new tests, all passing. Invalid requests leave the seeded database unchanged and never call indexing. Successful requests test both the default and explicit source, the values passed to indexing, and read-back of the saved document.

tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[-title] PASSED [ 41%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[-content] PASSED [ 42%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[-source] PASSED [ 43%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[ \n\t -title] PASSED [ 43%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[ \n\t -content] PASSED [ 44%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[ \n\t -source] PASSED [ 44%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[\u2003\xa0-title] PASSED [ 45%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[\u2003\xa0-content] PASSED [ 45%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[\u2003\xa0-source] PASSED [ 46%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[None-title] PASSED [ 47%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[None-content] PASSED [ 47%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[None-source] PASSED [ 48%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[123-title] PASSED [ 48%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[123-content] PASSED [ 49%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[123-source] PASSED [ 50%]
tests/test_documents.py::TestCreateDocument::test_rejects_missing_required_fields[title] PASSED [ 50%]
tests/test_documents.py::TestCreateDocument::test_rejects_missing_required_fields[content] PASSED [ 51%]
tests/test_documents.py::TestCreateDocument::test_creates_readable_document_and_indexes_preserved_content[None] PASSED [ 51%]
tests/test_documents.py::TestCreateDocument::test_creates_readable_document_and_indexes_preserved_content[ \tdocs\n ] PASSED [ 52%]
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
====================== 172 passed, 731 warnings in 7.36s =======================

The frontend gate also passed. No frontend UI changed.

[test-frontend] $ bun run test
$ bun test --preload ./tests/setup.ts
bun test v1.3.5 (1e86cebd)

tests/retrieval.test.tsx:
(pass) retrieval waits for a non-blank question and submits trimmed optional scope [55.35ms]
(pass) retrieval omits blank scope, preserves rank and raw distances, and shows an unset cutoff [14.12ms]
(pass) retrieval disables inputs and duplicate submissions while loading [11.42ms]
(pass) retrieval shows an empty result and a zero distance cutoff [9.45ms]
(pass) retrieval replaces a failed request with a successful retry [14.88ms]
(pass) retrieval clears old results when the question or channel changes [12.04ms]
(pass) retrieval aborts on leaving the tab and ignores late responses after reopening [14.00ms]
(pass) retrieval renders chunk and metadata markup as text [7.62ms]

tests/conversations.test.tsx:
(pass) loads only on opening the tab and paginates summaries and chronological turns independently [125.21ms]
(pass) shows loading, an empty page and a list error with a working retry [6.47ms]
(pass) retries missing conversation details without discarding the list [15.65ms]
(pass) ignores stale detail responses after selecting another conversation [14.21ms]
(pass) ignores stale errors after leaving and reopening the tab [8.26ms]
(pass) renders stored markup as text and supports missing metadata [8.54ms]
(pass) encodes conversation IDs in detail request paths [1.59ms]

 15 pass
 0 fail
 92 expect() calls
Ran 15 tests across 2 files. [692.00ms]

The backend started and stopped through mise run dev-backend -- --host 127.0.0.1 --port 18745.

INFO:     Uvicorn running on http://127.0.0.1:18745 (Press CTRL+C to quit)
INFO:     Application startup complete.
INFO:     Application shutdown complete.
INFO:     Stopping reloader process [20271]

The real HTTP capture used curl against the fixture backend. Temporary SQLite stores the documents and temporary Chroma uses fixed embeddings. No model, embedding provider, Pi runtime or Slack connection was used. Empty retrieval before and after rejected writes proves that invalid requests did not add chunks. The final fixed-vector distance demonstrates successful indexing and exact content preservation, not relevance quality.

$ curl -sS -w '
HTTP %{http_code}
' http://127.0.0.1:18745/health

{
  "status": "healthy"
}

HTTP 200

$ curl -sS -w '
HTTP %{http_code}
' http://127.0.0.1:18745/api/documents

{
  "items": [],
  "total": 0,
  "limit": 50,
  "offset": 0
}

HTTP 200

$ curl -sS -w '
HTTP %{http_code}
' http://127.0.0.1:18745/api/retrieval/preview -H 'Content-Type: application/json' -d '{"question": "How do I reset VPN?"}'

{
  "question": "How do I reset VPN?",
  "channel_id": null,
  "top_k": 5,
  "max_distance": null,
  "items": []
}

HTTP 200

$ curl -sS -w '
HTTP %{http_code}
' http://127.0.0.1:18745/api/documents -H 'Content-Type: application/json' -d '{"title": " \n\t ", "content": "Reset via IT portal.", "source": "docs"}'

{
  "detail": [
    {
      "type": "string_too_short",
      "loc": [
        "body",
        "title"
      ],
      "msg": "String should have at least 1 character",
      "input": " \n\t ",
      "ctx": {
        "min_length": 1
      }
    }
  ]
}

HTTP 422

$ curl -sS -w '
HTTP %{http_code}
' http://127.0.0.1:18745/api/documents -H 'Content-Type: application/json' -d '{"title": "VPN guide", "content": " \n\t ", "source": "docs"}'

{
  "detail": [
    {
      "type": "value_error",
      "loc": [
        "body",
        "content"
      ],
      "msg": "Value error, Content must not be blank",
      "input": " \n\t ",
      "ctx": {
        "error": {}
      }
    }
  ]
}

HTTP 422

$ curl -sS -w '
HTTP %{http_code}
' http://127.0.0.1:18745/api/documents -H 'Content-Type: application/json' -d '{"title": "VPN guide", "content": "Reset via IT portal.", "source": " \n\t "}'

{
  "detail": [
    {
      "type": "string_too_short",
      "loc": [
        "body",
        "source"
      ],
      "msg": "String should have at least 1 character",
      "input": " \n\t ",
      "ctx": {
        "min_length": 1
      }
    }
  ]
}

HTTP 422

$ curl -sS -w '
HTTP %{http_code}
' http://127.0.0.1:18745/api/documents

{
  "items": [],
  "total": 0,
  "limit": 50,
  "offset": 0
}

HTTP 200

$ curl -sS -w '
HTTP %{http_code}
' http://127.0.0.1:18745/api/retrieval/preview -H 'Content-Type: application/json' -d '{"question": "How do I reset VPN?"}'

{
  "question": "How do I reset VPN?",
  "channel_id": null,
  "top_k": 5,
  "max_distance": null,
  "items": []
}

HTTP 200

$ curl -sS -w '
HTTP %{http_code}
' http://127.0.0.1:18745/api/documents -H 'Content-Type: application/json' -d '{"title": "  VPN guide\n", "content": "\n    def reset_vpn():\n        return \"IT portal\"\n\n", "source": " docs "}'

{
  "id": 1,
  "message": "Document added successfully"
}

HTTP 200

$ curl -sS -w '
HTTP %{http_code}
' http://127.0.0.1:18745/api/documents/1

{
  "id": 1,
  "title": "VPN guide",
  "content": "\n    def reset_vpn():\n        return \"IT portal\"\n\n",
  "source": "docs",
  "created_at": "2026-11-14T10:21:44.523239",
  "updated_at": "2026-11-14T10:21:44.523246"
}

HTTP 200

$ curl -sS -w '
HTTP %{http_code}
' http://127.0.0.1:18745/api/retrieval/preview -H 'Content-Type: application/json' -d '{"question": "How do I reset VPN?"}'

{
  "question": "How do I reset VPN?",
  "channel_id": null,
  "top_k": 5,
  "max_distance": null,
  "items": [
    {
      "content": "\n    def reset_vpn():\n        return \"IT portal\"\n\n",
      "metadata": {
        "chunk": 0,
        "source": "docs",
        "title": "VPN guide"
      },
      "distance": 0.0
    }
  ]
}

HTTP 200

All assertions passed. Fixed vectors demonstrate validation and indexing wiring only, not semantic relevance.

The temporary proof guard was loaded through PYTHONPATH before importing the application. The clean process environment supplied fixture-only Slack credentials, MISE_NO_ENV=1, UV_PYTHON=3.13 and UV_FROZEN=1. The following setup keeps the application singletons and blocks external connections. It is a local capture harness, not a shipped application change.

"""Local proof guard. Never imported by the shipped application."""
import ipaddress
import socket
from slack_sdk import WebClient
from slack_bolt.adapter.socket_mode import SocketModeHandler

_original_connect = socket.socket.connect

def local_connect(sock, address):
    if isinstance(address, tuple):
        host = address[0]
        try:
            local = ipaddress.ip_address(host).is_loopback
        except ValueError:
            local = host == "localhost"
        if not local:
            raise AssertionError(f"Offline proof blocked outbound connection to {host}")
    return _original_connect(sock, address)

def blocked(*args, **kwargs):
    raise AssertionError("Offline proof must not contact Slack or start Socket Mode")

socket.socket.connect = local_connect
WebClient.auth_test = lambda *args, **kwargs: {
    "ok": True, "bot_id": "BTEST", "user_id": "UTEST", "team_id": "TTEST",
}
WebClient.api_call = blocked
SocketModeHandler.start = blocked

import os
if os.environ.get("WINGMAN_PROOF_FIXTURES") == "1":
    import chromadb
    from chromadb.config import Settings as ChromaSettings
    from chromadb.telemetry.product.posthog import Posthog
    from langchain_community.vectorstores import Chroma
    from langchain_core.embeddings import Embeddings
    from app.vector_store import vector_store

    class FixtureEmbeddings(Embeddings):
        def embed_documents(self, texts):
            return [[1.0, 0.0] for _ in texts]
        def embed_query(self, text):
            return [1.0, 0.0]

    Posthog.capture = lambda *args, **kwargs: None
    vector_store.vector_store = Chroma(
        client=chromadb.PersistentClient(
            path=os.environ["WINGMAN_PROOF_CHROMA"],
            settings=ChromaSettings(anonymized_telemetry=False),
        ),
        collection_name="document-validation-proof",
        embedding_function=FixtureEmbeddings(),
    )
    vector_store._initialized = True

Backend capture method (a) succeeded. No capture method was blocked; a UI screenshot was not applicable. No real tokens or .env files were read, no Slack message was sent and Socket Mode was never started.

@johnnyhuy

Copy link
Copy Markdown
Contributor Author

Revalidated the existing slice at 16d58bd for the merge sweep on 14 November 2026. No new application changes were needed. All remote checks were green and the branch was current with main.

The following commands passed in a clean process environment with MISE_NO_ENV=1, UV_PYTHON=3.13 and UV_FROZEN=1. A temporary PYTHONPATH guard disabled Pydantic .env loading, blocked non-loopback Python connections, mocked Slack authentication and blocked Slack API calls and Socket Mode. No real credentials were used or .env files read.

  • mise --no-env run install passed.
  • mise --no-env run test-backend passed, 172 tests and 731 existing deprecation warnings.
  • mise --no-env run test-frontend passed, 15 tests.
  • mise --no-env run dev-backend -- --host 127.0.0.1 --port 18746 started, served the fixture curl capture and stopped cleanly.
  • git diff --check passed; the tracked worktree stayed clean.

Backend capture method (a) succeeded again. No capture path was blocked. No frontend UI changed, so a screenshot was not applicable. No live Slack verification or migration was needed.

The new document tests passed again.

tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[-title] PASSED [ 41%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[-content] PASSED [ 42%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[-source] PASSED [ 43%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[ \n\t -title] PASSED [ 43%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[ \n\t -content] PASSED [ 44%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[ \n\t -source] PASSED [ 44%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[\u2003\xa0-title] PASSED [ 45%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[\u2003\xa0-content] PASSED [ 45%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[\u2003\xa0-source] PASSED [ 46%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[None-title] PASSED [ 47%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[None-content] PASSED [ 47%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[None-source] PASSED [ 48%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[123-title] PASSED [ 48%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[123-content] PASSED [ 49%]
tests/test_documents.py::TestCreateDocument::test_rejects_invalid_fields_without_writes_or_indexing[123-source] PASSED [ 50%]
tests/test_documents.py::TestCreateDocument::test_rejects_missing_required_fields[title] PASSED [ 50%]
tests/test_documents.py::TestCreateDocument::test_rejects_missing_required_fields[content] PASSED [ 51%]
tests/test_documents.py::TestCreateDocument::test_creates_readable_document_and_indexes_preserved_content[None] PASSED [ 51%]
tests/test_documents.py::TestCreateDocument::test_creates_readable_document_and_indexes_preserved_content[ \tdocs\n ] PASSED [ 52%]
====================== 172 passed, 731 warnings in 6.87s =======================
[test-frontend] $ bun run test
$ bun test --preload ./tests/setup.ts
bun test v1.3.5 (1e86cebd)

tests/retrieval.test.tsx:
(pass) retrieval waits for a non-blank question and submits trimmed optional scope [71.32ms]
(pass) retrieval omits blank scope, preserves rank and raw distances, and shows an unset cutoff [20.43ms]
(pass) retrieval disables inputs and duplicate submissions while loading [10.30ms]
(pass) retrieval shows an empty result and a zero distance cutoff [13.15ms]
(pass) retrieval replaces a failed request with a successful retry [16.12ms]
(pass) retrieval clears old results when the question or channel changes [12.59ms]
(pass) retrieval aborts on leaving the tab and ignores late responses after reopening [16.58ms]
(pass) retrieval renders chunk and metadata markup as text [8.59ms]

tests/conversations.test.tsx:
(pass) loads only on opening the tab and paginates summaries and chronological turns independently [133.56ms]
(pass) shows loading, an empty page and a list error with a working retry [6.60ms]
(pass) retries missing conversation details without discarding the list [16.94ms]
(pass) ignores stale detail responses after selecting another conversation [16.98ms]
(pass) ignores stale errors after leaving and reopening the tab [7.13ms]
(pass) renders stored markup as text and supports missing metadata [8.42ms]
(pass) encodes conversation IDs in detail request paths [1.56ms]

 15 pass
 0 fail
 92 expect() calls
Ran 15 tests across 2 files. [791.00ms]

INFO:     Uvicorn running on http://127.0.0.1:18746 (Press CTRL+C to quit)
INFO:     Application startup complete.
INFO:     Application shutdown complete.
INFO:     Stopping reloader process [37055]

The fixture API used fresh local SQLite and Chroma storage with fixed embeddings, retaining the shared RAG and vector-store singletons. The following curl transcript proves that all three blank fields are rejected with no documents or chunks stored, then valid content is stored and retrieved with formatting intact. Distance zero proves indexing wiring only, not semantic relevance.

$ curl --noproxy '*' -sS -w '
HTTP %{http_code}
' http://127.0.0.1:18746/health
{
  "status": "healthy"
}
HTTP 200

$ curl --noproxy '*' -sS -w '
HTTP %{http_code}
' http://127.0.0.1:18746/api/documents
{
  "items": [],
  "total": 0,
  "limit": 50,
  "offset": 0
}
HTTP 200

$ curl --noproxy '*' -sS -w '
HTTP %{http_code}
' http://127.0.0.1:18746/api/retrieval/preview -H 'Content-Type: application/json' -d '{"question": "How do I reset VPN?"}'
{
  "question": "How do I reset VPN?",
  "channel_id": null,
  "top_k": 5,
  "max_distance": null,
  "items": []
}
HTTP 200

$ curl --noproxy '*' -sS -w '
HTTP %{http_code}
' http://127.0.0.1:18746/api/documents -H 'Content-Type: application/json' -d '{"title": " \n\t ", "content": "Reset via IT portal.", "source": "docs"}'
{
  "detail": [
    {
      "type": "string_too_short",
      "loc": [
        "body",
        "title"
      ],
      "msg": "String should have at least 1 character",
      "input": " \n\t ",
      "ctx": {
        "min_length": 1
      }
    }
  ]
}
HTTP 422

$ curl --noproxy '*' -sS -w '
HTTP %{http_code}
' http://127.0.0.1:18746/api/documents -H 'Content-Type: application/json' -d '{"title": "VPN guide", "content": " \n\t ", "source": "docs"}'
{
  "detail": [
    {
      "type": "value_error",
      "loc": [
        "body",
        "content"
      ],
      "msg": "Value error, Content must not be blank",
      "input": " \n\t ",
      "ctx": {
        "error": {}
      }
    }
  ]
}
HTTP 422

$ curl --noproxy '*' -sS -w '
HTTP %{http_code}
' http://127.0.0.1:18746/api/documents -H 'Content-Type: application/json' -d '{"title": "VPN guide", "content": "Reset via IT portal.", "source": " \n\t "}'
{
  "detail": [
    {
      "type": "string_too_short",
      "loc": [
        "body",
        "source"
      ],
      "msg": "String should have at least 1 character",
      "input": " \n\t ",
      "ctx": {
        "min_length": 1
      }
    }
  ]
}
HTTP 422

$ curl --noproxy '*' -sS -w '
HTTP %{http_code}
' http://127.0.0.1:18746/api/documents
{
  "items": [],
  "total": 0,
  "limit": 50,
  "offset": 0
}
HTTP 200

$ curl --noproxy '*' -sS -w '
HTTP %{http_code}
' http://127.0.0.1:18746/api/retrieval/preview -H 'Content-Type: application/json' -d '{"question": "How do I reset VPN?"}'
{
  "question": "How do I reset VPN?",
  "channel_id": null,
  "top_k": 5,
  "max_distance": null,
  "items": []
}
HTTP 200

$ curl --noproxy '*' -sS -w '
HTTP %{http_code}
' http://127.0.0.1:18746/api/documents -H 'Content-Type: application/json' -d '{"title": "  VPN guide\n", "content": "\n    def reset_vpn():\n        return \"IT portal\"\n\n", "source": " docs "}'
{
  "id": 1,
  "message": "Document added successfully"
}
HTTP 200

$ curl --noproxy '*' -sS -w '
HTTP %{http_code}
' http://127.0.0.1:18746/api/documents/1
{
  "id": 1,
  "title": "VPN guide",
  "content": "\n    def reset_vpn():\n        return \"IT portal\"\n\n",
  "source": "docs",
  "created_at": "2026-11-14T14:19:41.236681",
  "updated_at": "2026-11-14T14:19:41.236687"
}
HTTP 200

$ curl --noproxy '*' -sS -w '
HTTP %{http_code}
' http://127.0.0.1:18746/api/retrieval/preview -H 'Content-Type: application/json' -d '{"question": "How do I reset VPN?"}'
{
  "question": "How do I reset VPN?",
  "channel_id": null,
  "top_k": 5,
  "max_distance": null,
  "items": [
    {
      "content": "\n    def reset_vpn():\n        return \"IT portal\"\n\n",
      "metadata": {
        "chunk": 0,
        "source": "docs",
        "title": "VPN guide"
      },
      "distance": 0.0
    }
  ]
}
HTTP 200

All assertions passed. Fixed vectors demonstrate validation and indexing wiring, not semantic relevance.

@johnnyhuy
johnnyhuy merged commit 5e381ff into main Nov 14, 2026
10 checks passed
@johnnyhuy
johnnyhuy deleted the agent/issue-45-document-validation branch November 14, 2026 14:20
@github-actions github-actions Bot mentioned this pull request Nov 14, 2026
@johnnyhuy

Copy link
Copy Markdown
Contributor Author

Merged as 5e381ff. Re-ran mise --no-env run test-backend and mise --no-env run test-frontend on the fetched origin/main commit using the same offline guard and clean fixture environment. Both passed again, with 172 backend tests and 15 frontend tests. The tracked worktree is clean. The remote agent branch and its leftover local branch were deleted; the previous worktree was clean and left detached with its files preserved. Child #45 is closed by this merge; parent #5 remains open. Release PR #12 was skipped. No new slice was started under the existing-agent-PR lease.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reject blank document fields before persistence and indexing

1 participant