Skip to content

Reconcile doc-review (#1835) into dev: master-only feature discovered at beta.45 promote - #2247

Merged
jaylfc merged 3 commits into
devfrom
exec/tsk-737pd7
Aug 3, 2026
Merged

Reconcile doc-review (#1835) into dev: master-only feature discovered at beta.45 promote#2247
jaylfc merged 3 commits into
devfrom
exec/tsk-737pd7

Conversation

@jaylfc

@jaylfc jaylfc commented Aug 2, 2026

Copy link
Copy Markdown
Owner

CARD TITLE (intent, not commit subject): Reconcile doc-review (#1835) into dev: master-only feature discovered at beta.45 promote

Autonomous build of board card tsk-737pd7.

Files:
tests/test_doc_review.py | 358 +++++++++++++++++++++++++++++++
tinyagentos/app.py | 5 +
tinyagentos/auth_middleware.py | 21 ++
tinyagentos/projects/doc_review_store.py | 149 +++++++++++++
tinyagentos/routes/init.py | 3 +
tinyagentos/routes/agent_registry.py | 1 +
tinyagentos/routes/project_doc_review.py | 112 ++++++++++
9 files changed, 660 insertions(+)

Summary by CodeRabbit

  • New Features

    • Added project document-review tracking with awaiting review, approved, and changes requested states.
    • Added review badges to file grid and list views, including a Review column.
    • Added APIs to retrieve, update, list, and filter document reviews.
    • Added scoped agent access for authorized project document-review operations.
    • Added clear handling for missing reviews, invalid transitions, and inaccessible projects.
  • Documentation

    • Documented document-review access rules and supported operations.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds SQLite-backed document-review state management, project-scoped HTTP routes, agent-token authorization, application lifecycle integration, registry scope support, desktop Files app integration, documentation, and asynchronous tests.

Changes

Document review workflow

Layer / File(s) Summary
Review persistence and state transitions
tinyagentos/projects/doc_review_store.py
Adds the review schema, transition validation, actor and timestamp recording, lookup, listing, and deletion.
Review API and application wiring
tinyagentos/routes/project_doc_review.py, tinyagentos/routes/__init__.py, tinyagentos/app.py
Adds authenticated GET, PUT, and list endpoints. Registers the router and manages store startup, shutdown, and application state.
Agent scope and route authorization
tinyagentos/auth_middleware.py, tinyagentos/routes/agent_registry.py, tinyagentos/routes/agent_auth_requests.py, docs/agent-coordination.md, tests/test_agent_registry.py
Adds the project_doc_review grant scope and restricts registry-JWT access to project document-review routes.
Desktop review state integration
desktop/src/lib/projects.ts, desktop/src/apps/FilesApp.tsx
Adds review types and API methods. Displays review badges in grid and list views and cycles review states through the project API.
Persistence and route validation
tests/test_doc_review.py
Tests state transitions, metadata, filtering, deletion, owner access, agent access, project isolation, scope errors, and authentication errors.
Release documentation
CHANGELOG.md
Records the document-review feature in the Unreleased changelog.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant FilesApp
  participant AuthMiddleware
  participant project_doc_review
  participant DocReviewStore
  participant SQLite
  FilesApp->>AuthMiddleware: Request document-review state
  AuthMiddleware->>project_doc_review: Pass authorized request
  project_doc_review->>DocReviewStore: Read or update review
  DocReviewStore->>SQLite: Load or persist review state
  SQLite-->>DocReviewStore: Return review record
  DocReviewStore-->>project_doc_review: Return review data
  project_doc_review-->>FilesApp: Return HTTP response
Loading

Possibly related PRs

  • jaylfc/taOS#2232: Also changes authenticated non-session route handling in tinyagentos/auth_middleware.py.
  • jaylfc/taOS#2240: Also changes project-scoped registry-JWT authorization in tinyagentos/auth_middleware.py.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: reconciling the doc-review feature into the dev branch.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch exec/tsk-737pd7

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gitar-bot

gitar-bot Bot commented Aug 2, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add project doc-review stamps with agent-scoped API + persistence

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add per-project doc-review stamp store with a small review-state machine.
• Expose GET/PUT doc-review APIs gated by owner session or scoped agent JWT.
• Extend agent-token allowlist and scope minting, with coverage for security invariants.
Diagram

graph TD
A["Client (admin or agent)"] --> B["Auth middleware allowlist"] --> C["Doc-review API routes"]
C --> D["DocReviewStore (SQLite)"]
C --> E["ProjectStore"]
C --> F["Agent grants/registry auth"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Persist stamps in project settings JSON (ProjectStore)
  • ➕ Avoids adding a new table/schema
  • ➕ Keeps all project metadata in one row
  • ➖ Harder to query/list/filter by review state efficiently
  • ➖ More contention on project row updates and higher risk of accidental overwrites
2. Store stamps as file-side metadata (e.g., sidecar .json in project files)
  • ➕ Human-inspectable and easy to export with the project filesystem
  • ➕ No DB schema changes
  • ➖ Concurrency/atomicity is trickier than SQLite transactions
  • ➖ Requires careful path normalization and backup/restore semantics
3. Model doc review as a generic activity/event stream
  • ➕ More extensible for future workflow states and auditing
  • ➕ Could unify with existing project activity concepts
  • ➖ Overkill for a small 3-state stamp model
  • ➖ More moving parts and more complex reads for the UI/API

Recommendation: The PR’s approach (dedicated SQLite table + small transition gate + dual-auth route wrapper) is the best fit for a simple, queryable per-doc stamp model. It keeps authorization explicit (middleware allowlist + project-scoped grant check) and provides strong regression coverage for the intended security invariants. The main thing to watch in review is ensuring path patterns remain narrowly allowlisted and that doc_path handling (slashes) doesn’t accidentally broaden the reachable surface.

Files changed (9) +660 / -0

Enhancement (6) +291 / -0
app.pyWire DocReviewStore into app lifecycle +5/-0

Wire DocReviewStore into app lifecycle

• Instantiates DocReviewStore against projects.db, adds it to app.state, and ensures it is initialized and closed during application lifespan.

tinyagentos/app.py

auth_middleware.pyAllowlist doc-review routes for agent registry JWT passthrough +21/-0

Allowlist doc-review routes for agent registry JWT passthrough

• Adds a regex-based allowlist for the doc-review endpoints, including a path-typed doc_path segment that can contain slashes. Hooks the allowlist into the middleware dispatch decision so only these doc-review paths can be reached by agent Bearer tokens.

tinyagentos/auth_middleware.py

doc_review_store.pyAdd SQLite-backed doc-review stamp store with transition validation +149/-0

Add SQLite-backed doc-review stamp store with transition validation

• Introduces a new doc_reviews table and indexes keyed by (project_id, doc_path) with optional reviewer/changes-requested actor stamps and timestamps. Implements get/set/list/delete APIs and enforces allowed state transitions, raising ValueError on invalid states/transitions.

tinyagentos/projects/doc_review_store.py

__init__.pyRegister project doc-review router +3/-0

Register project doc-review router

• Includes the new project_doc_review router in the main FastAPI router registration so endpoints are served under /api/projects/...

tinyagentos/routes/init.py

agent_registry.pyPermit granting project_doc_review scope when minting internal agents +1/-0

Permit granting project_doc_review scope when minting internal agents

• Extends the internal mint allowlist of grantable scopes to include project_doc_review, preventing the mint endpoint from becoming a backdoor for unknown scopes.

tinyagentos/routes/agent_registry.py

project_doc_review.pyAdd doc-review GET/PUT/list endpoints with dual-auth authorization +112/-0

Add doc-review GET/PUT/list endpoints with dual-auth authorization

• Adds endpoints to update a doc’s review state, read a single doc’s stamp, and list stamps (optionally filtered by state). Authorization supports owner/admin sessions or agent registry JWTs with a project_doc_review grant bound to the target project, collapsing project mismatches into existence-hiding 404s and mapping invalid transitions to HTTP 409.

tinyagentos/routes/project_doc_review.py

Tests (2) +364 / -0
test_agent_registry.pyAssert project_doc_review is mintable via agent registry allowlist +6/-0

Assert project_doc_review is mintable via agent registry allowlist

• Imports the mint allowlist and adds a regression test ensuring project_doc_review is included so internal agents can be granted the scope.

tests/test_agent_registry.py

test_doc_review.pyEnd-to-end tests for doc-review store and route security invariants +358/-0

End-to-end tests for doc-review store and route security invariants

• Adds store-level tests for state transitions, actor stamping, listing, and deletion behavior. Adds route-level HTTP tests covering owner access, agent project scoping (404 on mismatch), missing-scope 403, and unauthenticated 401 behavior, plus correct 400/409 mappings.

tests/test_doc_review.py

Documentation (1) +5 / -0
agent-coordination.mdDocument new project_doc_review scope and endpoints +5/-0

Document new project_doc_review scope and endpoints

• Adds the project_doc_review scope to the documented agent API surface, including the list/read/write endpoints and the security model (JWT + grant + project binding with a closed middleware allowlist).

docs/agent-coordination.md

@jaylfc

jaylfc commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

Reviewed + lead-completed. The backend half was a byte-perfect port (store/routes/tests identical to master) with correct extras (scope in _ALLOWED_SCOPES + test, docs line). Two notes: (1) the _csrf mount differs from master's bare mount - kept, it is the sibling-router idiom and all 21 doc-review tests pass through the real app with it; (2) the SPA half was missing (FilesApp UI + projects.docReviews client), which would have left the next promote's tree diff dirty - pushed it onto this branch myself (master-verbatim FilesApp hunks, client block matching the promote branch), SPA builds clean. Merging on green. After this + PR 2246 land, git diff dev master must be empty except uv.lock noise - asserting that at the next promote.

@jaylfc

jaylfc commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

nemotron-ultra-kilo review

VERDICT: The implementation is functionally correct with comprehensive tests, but has fragile error handling and a few style concerns.

  • tinyagentos/routes/project_doc_review.py:58-61 - Fragile string matching on exception messages ("invalid transition" in detail) to determine HTTP 409 vs 400. Should use custom exception types for reliable error mapping.

  • tinyagentos/projects/doc_review_store.py:62 - The description parameter in _row_to_review is actually a cursor description tuple list, not a description string; misleading variable name.

  • tinyagentos/projects/doc_review_store.py:68-72 - First-write validation logic allows approved/changes_requested as initial states but the condition is hard to read; consider extracting to a helper or adding a comment clarifying the intent.

  • tinyagentos/auth_middleware.py:92-93 - Regex (.+)$ for doc_path is greedy but anchored; works correctly but ([^/]+(?:/.+)?)? would be more precise for path segments.

  • tests/test_doc_review.py - Missing test cases: idempotent transition (awaiting_reviewawaiting_review should probably 409), concurrent updates to same doc, very long/special-character doc_path values, and DELETE route (store has delete_review but no route exposed).

  • tinyagentos/routes/project_doc_review.py:30-48 - _authorize_doc_review_actor returns tuple[str, bool, dict] | JSONResponse with a tuple of (actor_id, is_agent, project) — the bool position is confusing; consider a NamedTuple or dataclass for clarity.

  • docs/agent-coordination.md:199-203 - Documentation correctly describes the new scope and routes.
    VERDICT: The implementation is functionally correct with comprehensive tests, but has fragile error handling and a few style concerns.

  • tinyagentos/routes/project_doc_review.py:58-61 - Fragile string matching on exception messages ("invalid transition" in detail) to determine HTTP 409 vs 400. Should use custom exception types for reliable error mapping.

  • tinyagentos/projects/doc_review_store.py:62 - The description parameter in _row_to_review is actually a cursor description tuple list, not a description string; misleading variable name.

  • tinyagentos/projects/doc_review_store.py:68-72 - First-write validation logic allows approved/changes_requested as initial states but the condition is hard to read; consider extracting to a helper or adding a comment clarifying the intent.

  • tinyagentos/auth_middleware.py:92-93 - Regex (.+)$ for doc_path is greedy but anchored; works correctly but ([^/]+(?:/.+)?)? would be more precise for path segments.

  • tests/test_doc_review.py - Missing test cases: idempotent transition (awaiting_reviewawaiting_review should probably 409), concurrent updates to same doc, very long/special-character doc_path values, and DELETE route (store has delete_review but no route exposed).

  • tinyagentos/routes/project_doc_review.py:30-48 - _authorize_doc_review_actor returns tuple[str, bool, dict] | JSONResponse with a tuple of (actor_id, is_agent, project) — the bool position is confusing; consider a NamedTuple or dataclass for clarity.

  • docs/agent-coordination.md:199-203 - Documentation correctly describes the new scope and routes.

Automated first-pass review by the nemotron-ultra-kilo lane. The lead still reviews before merge.

@qodo-code-review

qodo-code-review Bot commented Aug 2, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (5)

Context used
✅ Compliance rules (platform): 35 rules

Grey Divider


Action required

1. Non-atomic state transitions 🐞 Bug ≡ Correctness
Description
DocReviewStore.set_review_state() validates transitions against a previously-read state but UPDATEs
rows without guarding on the prior review_state (and without a transaction), so concurrent writers
can persist a transition that would be invalid from the actually-committed state. The same TOCTOU
pattern can also raise an unhandled UNIQUE constraint error on concurrent first insert for the same
(project_id, doc_path), causing 500s.
Code

tinyagentos/projects/doc_review_store.py[R118-121]

+        await self._db.execute(
+            f"UPDATE doc_reviews SET {', '.join(sets)} WHERE project_id = ? AND doc_path = ?",
+            params,
+        )
Relevance

●●● Strong

Repo has accepted fixes that make SQLite writes more atomic/guarded against TOCTOU and concurrent
update issues.

PR-#260

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The UPDATE is not conditioned on the previously-validated state, and the initial insert path is
preceded by a separate SELECT despite a unique index on (project_id, doc_path), so concurrent calls
can both write based on stale reads or throw IntegrityError. Other stores in this codebase
explicitly use conditional UPDATEs and BEGIN IMMEDIATE + IntegrityError handling to avoid these
exact race windows.

tinyagentos/projects/doc_review_store.py[21-23]
tinyagentos/projects/doc_review_store.py[61-63]
tinyagentos/projects/doc_review_store.py[98-123]
tinyagentos/agent_tokens_store.py[78-104]
tinyagentos/projects/task_store.py[313-324]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DocReviewStore.set_review_state()` performs read/validate/write across multiple statements without atomicity:
- Two concurrent requests can both read the same `review_state`, both pass validation, and then both write; the last write wins even if it represents an invalid transition from the first write’s committed state.
- Two concurrent “first writes” for the same `(project_id, doc_path)` can both observe `existing is None` and attempt `INSERT`, causing an uncaught `aiosqlite.IntegrityError` due to the unique index.

## Issue Context
This store is reachable by both admin sessions and agent tokens (project_doc_review scope), so parallel writes are plausible.

## Fix Focus Areas
- tinyagentos/projects/doc_review_store.py[61-123]

### Implementation notes
Use one of these patterns (either is fine):
1) **Transactional + locking**: `BEGIN IMMEDIATE` around the read/validate/write sequence; on `IntegrityError` rollback and re-read.
2) **Optimistic concurrency**: `UPDATE ... WHERE project_id=? AND doc_path=? AND review_state=?` and check `cursor.rowcount==1`; if 0, re-fetch current row and raise a conflict/ValueError. For insert, either `INSERT ... ON CONFLICT(project_id, doc_path) DO NOTHING` + re-read, or catch `IntegrityError` and re-read.

Also ensure you `ROLLBACK` on exceptions inside the transaction to avoid leaving the connection mid-transaction.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. VALID_SCOPES missing project_doc_review ✓ Resolved 📜 Skill insight ≡ Correctness
Description
project_doc_review was added to _ALLOWED_SCOPES but is not present in VALID_SCOPES, breaking
the required synchronization and potentially causing scope validation inconsistencies. This violates
the scope synchronization rule.
Code

tinyagentos/routes/agent_registry.py[R95-99]

    "project_tasks",
    "project_tasks_create",
    "project_tasks_update",
+    "project_doc_review",
    "canvas_read", "canvas_write",
Relevance

●●● Strong

Scope-list synchronization is a straightforward correctness fix (add missing string) and typically
gets accepted.

PR-#2122

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2185251 requires _ALLOWED_SCOPES and VALID_SCOPES to match when adding a new
scope. The PR adds project_doc_review to _ALLOWED_SCOPES, but VALID_SCOPES does not include
it.

tinyagentos/routes/agent_registry.py[87-102]
tinyagentos/routes/agent_auth_requests.py[43-83]
Skill: taos-development-skill

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new scope `project_doc_review` was added to `_ALLOWED_SCOPES` but not added to `VALID_SCOPES`, violating the requirement that these sets stay synchronized.

## Issue Context
The checklist states a test asserts these sets are equal; leaving them out of sync can cause approvals/minting and consent flows to disagree on supported scopes.

## Fix Focus Areas
- tinyagentos/routes/agent_registry.py[90-102]
- tinyagentos/routes/agent_auth_requests.py[43-83]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. DocReviewStore reuses projects.db 📜 Skill insight ⌂ Architecture
Description
DocReviewStore is instantiated with data_dir / "projects.db", violating the requirement that
each store use its own SQLite file and avoid cross-store database sharing. This increases coupling
and risks schema collisions across stores.
Code

tinyagentos/app.py[R424-426]

    project_canvas_store = ProjectCanvasStoreImpl(data_dir / "projects.db", broker=project_event_broker)
+    doc_review_store = DocReviewStore(data_dir / "projects.db")
    from tinyagentos.decisions.decision_store import DecisionStore
Relevance

●● Moderate

DB-per-store rule is architectural; no close precedent found for rejecting/accepting DB file
separation changes.

PR-#260

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2185445 requires each store to use its own SQLite file. The PR constructs
DocReviewStore(data_dir / "projects.db"), sharing the same DB file as other project stores.

tinyagentos/app.py[404-426]
Skill: taos-development-skill

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DocReviewStore` is created using `projects.db`, but compliance requires each store to use its own SQLite file (no shared DB across stores).

## Issue Context
The rule explicitly forbids cross-store DB sharing; even without cross-table queries, sharing the same SQLite file couples schemas and migrations.

## Fix Focus Areas
- tinyagentos/app.py[398-426]
- tinyagentos/projects/doc_review_store.py[1-26]
- tests/test_doc_review.py[1-358]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Route cross-imports routes.projects 📜 Skill insight ⌂ Architecture
Description
tinyagentos/routes/project_doc_review.py imports _get_owned_project from another route module,
creating forbidden coupling and risk of circular imports. This violates the rule prohibiting
cross-importing between route modules.
Code

tinyagentos/routes/project_doc_review.py[R11-13]

+from tinyagentos.auth_context import CurrentUser
+from tinyagentos.routes.projects import _get_owned_project
+
Relevance

●● Moderate

Route cross-import prohibition is architectural; no closely matching historical accept/reject
precedent found.

PR-#260

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2185115 forbids importing one route module from another. The new route module
directly imports from tinyagentos.routes.projects.

tinyagentos/routes/project_doc_review.py[11-13]
Skill: taos-development-skill

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`tinyagentos/routes/project_doc_review.py` imports `_get_owned_project` from `tinyagentos/routes/projects.py`, which violates the no-cross-imports rule for route modules.

## Issue Context
Route modules should not depend on other route modules; shared logic should live in a non-route module (e.g. `tinyagentos/projects/...` or `tinyagentos/authz/...`) to avoid circular import risk and improve modularity.

## Fix Focus Areas
- tinyagentos/routes/project_doc_review.py[11-33]
- tinyagentos/routes/projects.py[1-120]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. Test file name doesn't mirror module 📜 Skill insight ⚙ Maintainability
Description
A new route module tinyagentos/routes/project_doc_review.py was added but the corresponding test
file is named tests/test_doc_review.py, not tests/test_project_doc_review.py. This violates the
required test naming convention that mirrors module structure.
Code

tests/test_doc_review.py[R1-4]

+"""Doc-review stamp store: state machine, actor recording, invalid-transition
+rejects, and project-scoped agent-token gating on writes.
+
+The store (tinyagentos/projects/doc_review_store.py) is exercised directly for
Relevance

●●● Strong

Test naming/style cleanups are commonly accepted; renaming tests for clarity/naming has precedent.

PR-#364

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2185311 requires test files to mirror the module structure. The PR adds
tinyagentos/routes/project_doc_review.py but introduces tests/test_doc_review.py which does not
mirror the route module name.

tests/test_doc_review.py[1-10]
tinyagentos/routes/project_doc_review.py[1-15]
Skill: taos-development-skill

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new route module name `project_doc_review.py` is not mirrored by the test filename; the PR adds `tests/test_doc_review.py` instead of `tests/test_project_doc_review.py`.

## Issue Context
The checklist requires tests to follow `tests/test_<module>.py` mirroring `routes/<module>.py` for discoverability and consistency.

## Fix Focus Areas
- tests/test_doc_review.py[1-358]
- tinyagentos/routes/project_doc_review.py[1-112]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Cursor metadata after context 🐞 Bug ☼ Reliability
Description
DocReviewStore.list_reviews() accesses cur.description after the async-with cursor context has
exited, which is an unsafe cursor-lifetime pattern and can break list responses depending on cursor
finalization behavior. Elsewhere in the repo, similar code snapshots the description inside the
context before exiting.
Code

tinyagentos/projects/doc_review_store.py[R138-140]

+            ) as cur:
+                rows = await cur.fetchall()
+        return [self._row_to_review(r, cur.description) for r in rows]
Relevance

●●● Strong

Cursor-lifetime safety bug is a small deterministic reliability fix; likely to be accepted when
flagged.

PR-#260

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The code returns a list built with cur.description after leaving the async-with block that owns
cur. A similar store pattern explicitly snapshots desc inside the context before returning,
indicating the expected safe approach in this codebase.

tinyagentos/projects/doc_review_store.py[128-140]
tinyagentos/decisions/decision_store.py[148-155]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`list_reviews()` uses `cur.description` outside the `async with self._db.execute(...) as cur:` block. After the context exits, the cursor is closed/finalized, so accessing cursor metadata afterward is not a safe contract.

## Issue Context
Several other stores in this repo capture `desc = cur.description` inside the context manager and then use `desc` after the block.

## Fix Focus Areas
- tinyagentos/projects/doc_review_store.py[125-140]

### Implementation notes
Inside each `async with` branch:
- assign `desc = cur.description` before leaving the block
Then return using `desc`:
- `return [self._row_to_review(r, desc) for r in rows]`

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

7. Doc-review routes return raw dicts 📜 Skill insight ✧ Quality
Description
The new doc-review route handlers return unmodeled dict payloads (and do not declare
response_model), reducing validation and schema stability. This violates the requirement to use
Pydantic models for request/response payloads.
Code

tinyagentos/routes/project_doc_review.py[R92-95]

+    review = await store.get_review(project_id, doc_path)
+    if review is None:
+        return {"project_id": project_id, "doc_path": doc_path, "review_state": None}
+    return review
Relevance

● Weak

Close precedent: request to add Pydantic response_model instead of raw dict responses was rejected.

PR-#2122

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2185155 requires using Pydantic models for route request and response payloads. The
handler returns raw dicts (e.g., when a review is missing and when listing reviews) without a
declared response model.

tinyagentos/routes/project_doc_review.py[92-95]
tinyagentos/routes/project_doc_review.py[111-112]
Skill: taos-development-skill

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Doc-review route handlers return raw `dict` payloads without `response_model` declarations.

## Issue Context
The compliance rule requires Pydantic models for both request bodies and response schemas to ensure validation and stable contracts.

## Fix Focus Areas
- tinyagentos/routes/project_doc_review.py[17-19]
- tinyagentos/routes/project_doc_review.py[51-77]
- tinyagentos/routes/project_doc_review.py[79-96]
- tinyagentos/routes/project_doc_review.py[98-112]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. DocReviewStore missing MIGRATIONS 📜 Skill insight ⌂ Architecture
Description
The new DocReviewStore defines SCHEMA but does not define a MIGRATIONS class attribute as
required by the store contract. This violates the BaseStore subclassing requirements for new stores.
Code

tinyagentos/projects/doc_review_store.py[R34-36]

+class DocReviewStore(BaseStore):
+    SCHEMA = DOC_REVIEW_SCHEMA
+
Relevance

● Weak

Close precedent: adding required MIGRATIONS attribute was explicitly rejected previously.

PR-#2122

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2185172 requires new store classes to subclass BaseStore and define both SCHEMA
and MIGRATIONS. DocReviewStore sets SCHEMA but has no MIGRATIONS attribute defined in the
class body.

tinyagentos/projects/doc_review_store.py[34-36]
Skill: taos-development-skill

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DocReviewStore` subclasses `BaseStore` and sets `SCHEMA`, but does not define `MIGRATIONS`.

## Issue Context
Other stores explicitly define `MIGRATIONS` (even as an empty list) to make migration intent clear and to comply with the store interface requirements.

## Fix Focus Areas
- tinyagentos/projects/doc_review_store.py[34-37]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines +11 to +13
from tinyagentos.auth_context import CurrentUser
from tinyagentos.routes.projects import _get_owned_project

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Route cross-imports routes.projects 📜 Skill insight ⌂ Architecture

tinyagentos/routes/project_doc_review.py imports _get_owned_project from another route module,
creating forbidden coupling and risk of circular imports. This violates the rule prohibiting
cross-importing between route modules.
Agent Prompt
## Issue description
`tinyagentos/routes/project_doc_review.py` imports `_get_owned_project` from `tinyagentos/routes/projects.py`, which violates the no-cross-imports rule for route modules.

## Issue Context
Route modules should not depend on other route modules; shared logic should live in a non-route module (e.g. `tinyagentos/projects/...` or `tinyagentos/authz/...`) to avoid circular import risk and improve modularity.

## Fix Focus Areas
- tinyagentos/routes/project_doc_review.py[11-33]
- tinyagentos/routes/projects.py[1-120]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread tinyagentos/routes/agent_registry.py
Comment thread tinyagentos/app.py
Comment on lines 424 to 426
project_canvas_store = ProjectCanvasStoreImpl(data_dir / "projects.db", broker=project_event_broker)
doc_review_store = DocReviewStore(data_dir / "projects.db")
from tinyagentos.decisions.decision_store import DecisionStore

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. docreviewstore reuses projects.db 📜 Skill insight ⌂ Architecture

DocReviewStore is instantiated with data_dir / "projects.db", violating the requirement that
each store use its own SQLite file and avoid cross-store database sharing. This increases coupling
and risks schema collisions across stores.
Agent Prompt
## Issue description
`DocReviewStore` is created using `projects.db`, but compliance requires each store to use its own SQLite file (no shared DB across stores).

## Issue Context
The rule explicitly forbids cross-store DB sharing; even without cross-table queries, sharing the same SQLite file couples schemas and migrations.

## Fix Focus Areas
- tinyagentos/app.py[398-426]
- tinyagentos/projects/doc_review_store.py[1-26]
- tests/test_doc_review.py[1-358]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread tests/test_doc_review.py
Comment on lines +1 to +4
"""Doc-review stamp store: state machine, actor recording, invalid-transition
rejects, and project-scoped agent-token gating on writes.

The store (tinyagentos/projects/doc_review_store.py) is exercised directly for

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

6. Test file name doesn't mirror module 📜 Skill insight ⚙ Maintainability

A new route module tinyagentos/routes/project_doc_review.py was added but the corresponding test
file is named tests/test_doc_review.py, not tests/test_project_doc_review.py. This violates the
required test naming convention that mirrors module structure.
Agent Prompt
## Issue description
The new route module name `project_doc_review.py` is not mirrored by the test filename; the PR adds `tests/test_doc_review.py` instead of `tests/test_project_doc_review.py`.

## Issue Context
The checklist requires tests to follow `tests/test_<module>.py` mirroring `routes/<module>.py` for discoverability and consistency.

## Fix Focus Areas
- tests/test_doc_review.py[1-358]
- tinyagentos/routes/project_doc_review.py[1-112]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +118 to +121
await self._db.execute(
f"UPDATE doc_reviews SET {', '.join(sets)} WHERE project_id = ? AND doc_path = ?",
params,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

7. Non-atomic state transitions 🐞 Bug ≡ Correctness

DocReviewStore.set_review_state() validates transitions against a previously-read state but UPDATEs
rows without guarding on the prior review_state (and without a transaction), so concurrent writers
can persist a transition that would be invalid from the actually-committed state. The same TOCTOU
pattern can also raise an unhandled UNIQUE constraint error on concurrent first insert for the same
(project_id, doc_path), causing 500s.
Agent Prompt
## Issue description
`DocReviewStore.set_review_state()` performs read/validate/write across multiple statements without atomicity:
- Two concurrent requests can both read the same `review_state`, both pass validation, and then both write; the last write wins even if it represents an invalid transition from the first write’s committed state.
- Two concurrent “first writes” for the same `(project_id, doc_path)` can both observe `existing is None` and attempt `INSERT`, causing an uncaught `aiosqlite.IntegrityError` due to the unique index.

## Issue Context
This store is reachable by both admin sessions and agent tokens (project_doc_review scope), so parallel writes are plausible.

## Fix Focus Areas
- tinyagentos/projects/doc_review_store.py[61-123]

### Implementation notes
Use one of these patterns (either is fine):
1) **Transactional + locking**: `BEGIN IMMEDIATE` around the read/validate/write sequence; on `IntegrityError` rollback and re-read.
2) **Optimistic concurrency**: `UPDATE ... WHERE project_id=? AND doc_path=? AND review_state=?` and check `cursor.rowcount==1`; if 0, re-fetch current row and raise a conflict/ValueError. For insert, either `INSERT ... ON CONFLICT(project_id, doc_path) DO NOTHING` + re-read, or catch `IntegrityError` and re-read.

Also ensure you `ROLLBACK` on exceptions inside the transaction to avoid leaving the connection mid-transaction.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +138 to +140
) as cur:
rows = await cur.fetchall()
return [self._row_to_review(r, cur.description) for r in rows]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

8. Cursor metadata after context 🐞 Bug ☼ Reliability

DocReviewStore.list_reviews() accesses cur.description after the async-with cursor context has
exited, which is an unsafe cursor-lifetime pattern and can break list responses depending on cursor
finalization behavior. Elsewhere in the repo, similar code snapshots the description inside the
context before exiting.
Agent Prompt
## Issue description
`list_reviews()` uses `cur.description` outside the `async with self._db.execute(...) as cur:` block. After the context exits, the cursor is closed/finalized, so accessing cursor metadata afterward is not a safe contract.

## Issue Context
Several other stores in this repo capture `desc = cur.description` inside the context manager and then use `desc` after the block.

## Fix Focus Areas
- tinyagentos/projects/doc_review_store.py[125-140]

### Implementation notes
Inside each `async with` branch:
- assign `desc = cur.description` before leaving the block
Then return using `desc`:
- `return [self._row_to_review(r, desc) for r in rows]`

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tinyagentos/routes/project_doc_review.py (1)

70-74: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Preserve exception context on the re-raised HTTPExceptions.

Both raise HTTPException(...) statements inside except ValueError as exc: drop the original traceback context. Add from exc so the original store-level error stays visible in logs and tracebacks.

♻️ Proposed fix
     except ValueError as exc:
         detail = str(exc)
         if "invalid transition" in detail:
-            raise HTTPException(status_code=409, detail=detail)
-        raise HTTPException(status_code=400, detail=detail)
+            raise HTTPException(status_code=409, detail=detail) from exc
+        raise HTTPException(status_code=400, detail=detail) from exc
🤖 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 `@tinyagentos/routes/project_doc_review.py` around lines 70 - 74, Update both
HTTPException raises in the ValueError handler to explicitly chain the original
exception with from exc, preserving the store-level traceback while retaining
the existing status codes and detail values.

Source: Linters/SAST tools

🤖 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 `@tinyagentos/projects/doc_review_store.py`:
- Around line 51-123: Guard the read-then-write sequence in set_review_state
with an instance asyncio.Lock, initialized as self._write_lock like the existing
AgentGrantsStore.init and UserSharesStore.init patterns. Acquire the lock across
get_review, transition validation, INSERT/UPDATE, commit, and the final read so
concurrent first writes for the same project/document are serialized; preserve
the existing transition behavior and return value.

---

Nitpick comments:
In `@tinyagentos/routes/project_doc_review.py`:
- Around line 70-74: Update both HTTPException raises in the ValueError handler
to explicitly chain the original exception with from exc, preserving the
store-level traceback while retaining the existing status codes and detail
values.
🪄 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: CHILL

Plan: Pro Plus

Run ID: 125eac26-6af9-4dc7-b970-a671448c34b7

📥 Commits

Reviewing files that changed from the base of the PR and between 1193b6b and eacf8d6.

📒 Files selected for processing (9)
  • docs/agent-coordination.md
  • tests/test_agent_registry.py
  • tests/test_doc_review.py
  • tinyagentos/app.py
  • tinyagentos/auth_middleware.py
  • tinyagentos/projects/doc_review_store.py
  • tinyagentos/routes/__init__.py
  • tinyagentos/routes/agent_registry.py
  • tinyagentos/routes/project_doc_review.py

Comment on lines +51 to +123
async def set_review_state(
self,
project_id: str,
doc_path: str,
new_state: str,
actor_id: str,
) -> dict:
if new_state not in VALID_TRANSITIONS:
raise ValueError(f"invalid review state: {new_state}")

now = time.time()
existing = await self.get_review(project_id, doc_path)

if existing is None:
if new_state != "awaiting_review" and new_state not in VALID_TRANSITIONS.get("awaiting_review", []):
raise ValueError(
f"invalid transition: (new) -> {new_state}; "
f"first state must be awaiting_review or a direct transition target"
)
review_id = new_id("rev")
reviewed_by = None
reviewed_at = None
changes_requested_by = None
changes_requested_at = None
if new_state == "approved":
reviewed_by = actor_id
reviewed_at = now
elif new_state == "changes_requested":
changes_requested_by = actor_id
changes_requested_at = now
await self._db.execute(
"""INSERT INTO doc_reviews
(id, project_id, doc_path, review_state,
reviewed_by, reviewed_at,
changes_requested_by, changes_requested_at,
created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(
review_id, project_id, doc_path, new_state,
reviewed_by, reviewed_at,
changes_requested_by, changes_requested_at,
now, now,
),
)
await self._db.commit()
return await self.get_review(project_id, doc_path)

current_state = existing["review_state"]
allowed = VALID_TRANSITIONS.get(current_state, [])
if new_state not in allowed:
raise ValueError(
f"invalid transition: {current_state} -> {new_state}"
)

sets: list[str] = ["review_state = ?", "updated_at = ?"]
params: list = [new_state, now]

if new_state == "approved":
sets.append("reviewed_by = ?")
sets.append("reviewed_at = ?")
params.extend([actor_id, now])
elif new_state == "changes_requested":
sets.append("changes_requested_by = ?")
sets.append("changes_requested_at = ?")
params.extend([actor_id, now])

params.extend([project_id, doc_path])
await self._db.execute(
f"UPDATE doc_reviews SET {', '.join(sets)} WHERE project_id = ? AND doc_path = ?",
params,
)
await self._db.commit()
return await self.get_review(project_id, doc_path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard set_review_state against a concurrent first-write race.

set_review_state reads existing and then inserts or updates without holding a lock. Two concurrent calls for the same new (project_id, doc_path) can both see existing is None at line 64. Both then attempt the INSERT at line 81. The idx_doc_reviews_project_path unique index (lines 21-22) rejects the second insert with aiosqlite.IntegrityError. The route only catches ValueError (tinyagentos/routes/project_doc_review.py lines 66-74), so this exception propagates as an unhandled 500 instead of a clean 409 or a correct state transition.

Other stores in this codebase (for example AgentGrantsStore.init, UserSharesStore.init) create a self._write_lock = asyncio.Lock() and hold it across read-then-write sequences for this exact reason. Add the same pattern here.

🔒 Proposed fix: serialize read-then-write with a lock
+import asyncio
 import time

 from tinyagentos.base_store import BaseStore
 from tinyagentos.projects.ids import new_id
@@
 class DocReviewStore(BaseStore):
     SCHEMA = DOC_REVIEW_SCHEMA

+    async def init(self) -> None:
+        await super().init()
+        self._write_lock = asyncio.Lock()
+
     def _row_to_review(self, row, description) -> dict:
         keys = [d[0] for d in description]
         return dict(zip(keys, row))
@@
     async def set_review_state(
         self,
         project_id: str,
         doc_path: str,
         new_state: str,
         actor_id: str,
     ) -> dict:
         if new_state not in VALID_TRANSITIONS:
             raise ValueError(f"invalid review state: {new_state}")

-        now = time.time()
-        existing = await self.get_review(project_id, doc_path)
+        async with self._write_lock:
+            now = time.time()
+            existing = await self.get_review(project_id, doc_path)
+            return await self._apply_state_change(
+                project_id, doc_path, new_state, actor_id, now, existing
+            )
🧰 Tools
🪛 OpenGrep (1.26.0)

[ERROR] 118-121: SQL query built via f-string passed to execute()/executemany(). Use parameterized queries with placeholders instead.

(coderabbit.sql-injection.python-fstring-execute)

🪛 Ruff (0.16.0)

[error] 119-119: Possible SQL injection vector through string-based query construction

(S608)

🤖 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 `@tinyagentos/projects/doc_review_store.py` around lines 51 - 123, Guard the
read-then-write sequence in set_review_state with an instance asyncio.Lock,
initialized as self._write_lock like the existing AgentGrantsStore.init and
UserSharesStore.init patterns. Acquire the lock across get_review, transition
validation, INSERT/UPDATE, commit, and the final read so concurrent first writes
for the same project/document are serialized; preserve the existing transition
behavior and return value.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
desktop/src/apps/FilesApp.tsx (1)

824-834: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restore trash-specific error state for empty-trash failures.

handleEmptyWorkspaceTrash's catch block now calls setError(msg) instead of setWorkspaceTrashError(msg). Every other trash action (handleWorkspaceTrashRestore, handleWorkspaceTrashPurge) still uses setWorkspaceTrashError.

This action is only triggered from the Recycle Bin view (recycleBinUI, rendered when location === "recycle", Line 1918). recycleBinUI's error banner reads recycleError || workspaceTrashError (Lines 1347-1350); it does not read the general error state, which is only rendered in mainContentUI (Lines 1596-1602). Since mainContentUI is not shown while location === "recycle", a failed "Empty Trash" action now sets an error that is never displayed to the user.

🔧 Proposed fix: use the trash-specific error state
     } catch (e: unknown) {
-      const msg = e instanceof Error ? e.message : "Empty trash failed";
-      setError(msg);
+      const msg = e instanceof Error ? e.message : "Empty trash failed";
+      setWorkspaceTrashError(msg);
     }
   }, [workspaceTrashItems.length, fetchWorkspaceTrash]);
🤖 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 `@desktop/src/apps/FilesApp.tsx` around lines 824 - 834, Update the catch block
in handleEmptyWorkspaceTrash to call setWorkspaceTrashError(msg) instead of
setError(msg), ensuring empty-trash failures appear in the recycleBinUI error
banner alongside other trash actions.
🧹 Nitpick comments (1)
desktop/src/lib/projects.ts (1)

359-374: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Apply DocReviewState consistently instead of string. The exported DocReviewState union ("awaiting_review" | "approved" | "changes_requested") is defined in projects.ts and already used in FilesApp.tsx's cycleDocReview, but the client API and UI props widen it back to string, losing compile-time protection against invalid state values.

  • desktop/src/lib/projects.ts#L359-L374: type list's state?: string and set's state: string parameters as DocReviewState.
  • desktop/src/apps/FilesApp.tsx#L378-L380: type FileRowProps.reviewState (and ReviewBadge's state prop) as DocReviewState | null instead of string | null.
🤖 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 `@desktop/src/lib/projects.ts` around lines 359 - 374, Replace the widened
string state types with the existing DocReviewState union: update list’s
optional state and set’s state in desktop/src/lib/projects.ts lines 359-374, and
update FileRowProps.reviewState and ReviewBadge.state in
desktop/src/apps/FilesApp.tsx lines 378-380 to DocReviewState | null. Preserve
the existing API behavior while enforcing valid review states at compile time.
🤖 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 `@desktop/src/apps/FilesApp.tsx`:
- Around line 302-345: Update ReviewBadge’s onClick rendering to use a span with
role="button" and tabIndex={0} instead of a nested button, preserving click
handling and adding keyboard activation for Enter and Space. Keep the
non-interactive span path unchanged, and apply the same pattern to the
corresponding grid file-card usage.

---

Outside diff comments:
In `@desktop/src/apps/FilesApp.tsx`:
- Around line 824-834: Update the catch block in handleEmptyWorkspaceTrash to
call setWorkspaceTrashError(msg) instead of setError(msg), ensuring empty-trash
failures appear in the recycleBinUI error banner alongside other trash actions.

---

Nitpick comments:
In `@desktop/src/lib/projects.ts`:
- Around line 359-374: Replace the widened string state types with the existing
DocReviewState union: update list’s optional state and set’s state in
desktop/src/lib/projects.ts lines 359-374, and update FileRowProps.reviewState
and ReviewBadge.state in desktop/src/apps/FilesApp.tsx lines 378-380 to
DocReviewState | null. Preserve the existing API behavior while enforcing valid
review states at compile time.
🪄 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: CHILL

Plan: Pro Plus

Run ID: 33b64c5b-02ae-413e-bed4-d59a5abcc42f

📥 Commits

Reviewing files that changed from the base of the PR and between eacf8d6 and c2e1aaf.

📒 Files selected for processing (2)
  • desktop/src/apps/FilesApp.tsx
  • desktop/src/lib/projects.ts

Comment on lines +302 to +345
const REVIEW_BADGE_STYLES: Record<string, string> = {
approved: "bg-emerald-500/15 text-emerald-400 border-emerald-500/20",
changes_requested: "bg-amber-500/15 text-amber-400 border-amber-500/20",
awaiting_review: "bg-zinc-500/10 text-zinc-400 border-zinc-500/15",
};

const REVIEW_BADGE_LABEL: Record<string, string> = {
approved: "Approved",
changes_requested: "Changes requested",
awaiting_review: "Awaiting review",
};

function ReviewBadge({
state,
onClick,
}: {
state: string | null;
onClick?: () => void;
}) {
if (!state) return null;
const style = REVIEW_BADGE_STYLES[state] ?? REVIEW_BADGE_STYLES.awaiting_review;
const label = REVIEW_BADGE_LABEL[state] ?? state;
if (onClick) {
return (
<button
type="button"
onClick={onClick}
title={`Review state: ${label} (click to change)`}
className={`shrink-0 text-[10px] px-1.5 py-0.5 rounded-full border ${style} hover:brightness-125 transition`}
>
{label}
</button>
);
}
return (
<span
className={`shrink-0 text-[10px] px-1.5 py-0.5 rounded-full border ${style}`}
title={`Review state: ${label}`}
>
{label}
</span>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
git ls-files | rg 'desktop/src/apps/FilesApp\.tsx$' || true

echo "== outline relevant symbols =="
ast-grep outline desktop/src/apps/FilesApp.tsx --view compact | sed -n '1,220p' || true

echo "== relevant sections =="
sed -n '280,360p' desktop/src/apps/FilesApp.tsx
echo "===== 1640-1775 ====="
sed -n '1640,1775p' desktop/src/apps/FilesApp.tsx

echo "== search ReviewBadge usages in FilesApp =="
rg -n "ReviewBadge|review.*badge|Review badge|review" desktop/src/apps/FilesApp.tsx

Repository: jaylfc/taOS

Length of output: 10279


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== DocReviewBadge section =="
sed -n '430,475p' desktop/src/apps/FilesApp.tsx

echo "== list view containing interactive elements near ReviewBadge =="
sed -n '1775,1835p' desktop/src/apps/FilesApp.tsx

echo "== parse JSX nesting for ReviewBadge onClick usages =="
python3 - <<'PY'
from pathlib import Path
import re

p = Path("desktop/src/apps/FilesApp.tsx")
s = p.read_text()
for var, body in re.findall(r"function\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(\{\s*(.*?)\s*\}\)\s*\{", s, re.S):
    if var in {"DocReviewBadge", "FilesApp"}:
        print(f"--- {var} ---")
        start = s.index(f"function {var}")
        brace = s.index("{", s.index("(", start))
        depth = 0
        for i in range(brace, len(s)):
            if s[i] == "{":
                depth += 1
            elif s[i] == "}":
                depth -= 1
                if depth == 0:
                    print(s[start : i+1])
                    break
PY

Repository: jaylfc/taOS

Length of output: 4590


Fix nested <button> inside the grid file-card button.

In grid view, the clickable ReviewBadge with onClick is rendered inside the file-open <button>, which makes the card button become invalid HTML. Use the same non-button interactive pattern as the delete overlay: a <span role="button" tabIndex={0} ...> with keyboard activation.

🔧 Proposed fix
   if (onClick) {
     return (
-      <button
-        type="button"
+      <span
+        role="button"
+        tabIndex={0}
         onClick={onClick}
+        onKeyDown={(e) => {
+          if (e.key === "Enter" || e.key === " ") {
+            e.preventDefault();
+            onClick();
+          }
+        }}
         title={`Review state: ${label} (click to change)`}
         className={`shrink-0 text-[10px] px-1.5 py-0.5 rounded-full border ${style} hover:brightness-125 transition`}
       >
         {label}
-      </button>
+      </span>
     );
   }

Also applies to: 1712-1725

🤖 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 `@desktop/src/apps/FilesApp.tsx` around lines 302 - 345, Update ReviewBadge’s
onClick rendering to use a span with role="button" and tabIndex={0} instead of a
nested button, preserving click handling and adding keyboard activation for
Enter and Space. Keep the non-interactive span path unchanged, and apply the
same pattern to the corresponding grid file-card usage.

jaylfc added 3 commits August 2, 2026 22:18
Completes the reconciliation: the backend port alone would have left the
SPA half master-only and the next promote's tree diff dirty. FilesApp hunks
are master's verbatim; the client block and types match the beta.45 promote
branch (master's redundant docReview singular block deliberately omitted).
SPA builds clean.
…lary + changelog line

The scope-parity test rightly caught _ALLOWED_SCOPES diverging from
VALID_SCOPES; the doc-gate rightly demanded the changelog entry.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
tinyagentos/routes/project_doc_review.py (2)

17-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Restrict state to the known review states at the schema level.

DocReviewUpdate.state accepts any str. Restricting it to a Literal of the valid states documents the accepted values in the OpenAPI schema and lets FastAPI reject typos with a 422 before the request reaches the store.

♻️ Proposed refactor
+from typing import Literal
+
 class DocReviewUpdate(BaseModel):
-    state: str = Field(..., description="Target review state")
+    state: Literal["awaiting_review", "approved", "changes_requested"] = Field(
+        ..., description="Target review state"
+    )
🤖 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 `@tinyagentos/routes/project_doc_review.py` around lines 17 - 18, Update the
DocReviewUpdate.state field to use a Literal containing the known valid
review-state values instead of unrestricted str, and add or reuse the necessary
typing import. Preserve the existing field description so FastAPI and OpenAPI
expose the accepted states and reject invalid values during request validation.

21-48: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the bare tuple return with a typed structure.

_authorize_doc_review_actor returns tuple[str, bool, dict] | JSONResponse. At both call sites (actor_id, _is_agent, _project = auth), the reader must go back to this function to learn what each position means. A small dataclass (or NamedTuple) documents the fields once and removes the need to infer them from destructuring order.

♻️ Proposed refactor
+from dataclasses import dataclass
+
+
+@dataclass
+class AuthorizedDocReviewActor:
+    actor_id: str
+    is_agent: bool
+    project: dict
+
+
 async def _authorize_doc_review_actor(
     request: Request, pstore, project_id: str
-) -> "tuple[str, bool, dict] | JSONResponse":
+) -> "AuthorizedDocReviewActor | JSONResponse":
     uid = getattr(request.state, "user_id", None)
     if uid:
         user = CurrentUser(
             user_id=uid,
             is_admin=bool(getattr(request.state, "is_admin", False)),
         )
         project_or_err = await _get_owned_project(pstore, project_id, user)
         if isinstance(project_or_err, JSONResponse):
             return project_or_err
-        return (user.user_id, False, project_or_err)
+        return AuthorizedDocReviewActor(user.user_id, False, project_or_err)
🤖 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 `@tinyagentos/routes/project_doc_review.py` around lines 21 - 48, Replace the
positional tuple returned by _authorize_doc_review_actor with a small typed
structure, such as a dataclass or NamedTuple, exposing named fields for actor
ID, agent status, and project. Update both call sites that currently destructure
auth into actor_id, _is_agent, and _project to access the corresponding named
fields while preserving the existing JSONResponse error returns and values.
tests/test_doc_review.py (1)

287-358: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add HTTP-level coverage for the list route's project-boundary and filter behavior.

TestAgentScopeGating covers the PUT and single-doc GET routes thoroughly, but no test exercises GET /api/projects/{project_id}/doc-reviews with an agent token bound to a different project, and no test exercises the state query filter through the HTTP route (only test_list_reviews_filters_by_state covers this at the store level, lines 144-156). Add a test mirroring test_agent_other_project_is_404 for the list endpoint, and one owner-session test that filters /doc-reviews?state=approved over HTTP.

🤖 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 `@tests/test_doc_review.py` around lines 287 - 358, Add HTTP-level tests to
TestAgentScopeGating for GET /api/projects/{project_id}/doc-reviews: verify an
agent token bound to another project receives the same existence-hiding 404 as
test_agent_other_project_is_404, and verify an owner session requesting
?state=approved returns only approved reviews. Use the existing project,
authentication, and response assertion helpers while preserving current route
behavior.
tinyagentos/projects/doc_review_store.py (1)

64-69: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Simplify the first-write validation; it is currently unreachable.

Trace the two guards together. Line 58-59 already rejects any new_state not in VALID_TRANSITIONS (its three keys: awaiting_review, changes_requested, approved). By the time execution reaches line 65, new_state can only be one of those three values. The condition new_state != "awaiting_review" and new_state not in VALID_TRANSITIONS.get("awaiting_review", []) then requires new_state to be neither "awaiting_review" nor in ["approved", "changes_requested"] — but those three values are exhaustive, so the branch can never raise today.

This is not a functional bug (tests confirm every first-write case currently succeeds), but the intent — guarding against a future terminal state that should not be a valid initial state — is hard to read from this expression. Rewrite it to state the intent directly.

♻️ Proposed clarification
         if existing is None:
-            if new_state != "awaiting_review" and new_state not in VALID_TRANSITIONS.get("awaiting_review", []):
-                raise ValueError(
-                    f"invalid transition: (new) -> {new_state}; "
-                    f"first state must be awaiting_review or a direct transition target"
-                )
+            allowed_initial_states = {"awaiting_review", *VALID_TRANSITIONS["awaiting_review"]}
+            if new_state not in allowed_initial_states:
+                raise ValueError(
+                    f"invalid transition: (new) -> {new_state}; "
+                    f"first state must be one of {sorted(allowed_initial_states)}"
+                )
🤖 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 `@tinyagentos/projects/doc_review_store.py` around lines 64 - 69, Rewrite the
first-write validation in the existing-state check around existing and new_state
so it directly rejects any initial state other than awaiting_review, rather than
relying on VALID_TRANSITIONS membership. Preserve the existing ValueError
behavior and message intent, while leaving the earlier transition validation
unchanged.
🤖 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 `@tinyagentos/routes/agent_auth_requests.py`:
- Around line 81-84: The project_doc_review scope is accepted but not treated as
project-bound during approval. Update _PROJECT_SCOPES to include
project_doc_review, and update approve_request_record to use the project-bound
approval-path values when issuing the granted token so project_doc_review
approvals require and retain project_id.

In `@tinyagentos/routes/project_doc_review.py`:
- Around line 66-74: Update the review-state handling around set_review_state to
catch a distinct InvalidTransitionError and return HTTP 409, while catching
remaining ValueError instances as HTTP 400 without inspecting exception message
text. Define and raise InvalidTransitionError in the store’s invalid-transition
path, and add from exc to both HTTPException raises to preserve exception
chaining.

---

Nitpick comments:
In `@tests/test_doc_review.py`:
- Around line 287-358: Add HTTP-level tests to TestAgentScopeGating for GET
/api/projects/{project_id}/doc-reviews: verify an agent token bound to another
project receives the same existence-hiding 404 as
test_agent_other_project_is_404, and verify an owner session requesting
?state=approved returns only approved reviews. Use the existing project,
authentication, and response assertion helpers while preserving current route
behavior.

In `@tinyagentos/projects/doc_review_store.py`:
- Around line 64-69: Rewrite the first-write validation in the existing-state
check around existing and new_state so it directly rejects any initial state
other than awaiting_review, rather than relying on VALID_TRANSITIONS membership.
Preserve the existing ValueError behavior and message intent, while leaving the
earlier transition validation unchanged.

In `@tinyagentos/routes/project_doc_review.py`:
- Around line 17-18: Update the DocReviewUpdate.state field to use a Literal
containing the known valid review-state values instead of unrestricted str, and
add or reuse the necessary typing import. Preserve the existing field
description so FastAPI and OpenAPI expose the accepted states and reject invalid
values during request validation.
- Around line 21-48: Replace the positional tuple returned by
_authorize_doc_review_actor with a small typed structure, such as a dataclass or
NamedTuple, exposing named fields for actor ID, agent status, and project.
Update both call sites that currently destructure auth into actor_id, _is_agent,
and _project to access the corresponding named fields while preserving the
existing JSONResponse error returns and values.
🪄 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: CHILL

Plan: Pro Plus

Run ID: b25f8369-9205-4b43-a09b-1a0dbbbaac43

📥 Commits

Reviewing files that changed from the base of the PR and between c2e1aaf and cd01972.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • desktop/src/apps/FilesApp.tsx
  • desktop/src/lib/projects.ts
  • docs/agent-coordination.md
  • tests/test_agent_registry.py
  • tests/test_doc_review.py
  • tinyagentos/app.py
  • tinyagentos/auth_middleware.py
  • tinyagentos/projects/doc_review_store.py
  • tinyagentos/routes/__init__.py
  • tinyagentos/routes/agent_auth_requests.py
  • tinyagentos/routes/agent_registry.py
  • tinyagentos/routes/project_doc_review.py
🚧 Files skipped from review as they are similar to previous changes (7)
  • tinyagentos/routes/agent_registry.py
  • tinyagentos/routes/init.py
  • tinyagentos/app.py
  • tests/test_agent_registry.py
  • docs/agent-coordination.md
  • desktop/src/lib/projects.ts
  • desktop/src/apps/FilesApp.tsx

Comment on lines +81 to +84
# Doc-review stamps: read/set review state on a project's docs
# (project-bound like project_tasks). Reconciled from master at beta.45 -
# the routes shipped on every install while the scope was missing here.
"project_doc_review",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 6 \
  '_PROJECT_SCOPES|VALID_SCOPES|project_doc_review|effective_project|project_id' \
  tinyagentos/routes/agent_auth_requests.py \
  tinyagentos/auth_middleware.py \
  tests

Repository: jaylfc/taOS

Length of output: 50368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== agent_auth_requests.py: relevant definitions =="
sed -n '1,130p' tinyagentos/routes/agent_auth_requests.py
echo
echo "== agent_auth_requests.py: approval path =="
sed -n '350,455p' tinyagentos/routes/agent_auth_requests.py
echo
echo "== direct matches in targeted files =="
rg -n '_PROJECT_SCOPES|VALID_SCOPES|project_doc_review|effective_project|project_id' tinyagentos/routes/agent_auth_requests.py tinyagentos/auth_middleware.py
echo
echo "== doc_review tests matches =="
rg -n 'project_doc_review|_grant_.*doc|create.*grant|create.*token|approve_request' tests/test_doc_review.py || true
echo
echo "== any doc_review scopes/tests =="
rg -n 'doc_review|project_doc_review' tests tinyagentos/routes tinyagentos/auth_middleware.py

Repository: jaylfc/taOS

Length of output: 23654


Require project-doc-review on the approval path when granting project_doc_review.

VALID_SCOPES accepts project_doc_review, but _PROJECT_SCOPES at tinyagentos/routes/agent_auth_requests.py:207 only includes task, canvas, and file scopes. Since approve_request_record only enforces project_id for project-scoped grants through _PROJECT_SCOPES, approve project_doc_review without a project and the route gets a global grant instead of a project-bound token. Add project_doc_review to the project-bind list and use bound approval-path values for the granted token.

🤖 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 `@tinyagentos/routes/agent_auth_requests.py` around lines 81 - 84, The
project_doc_review scope is accepted but not treated as project-bound during
approval. Update _PROJECT_SCOPES to include project_doc_review, and update
approve_request_record to use the project-bound approval-path values when
issuing the granted token so project_doc_review approvals require and retain
project_id.

Comment on lines +66 to +74
try:
review = await store.set_review_state(
project_id, doc_path, payload.state, actor_id
)
except ValueError as exc:
detail = str(exc)
if "invalid transition" in detail:
raise HTTPException(status_code=409, detail=detail)
raise HTTPException(status_code=400, detail=detail)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate files =="
fd -a 'project_doc_review.py|doc_review_store.py|test_doc_review.py|*doc_review*' . | sed 's#^\./##'

echo "== git status/stat =="
git diff --stat || true

echo "== route relevant lines =="
if [ -f tinyagentos/routes/project_doc_review.py ]; then
  sed -n '1,120p' tinyagentos/routes/project_doc_review.py | cat -n
fi

echo "== store relevant lines =="
if [ -f tinyagentos/projects/doc_review_store.py ]; then
  wc -l tinyagentos/projects/doc_review_store.py
  rg -n "invalid (transition|review state)|set_review_state|class .*Error|def set_review_state" tinyagentos/projects/doc_review_store.py
  sed -n '1,220p' tinyagentos/projects/doc_review_store.py | cat -n
fi

echo "== tests relevant lines =="
if [ -f tests/test_doc_review.py ]; then
  wc -l tests/test_doc_review.py
  rg -n "invalid (transition|review state)|status_code|409|400|review" tests/test_doc_review.py
  sed -n '90,135p' tests/test_doc_review.py | cat -n
fi

echo "== search usages for set_review_state and InvalidTransitionError =="
rg -n "set_review_state|InvalidTransitionError|invalid transition|invalid review state" .

Repository: jaylfc/taOS

Length of output: 582


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import ast

route = Path("tinyagentos/routes/project_doc_review.py")
store = Path("tinyagentos/projects/doc_review_store.py")
test = Path("tests/test_doc_review.py")

print("== AST exception handling at route ==")
if route.exists():
    tree = ast.parse(route.read_text())
    for node in ast.walk(tree):
        if isinstance(node, ast.ExceptHandler):
            exc_name = ast.unparse(node.type) if node.type else None
            catch = [
                (n.lineno, ast.unparse(n) or getattr(n, "id", None))
                for n in [node.exc, *node.body]
                if isinstance(n, ast.AST)
            ]
            print("try line", node.lineno, "handler:", exc_name)
            for li in node.body:
                print("  line", li.lineno, ast.unparse(li)[:220])
            print("  contains substring check", any(isinstance(n, ast.Compare) and any(isinstance(op, ast.In) for op in n.ops) for n in ast.walk(node)))

print("== store custom exception/class occurrences ==")
if store.exists():
    tree = ast.parse(store.read_text())
    for cls in ast.walk(tree):
        if isinstance(cls, ast.ClassDef) and any(isinstance(b, ast.ExceptionHandler) for b in cls.bases):
            print(cls.lineno, "class", cls.name, "bases", ast.unparse(cls))
    print("top-level except handlers count (data only):", sum(isinstance(n, ast.ExceptHandler) for n in ast.walk(tree)))

print("== tests status assertions ==")
if test.exists():
    text = test.read_text()
    for needle in ["invalid transition", "invalid review state", "status_code=409", "status_code=400"]:
        lines = [str(i+1) for i,line in enumerate(text.splitlines()) if needle in line]
        print(needle, lines)

print("== source message invariant ==")
for name, path in [("route", route), ("store", store), ("test", test)]:
    if path.exists():
        text = path.read_text()
        for needle in ["invalid transition", "invalid review state"]:
            print(name, needle, text.count(needle))
PY

Repository: jaylfc/taOS

Length of output: 323


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate files =="
fd -a 'project_doc_review.py|doc_review_store.py|test_doc_review.py|doc_review' . | sed 's#^\./##'

echo "== route relevant lines =="
if [ -f tinyagentos/routes/project_doc_review.py ]; then
  sed -n '1,130p' tinyagentos/routes/project_doc_review.py | cat -n
fi

echo "== store relevant lines =="
if [ -f tinyagentos/projects/doc_review_store.py ]; then
  wc -l tinyagentos/projects/doc_review_store.py
  rg -n "invalid (transition|review state)|set_review_state|class .*Error|def set_review_state" tinyagentos/projects/doc_review_store.py || true
  sed -n '1,240p' tinyagentos/projects/doc_review_store.py | cat -n
fi

echo "== tests relevant lines =="
if [ -f tests/test_doc_review.py ]; then
  wc -l tests/test_doc_review.py
  rg -n "invalid (transition|review state)|status_code|409|400|review" tests/test_doc_review.py || true
  sed -n '90,135p' tests/test_doc_review.py | cat -n
fi

echo "== search usages =="
rg -n "set_review_state|InvalidTransitionError|invalid transition|invalid review state|from exc" . || true

Repository: jaylfc/taOS

Length of output: 28294


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import ast

route = Path("tinyagentos/routes/project_doc_review.py")
store = Path("tinyagentos/projects/doc_review_store.py")
test = Path("tests/test_doc_review.py")

print("== AST exception handling at route ==")
if route.exists():
    tree = ast.parse(route.read_text())
    for node in ast.walk(tree):
        if not isinstance(node, ast.ExceptHandler):
            continue
        exc_name = ast.unparse(node.type) if node.type else None
        print("try line", node.lineno, "handler:", exc_name)
        for child in node.body:
            print("  line", child.lineno, ast.unparse(child)[:220])
        checks = []
        for comp in ast.walk(node):
            if not isinstance(comp, ast.Compare):
                continue
            for cmp in comp.comparators:
                value = ast.unparse(cmp)
                if value == '"invalid transition"':
                    checks.append(comp.lineno)
        print("  contains substring check", checks)

print("== store classes and custom exceptions ==")
if store.exists():
    tree = ast.parse(store.read_text())
    for cls in ast.walk(tree):
        if isinstance(cls, ast.ClassDef):
            bases = ast.unparse(cls) if cls.bases else None
            if bases and "Exception" in bases:
                print(cls.lineno, "class", cls.name, "bases", bases)
    for node in ast.walk(tree):
        if isinstance(node, ast.ClassDef):
            for base in node.bases:
                if isinstance(base, ast.Name) and base.id == "ValueError":
                    print("ValueError subclass subclassed at", node.lineno, node.name)
                break

print("== tests status/message assertions ==")
if test.exists():
    text = test.read_text()
    for needle in ["invalid transition", "invalid review state", "status_code=409", "status_code=400"]:
        lines = [str(i+1) for i,line in enumerate(text.splitlines()) if needle in line]
        print(needle, lines)

print("== source message occurrences ==")
for name, path in [("route", route), ("store", store), ("test", test)]:
    if path.exists():
        text = path.read_text()
        for needle in ["invalid transition", "invalid review state"]:
            print(name, needle, text.count(needle))
PY

Repository: jaylfc/taOS

Length of output: 1046


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import ast

route = Path("tinyagentos/routes/project_doc_review.py")
store = Path("tinyagentos/projects/doc_review_store.py")
test = Path("tests/test_doc_review.py")

print("== AST exception handling at route ==")
if route.exists():
    tree = ast.parse(route.read_text())
    for node in ast.walk(route):
        pass
    for node in ast.walk(tree):
        if not isinstance(node, ast.ExceptHandler):
            continue
        exc_name = ast.unparse(node.type) if node.types else None
        print("try line", node.lineno, "handler:", exc_name)
        for child in node.body:
            print("  line", child.lineno, ast.unparse(child)[:220])
        for cmp in ast.walk(node):
            if not isinstance(cmp, ast.Compare):
                continue
            for sub in ast.walk(cmp):
                if isinstance(sub, ast.Constant) and sub.value == "invalid transition":
                    print("  contains substring check at line", cmp.lineno)

print("== store classes and custom exceptions ==")
if store.exists():
    tree = ast.parse(store.read_text())
    for cls in ast.walk(tree):
        if isinstance(cls, ast.ClassDef):
            bases = ast.unparse(cls) if cls.bases else None
            if bases and "Exception" in bases:
                print(cls.lineno, "class", cls.name, "bases", bases)
    for node in ast.walk(tree):
        if isinstance(node, ast.ClassDef):
            if node.bases and isinstance(node.bases[0], ast.Name) and node.bases[0].id == "ValueError":
                print("ValueError subclass at", node.lineno, node.name)

print("== tests status/message assertions ==")
if test.exists():
    text = test.read_text()
    for needle in ["invalid transition", "invalid review state", "status_code=409", "status_code=400"]:
        lines = [str(i+1) for i,line in enumerate(text.splitlines()) if needle in line]
        print(needle, lines)

print("== source message occurrences ==")
for name, path in [("route", route), ("store", store), ("test", test)]:
    if path.exists():
        text = path.read_text()
        for needle in ["invalid transition", "invalid review state"]:
            print(name, needle, text.count(needle))
PY

Repository: jaylfc/taOS

Length of output: 623


Avoid branching on exception message text.

routes/project_doc_review.py chooses between 409 and 400 by checking "invalid transition" in str(exc), while doc_review_store.py uses "invalid transition: ..." for transitions and "invalid review state: ..." for unknown states. Store-level wording changes would silently change HTTP status codes. Raise a distinct InvalidTransitionError from doc_review_store.py for the conflict case, catch it here, and keep other ValueErrors as 400. Use from exc on both FastAPI raises so the original traceback is preserved.

🧰 Tools
🪛 Ruff (0.16.0)

[warning] 73-73: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


[warning] 74-74: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🤖 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 `@tinyagentos/routes/project_doc_review.py` around lines 66 - 74, Update the
review-state handling around set_review_state to catch a distinct
InvalidTransitionError and return HTTP 409, while catching remaining ValueError
instances as HTTP 400 without inspecting exception message text. Define and
raise InvalidTransitionError in the store’s invalid-transition path, and add
from exc to both HTTPException raises to preserve exception chaining.

@jaylfc

jaylfc commented Aug 3, 2026

Copy link
Copy Markdown
Owner Author

nemotron-ultra-orB review

VERDICT: Blocking issues found — UI state transition logic violates backend state machine; silent error handling in frontend; potential key collision in review state map.

  • desktop/src/apps/FilesApp.tsx:538-545cycleDocReview cycles through states in fixed order (awaiting_reviewapprovedchanges_requestedawaiting_review), but backend VALID_TRANSITIONS only allows approvedawaiting_review and changes_requestedawaiting_review. Clicking "Approved" badge will attempt approvedchanges_requested (invalid), causing 409; UI doesn't surface the error.

  • desktop/src/apps/FilesApp.tsx:564-565fetchReviewStates catches all errors and silently sets empty state, hiding network/permission failures from user.

  • desktop/src/apps/FilesApp.tsx:1608-1620reviewStates keyed by f.path || f.name; files with same name in different directories (e.g., src/foo.md and docs/foo.md) collide, showing wrong badge.

  • tinyagentos/projects/doc_review_store.py:68-73 — First-write validation logic is convoluted and error message misleading: "first state must be awaiting_review or a direct transition target" but awaiting_review is not in VALID_TRANSITIONS["awaiting_review"].

  • tinyagentos/routes/project_doc_review.py:32-33_authorize_doc_review_actor returns tuple | JSONResponse without explicit union type; callers use isinstance(auth, JSONResponse) which is fragile.

  • tests/test_doc_review.py — No test for cycleDocReview UI transition logic; no integration test exercising full FilesApp → API → store flow; no test for duplicate doc_path collision in reviewStates map.

  • desktop/src/apps/FilesApp.tsx:302-303REVIEW_BADGE_STYLES/REVIEW_BADGE_LABEL use raw string keys instead of DocReviewState type, losing type safety.

Automated first-pass review by the nemotron-ultra-orB lane. The lead still reviews before merge.

@jaylfc

jaylfc commented Aug 3, 2026

Copy link
Copy Markdown
Owner Author

nemotron-super review

VERDICT: No blocking issues found.

Automated first-pass review by the nemotron-super lane. The lead still reviews before merge.

@jaylfc
jaylfc merged commit e3829d4 into dev Aug 3, 2026
18 of 19 checks passed
@jaylfc
jaylfc deleted the exec/tsk-737pd7 branch August 3, 2026 01:02
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.

1 participant