@@ -22,16 +22,16 @@ Procedures and architecture for contributing to
2222 routes/ ← one APIRouter module per feature area (~127 modules)
2323 templates/ ← minimal: agent_debugger.html only (frontend is React SPA)
2424 channel_hub/ ← framework-agnostic messaging: connectors + MessageRouter
25- adapters/ ← thin per-framework agent adapters (~25 lines each)
25+ adapters/ ← per-framework agent adapters (generic ~20 lines, acp ~500); each declares verification_status
2626 cluster/ ← distributed compute: worker registry, task routing, GPU lease
2727 worker/ ← cross-platform worker apps (system tray, Android, iOS)
2828 *_store.py, base_store.py ← data layer: top-level BaseStore subclasses (aiosqlite); one SQLite file per store; no stores/ dir
2929 chat/ projects/ mcp/ ← chat, project board/canvas/A2A, MCP proxy+permissions
3030 installers/ containers/ ← model/app installers; Docker + LXC backends
3131 migrations/ ← DB migrations
3232 desktop/ ← React + TypeScript SPA (Vite)
33- app-catalog/ ← YAML app manifests + catalog.yaml (~108 apps)
34- tests/ ← pytest suite (~4,845 tests )
33+ app-catalog/ ← YAML app manifests + catalog.yaml
34+ tests/ ← pytest suite (large; counts rot - trust the tree )
3535 docs/ ← documentation; agent manual compiled from docs/agent-manual/
3636```
3737
@@ -47,10 +47,8 @@ Procedures and architecture for contributing to
4747 lifespan (` app.state.secrets ` , …). Never reference a migration-added column inside ` SCHEMA ` .
4848- ** Config** - ` AppConfig ` dataclass in ` config.py ` ; YAML serialisation; async-locked saves via
4949 ` save_config_locked() ` ; typed backends (` rkllama ` , ` ollama ` , ` openai ` , ` anthropic ` , …).
50- - ** Templates** - all real UI work goes in the ` desktop/ ` React SPA. The Pico CSS + htmx guidance
51- applies ** only** to the single legacy Jinja template (` agent_debugger.html ` ): ** Pico CSS utility
52- classes only** (no other CSS framework), htmx (` hx-get ` , ` hx-target ` , ` hx-swap ` ) for dynamic
53- partials, semantic HTML, ARIA labels on interactive elements without visible text.
50+ - ** Templates** - all real UI work goes in the ` desktop/ ` React SPA. One legacy Jinja template
51+ exists (` agent_debugger.html ` ); if you must touch it, match its existing Pico CSS + htmx style.
5452- ** Frontend** - React + TypeScript SPA in ` desktop/ ` . Built with Vite: ` npm run build ` outputs
5553 to ` static/desktop/ ` (gitignored). For development: ` npm run dev ` serves with hot reload on
5654 port 5173. One concern per component; API calls in dedicated hooks or service files.
@@ -92,10 +90,11 @@ Procedures and architecture for contributing to
9290 --title " feat(scope): description" --body " Fixes #<issue>. Tests: N/N pass."
9391 gh pr ready < PR#>
9492 ```
95- Do NOT wait for CI to * start* - fork PRs are gated behind maintainer workflow approval, so a
96- fresh fork PR's matrix run may never begin on its own; mark ready once the CODE is done and
97- local tests pass. This does NOT mean ignore red CI: once the matrix runs you own making it
98- green - re-check ` gh pr checks <PR#> ` and fix any red matrix job before considering the task done.
93+ Fork CI approval is per-contributor: a FIRST-TIME contributor's workflow runs sit in
94+ ` action_required ` until a maintainer approves (mark ready once the CODE is done and local tests
95+ pass, then surface the approval need); a RETURNING contributor's CI runs automatically - no
96+ approval wait, and you own making it green (` gh pr checks <PR#> ` : test 3.12/3.13 + lint +
97+ spa-build all pass) before considering the task done.
9998
100996 . ** Never commit directly to ` dev ` or ` master ` .** All work happens on branches.
101100 Main only receives merges via upstream PR approval.
@@ -147,6 +146,9 @@ uv run pytest tests/ --ignore=tests/e2e -n auto
147146- Python 3.12 + 3.13 on every PR/push; 3.11 on nightly cron only
148147- GitHub Actions: ` .github/workflows/ci.yml ` in upstream repo
149148- Uses ` uv sync --frozen ` and ` pytest -n auto `
149+ - Also required: ` spa-build ` (npm build + tsc + ** vitest** - a desktop type error or failing
150+ component test fails CI), a "Verify app starts" ` create_app ` import smoke, ` lint `
151+ (` compileall ` ), and ` cla ` . The doc-gate is a separate workflow.
150152
151153## CLA - HUMAN signs
152154
@@ -180,24 +182,37 @@ I have read the CLA Document and I hereby sign the CLA
180182
181183## Post-Push Bot Review Cycle
182184
183- After pushing a PR and marking it ready, automated bots (Kilo, CodeRabbit) run reviews.
184- Address their findings ** before** surfacing the PR for human maintainer review - this
185- eliminates the wasteful push→block→manual-check→unblock→re-dispatch cycle.
185+ After pushing a PR and marking it ready, automated bots review it. The reliable gate is
186+ ** Kilo Code Review + Gitar** . CodeRabbit is unreliable - a "pass" check can be a rate-limited
187+ no-op, so never treat a CodeRabbit pass alone as evidence of review (its findings, when it does
188+ run, still get folded). Qodo (` qodo-code-review ` ) appears on old PRs but is paused. Address all
189+ findings ** before** surfacing the PR for human maintainer review.
186190
187191### Procedure
188192
1891931 . ** Push PR and mark ready.** Wait ~ 10 minutes for bot reviews to complete.
190- 2 . ** Pull bot comments:**
194+ 2 . ** Pull bot findings - reviews AND inline comments, all bots.** The login FORM differs by API
195+ surface, so filter accordingly: the GraphQL command (` gh pr view --json ` ) returns BARE logins
196+ (` kilo-code-bot ` , ` gitar-bot ` , ` coderabbitai ` , ` qodo-code-review ` ), while the REST command
197+ (` gh api .../comments ` ) returns the ` [bot] ` -suffixed form (` kilo-code-bot[bot] ` ). The GraphQL
198+ filter below uses a substring ` test() ` so it matches the bare form; the REST command does not
199+ filter, so the suffix is harmless there.
191200 ``` bash
192- gh pr view < PR#> --repo jaylfc/taOS --json comments --jq \
193- ' .comments[] | select(.author.login == "kilo-code-bot" or .author.login == "coderabbitai[bot]")'
201+ # Review summaries + issue comments (GraphQL, bare logins):
202+ gh pr view < PR#> --repo jaylfc/taOS --json reviews,comments --jq \
203+ ' (.reviews[], .comments[]) | select((.author.login? // "") | test("kilo-code-bot|gitar-bot|coderabbitai|qodo")) | {login: .author.login, body: .body}'
204+ # Inline (line-anchored) review comments (REST, [bot]-suffixed) - Kilo/CodeRabbit post most findings here:
205+ gh api repos/jaylfc/taOS/pulls/< PR#> /comments --jq \
206+ ' .[] | {login: (.user.login? // ""), path, line, body}'
194207 ```
208+ Check which commit SHA each bot actually reviewed - a "pass" on a stale commit is not a pass
209+ on your head.
1952103 . ** If issues found:** fix all findings in a single commit, re-run local tests, push,
196211 then go back to step 1 (max 2 cycles). If findings still persist after 2 cycles, stop -
197212 do not loop further; surface the remaining findings to the human.
198- 4 . ** Only block for maintainer review when bots are clean** - 0 CRITICAL, 0 WARNING.
199- If a SUGGESTION-only finding is genuinely not applicable, note the rationale in a
200- PR comment before blocking.
213+ 4 . ** Only block for maintainer review when bots are clean** - 0 CRITICAL, 0 WARNING - and fold
214+ EVERY finding, nits and suggestions included. If a SUGGESTION is genuinely not applicable,
215+ note the rationale in a PR comment before blocking.
201216
202217### Severity tiers
203218
@@ -207,14 +222,59 @@ eliminates the wasteful push→block→manual-check→unblock→re-dispatch cycl
207222| WARNING | Must fix before blocking for review |
208223| SUGGESTION | Fix or explain why not applicable |
209224
210- ### Time estimates
225+ ## PR lifecycle discipline (fold-first, rebase, closures)
211226
212- | Phase | Duration |
213- | -------| ----------|
214- | First bot pass (Kilo + CodeRabbit) | ~ 10 min |
215- | Fix cycle (if needed) | ~ 5–10 min |
216- | Second bot pass (if re-pushed) | ~ 10 min |
217- | ** Worst case (2 cycles)** | ** ~ 30 min** |
227+ The review pipeline only works if the open-PR set stays small and every finding
228+ gets closed out. These rules are load-bearing; the maintainer gates on them.
229+
230+ ### Fold-first: findings outrank new work
231+
232+ If ANY of your open PRs has an unaddressed maintainer fold list or bot finding,
233+ addressing it comes BEFORE opening a new PR. Priority order each work session:
234+
235+ 1 . Fold open findings on existing PRs (maintainer comments first, then bot findings).
236+ 2 . Rebase any of your PRs that show CONFLICTING against the base branch.
237+ 3 . Only then start a new slice.
238+
239+ A finding folded within hours merges the same day; a finding left while you open
240+ new PRs stalls the whole train behind it (the maintainer will not merge past an
241+ open finding, ever).
242+
243+ ### Rebase cadence and the stale-base rule
244+
245+ - dev moves fast. When your PR shows CONFLICTING, rebase onto current dev
246+ promptly - a CONFLICTING PR is invisible to the merge queue.
247+ - A green CI run computed BEFORE the base branch moved is STALE. Two individually
248+ green PRs can conflict semantically with zero textual conflict (see the
249+ #2009 /#1932 App-key incident, fixed in #2041 ). If dev advanced since your last
250+ CI run, rebase (or push an empty commit) so CI re-runs against the current base
251+ before asking for merge.
252+ - Keep your open-PR count small (aim under 10). A wide-open set guarantees most
253+ of it is permanently CONFLICTING and re-review effort is wasted.
254+
255+ ### Closing PRs: always link the successor
256+
257+ Never close a PR silently. In the closing comment state exactly one of:
258+
259+ - "Superseded by #NNNN" (and confirm every still-open finding from this PR is
260+ in the successor's scope), or
261+ - "Landed via #NNNN" (when the content merged through another PR), or
262+ - "Abandoned because <reason >".
263+
264+ A close without a successor link reads as lost work and forces the maintainer
265+ into git forensics (this happened with #1927 /#1924 - both were legitimate
266+ "landed via" closures that looked like data loss for hours).
267+
268+ ### One PR per slice
269+
270+ - A fix and the test that proves it belong in ONE PR (pitfall 13 in
271+ docs/contributor-pitfalls.md).
272+ - Never open a sibling PR for a slice that already has one. If a fresh branch is
273+ genuinely needed, open the new PR, link it, and close the old one with the
274+ supersede note in the same action.
275+
276+ Read docs/contributor-pitfalls.md before every PR - fold lists reference its
277+ items by number (for example "pitfall 5").
218278
219279## Common fix patterns
220280
@@ -233,6 +293,41 @@ eliminates the wasteful push→block→manual-check→unblock→re-dispatch cycl
233293- ** Debugging a test:** confirm it uses the async ` client ` fixture and that ` tmp_data_dir ` setup is
234294 complete; check the store's ` init() ` ; isolate with ` pytest <path>::<test> -v ` .
235295
296+ ## Frontend CSRF contract (session-authenticated SPA calls)
297+
298+ Every mutating route requires ` X-CSRF-Token ` on cookie-session requests (` verify_csrf ` is attached
299+ router-wide). Any SPA ` fetch ` that POSTs/PUTs/PATCHes/DELETEs must attach the double-submit header:
300+ use ` withCsrf(init) ` from ` desktop/src/lib/csrf.ts ` , or the ` taosFetch ` wrapper
301+ (` desktop/src/lib/taos-fetch.ts ` ) which applies it automatically. ** A raw
302+ ` fetch("/api/...", {method:"POST"}) ` passes vitest AND pytest (tests bypass CSRF) but 403s
303+ "CSRF token missing" in production** - this exact class shipped as a bug (#1977 ). Bearer-token
304+ (agent JWT) calls are CSRF-exempt; only cookie sessions need the header.
305+
306+ ## Agent auth model (Bearer JWT vs session)
307+
308+ Agents authenticate with grant-gated registry JWTs (` Authorization: Bearer ... ` ), which are
309+ ** CSRF-exempt** ; human sessions use cookies + the CSRF header. Access derives from an ACTIVE grant
310+ per ` (agent, project) ` - not from token claims - and no-grant yields an existence-hiding 404.
311+ Agent-facing routes must ALSO be allowlisted in ` tinyagentos/auth_middleware.py ` (see Pitfalls).
312+ Onboarding surfaces: project invites (` routes/project_invites.py ` , URL + PIN), OS-level agent
313+ invites (` /api/agents/invites ` ), and the auth-request consent flow. Source of truth:
314+ ` docs/agent-coordination.md ` section "Agent API surface (scoped registry JWT)".
315+
316+ ## Adapter verification vocabulary
317+
318+ Every adapter in ` tinyagentos/adapters/__init__.py ` declares
319+ ` verification_status: tested | beta | experimental | broken ` (plus ` tracking_issue ` , required for
320+ ` broken ` ). ` tested ` and ` beta ` are the verified tiers (` verified_only ` filtering returns both);
321+ ` broken ` blocks the deploy wizard. A new or edited adapter must set this correctly - defaulting a
322+ new framework to ` experimental ` is the norm until it passes round-trip verification.
323+
324+ ## Version sync (release bumps)
325+
326+ Four files carry the version and must stay identical: ` pyproject.toml ` , ` tinyagentos/__init__.py ` ,
327+ ` desktop/package.json ` (all ` 1.0.0-beta.N ` ), and ` uv.lock ` 's ` tinyagentos ` entry in PEP 440
328+ normalized form (` 1.0.0bN ` ). Only pyproject vs uv.lock is test-gated
329+ (` tests/test_version_lock_sync.py ` ); the other two drift silently, so check all four on any bump.
330+
236331## Documentation gate
237332
238333A gate blocks PRs that add or remove certain feature code without a matching doc update
@@ -293,10 +388,11 @@ controls which hardware profiles see the app as recommended.
293388
294389- ** Full test suite is too large to run locally without ` -n auto ` .** Use the canonical
295390 gate: ` uv run pytest tests/ --ignore=tests/e2e -n auto ` . CI handles the full matrix.
296- - ** CI may show ` action_required ` on every PR from a fork.** GitHub requires maintainer approval
297- for workflow runs from first-time contributor forks. This can re-trigger on each new PR even after
298- previous PRs were approved - it's per-workflow-run, not per-contributor. Surface to the human;
299- do NOT poll or re-push.
391+ - ** Fork CI approval is first-time-only.** The repo's policy requires maintainer approval only for
392+ a contributor's FIRST fork PR (` action_required ` on the CI run - surface to the human, do NOT
393+ poll or re-push). A returning contributor's CI runs automatically; if your checks show only bots
394+ green but no test/spa-build jobs at all, check ` gh api "repos/jaylfc/taOS/actions/runs?head_sha=<sha>" `
395+ for ` action_required ` before assuming CI passed - bot-only green is NOT CI green.
300396- ** No formatter/linter * config* yet, but CI is not silent.** There is no ` .ruff.toml ` and no
301397 ` [tool.ruff] ` /` [tool.black] ` /` [tool.mypy] ` section in ` pyproject.toml ` (ruff may land soon -
302398 check ` pyproject.toml ` before assuming). CI does run a ` lint ` job (`python -m compileall
@@ -342,21 +438,8 @@ controls which hardware profiles see the app as recommended.
342438
343439## Issue triage
344440
345- ### Finding actionable issues
346- 1 . Filter GitHub issues by ` good first issue ` or ` help wanted ` labels
347- 2 . Check issue age - fresh issues (< 2 weeks) have highest chance of being unclaimed
348- 3 . Read the issue body carefully - look for clear reproduction steps
349- 4 . Check if anyone is already assigned
350-
351- ### Difficulty estimation
352- - ** Catalog app addition** (~ 30 min): New manifest.yaml + catalog.yaml entry
353- - ** Bug fix** (1–3 hours): Reproduce → find root cause → fix + regression test
354- - ** Feature** (3+ hours): Design → implement → tests → documentation
355-
356- ### Before starting work
357- 1 . Sync from upstream: ` git fetch upstream dev `
358- 2 . Verify the issue is still open and unassigned
359- 3 . Comment on the issue: "Working on this - will open a draft PR"
441+ taOS-specific rules only: verify the issue is still open and ** unassigned** , comment
442+ "Working on this - will open a draft PR" before starting, and sync from upstream ` dev ` first.
360443
361444## First-run setup
362445
0 commit comments