Skip to content

[review-only] Python port of Smithers + recursive meta-workflow - #1

Draft
lluisinthedesert wants to merge 37 commits into
mainfrom
port/resume
Draft

[review-only] Python port of Smithers + recursive meta-workflow#1
lluisinthedesert wants to merge 37 commits into
mainfrom
port/resume

Conversation

@lluisinthedesert

@lluisinthedesert lluisinthedesert commented May 18, 2026

Copy link
Copy Markdown

Summary

Draft PR for cloud-reviewer feedback (Codex / Greptile / Devin). Do not mergeport/resume is the long-lived Python-port branch on this fork; this PR exists to give reviewers a surface to comment against.

34 commits ahead of main. Three threads of work:

1. smithers_py — Python port of upstream Smithers (hand-coded)

Brings the Python port from ~30% to ~60% of upstream's component surface. Added subsystems:

  • smithers_py.memory (1,304 LoC, 18 tests) — cross-run memory, three layers (working / messages / semantic recall), four namespaces, pluggable embedding adapter (OpenAI default + Null for tests), three processors (TtlGC, TokenLimiter, Summarizer).
  • smithers_py.tools (1,467 LoC, 35 tests) — read/write/edit/grep/bash sandboxed tools + define_tool factory + persisted ts_tool_calls log. Path containment, network policy block-list, output truncation.
  • smithers_py.scorers (1,380 LoC, 27 tests) — schema_adherence / latency / relevancy / toxicity / faithfulness + llm_judge + create_scorer factory. Sampling (all/ratio/none). Persists to ts_scores.
  • smithers_py.cache (562 LoC, 18 tests) — cache.by + version + schema-signature key derivation. Three scopes (run/workflow/global). TTL + lazy sweep.

Existing 724 tests still pass. New total: 822 passing.

See PARITY_PLAN.md for the per-subsystem scope.

2. examples/smithers-port-py/ — recursive meta-workflow

A Smithers workflow that uses Smithers to port itself to Python. Two variants:

  • workflows/port-subsystem.tsx — API mode, parallel fan-out per file via AnthropicAgent. Cheap (~$0.20/subsystem) but suffers cross-file naming drift.
  • workflows/port-subsystem-cli.tsx — CLI mode, single Task with ClaudeCodeAgent + file tools. Agent reads what it just wrote before the next file, runs pytest against the generated tests, iterates. Pattern matches Cory's bun-port-smithers.

Plus the ongoing-sync meta-workflow (workflow.tsx) that watches upstream PRs and classifies them as port / skip / port-with-replacement, with parity verification via the existing examples/wire_compat/ cross-runtime test.

3. smithers_py_meta/ — recursive port proof

Generated by port-subsystem-cli.tsx from markdown specs. 4 of 5 subsystems shipped this way:

Subsystem Path LoC Tests Workflow run
serve smithers_py/serve/ 756 12/12 port-serve-cli-v2
memory smithers_py_meta/memory/ 1004 16/16 port-mem-cli-v3
tools smithers_py_meta/tools/ 450 35/35 port-tools-cli
cache smithers_py_meta/cache/ 669 21/21 port-cache-cli

Scorers run was discarded — the agent overwrote the hand-coded version despite the explicit pythonTargetDir directive. See META_WORKFLOW_PROOF.md for the failure-mode writeup and the spec-text fix.

Per-subsystem cost: ~$0.30-0.80 on Sonnet 4.5 (≤$1/subsystem acceptance). Tests pass independently for both hand-coded and meta-generated versions.

What I want reviewed

In order of priority:

  1. Idiom + correctness of the hand-coded subsystems — memory / tools / scorers / cache. Anything Pythonic-but-wrong, missing edge cases, or schemas that drift from upstream?
  2. The meta-workflow design — is port-subsystem-cli.tsx the right shape for porting whole subsystems? Anything obvious to harden in the prompt or workflow scaffold?
  3. Comparison artifactsmithers_py_meta/<subsystem> vs smithers_py/<subsystem> for memory / tools / cache. What did the agent miss that's load-bearing?
  4. The recursive-port narrative — does META_WORKFLOW_PROOF.md hold up as the canonical write-up?

Test plan

  • cd smithers_py && .venv/bin/python -m pytest -q — 822 passing baseline
  • examples/smithers-port-py/scripts/verify-subsystem.sh serve — 12/12 meta-workflow output
  • examples/smithers-port-py/scripts/verify-subsystem.sh memory smithers_py_meta — 16/16
  • examples/smithers-port-py/scripts/verify-subsystem.sh tools smithers_py_meta — 35/35
  • examples/smithers-port-py/scripts/verify-subsystem.sh cache smithers_py_meta — 21/21
  • Cross-runtime parity: examples/wire_compat/ 5/5 row diffs match TS↔Python

🤖 Generated with Claude Code

Greptile Summary

This PR ports the Smithers TypeScript runtime to Python (smithers_py), adding four new subsystems — memory, tools, scorers, and cache — totalling ~4,200 lines of hand-coded Python, plus a parallel smithers_py_meta set generated by a recursive meta-workflow using ClaudeCodeAgent.

  • smithers_py/tools: sandboxed read/write/edit/grep/bash built-ins with path-containment and network-block-list enforcement; two correctness bugs found (rg flag injection via un-guarded pattern, CRLF mismatch in the pure-Python unified-diff applier).
  • smithers_py/cache: SQLite-backed output cache with TTL and scope-prefix purge; purge_scope uses an unescaped LIKE wildcard that can delete entries from unintended scopes when a scope_id contains % or _.
  • smithers_py/serve/auth.py: bearer-token comparison uses Python != instead of hmac.compare_digest, making it vulnerable to timing-based token enumeration.

Confidence Score: 3/5

Several concrete bugs in the core tool and cache paths need fixing before the hand-coded subsystems are production-ready.

The edit tool silently fails on CRLF files, the grep tool misfires on patterns starting with -, cache purge can delete unrelated entries on scope_ids with %, and the auth handler leaks timing information. These are real defects on actively-used code paths.

smithers_py/tools/builtins.py (two bugs), smithers_py/cache/init.py (LIKE wildcard escaping), and smithers_py/serve/auth.py (timing-safe comparison)

Security Review

  • Timing-sensitive token comparison (smithers_py/serve/auth.py line 31): token != auth_token short-circuits on the first differing byte; an attacker with enough requests can use response-time differences to recover the token. Fix: hmac.compare_digest.
  • rg flag injection (smithers_py/tools/builtins.py): a grep pattern starting with - is passed to rg without a -- end-of-options marker, causing it to be interpreted as a flag rather than a search pattern.

Important Files Changed

Filename Overview
smithers_py/tools/builtins.py Implements read/write/edit/grep/bash built-in tools. Two bugs: rg receives pattern without a -- sentinel (flag injection), and _apply_unified_diff uses .rstrip("\n") which silently mismatches CRLF files.
smithers_py/tools/sandbox.py Path containment and network-command block-list helpers. Symlink-walk defense is solid. Block list mirrors upstream and intentionally leaves some vectors (nc, ssh) unblocked.
smithers_py/cache/init.py SQLite-backed task output cache with TTL and scope-prefix purge. purge_scope builds a LIKE pattern without escaping %/_ wildcards, so a scope_id containing those characters silently deletes entries from other scopes.
smithers_py/memory/store.py Three-layer SQLite memory store (working/messages/semantic recall). Correct for single-process asyncio use. save_message SELECT MAX + INSERT is not transactionally isolated for multi-process writers.
smithers_py/memory/processors.py TtlGarbageCollector, TokenLimiter, and Summarizer. Correct behavior; processors reach into store._connect() directly which is a tight coupling concern.
smithers_py/serve/auth.py FastAPI bearer-token dependency. Token comparison uses Python != (timing-sensitive); should use hmac.compare_digest for a production-facing service.
smithers_py/scorers/builtins.py Five built-in scorers plus llm_judge and create_scorer factories. Logic is clean and well-matched to upstream surface.
smithers_py/scorers/runner.py Concurrent scorer execution and persistence to ts_scores. Contains dead code: _one inner function is defined but never called; actual execution goes through _runWithCapture.
smithers_py/tools/define.py define_tool factory and ToolCallLog persistence. Well-structured; idempotency-key warning at construction time is a nice touch.
smithers_py/memory/embeddings.py OpenAI and Null embedding adapters plus pack/unpack helpers. Model-mismatch guard in recall() correctly skips vectors from a different model.

Fix All in Codex Fix All in Claude Code Fix All in Cursor

Reviews (1): Last reviewed commit: "META_WORKFLOW_PROOF.md: fold in the appr..." | Re-trigger Greptile

Greptile also left 6 inline comments on this PR.

Luis Manrique and others added 30 commits May 18, 2026 07:58
Adds PORT_RESUME.md at the repo root and a banner at the top of
smithers_py/README.md. Documents:

- This fork is the working area for resuming the v1.0.0 Python port
  that has been quiet on upstream/python since 2026-01-23.
- Catch-up target: parity with current TS main public API.
- Existing design choices (7-phase tick loop, decorator + jsx node
  trees, SQLite durable state, PydanticAI executors) are preserved.
- Wire-compatibility is the acceptance criterion: Python and TS
  runtimes share SQLite row shape; cross-runtime resume must work.
- Coordination assumes friendly fork until upstream responds; intent
  is to PR back to smithersai/smithers:python.

No upstream code is modified beyond the banner. Original LICENSE
and attribution preserved verbatim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Records the result of an as-of-2026-05 sanity check on the existing
smithers_py v1.0.0 code:

- uv sync resolves cleanly on Python 3.12.
- import smithers_py works at module level.
- pytest (sans e2e) passes 645 tests, 1 skipped, 0 failures in ~9s.

This narrows the resume scope materially. The work is API catch-up
against current TS main, not un-rotting a stale port.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the baseline health check, spot-checked the smithers_py public
node surface against what bun-port-smithers (canonical TS example) is
built from.

The gap is a *design* gap, not a translation gap:
- TS main uses: Workflow, Sequence, Parallel, Task (typed outputs),
  Subflow, ApprovalGate, HumanTask, Worktree, MergeQueue.
- smithers_py exposes: If, Phase, Step, Ralph, While, Fragment, Each,
  Claude, Effect.

Two honest paths forward — adding TS shape into smithers_py, or
keeping the v1.0.0 primitives and translating workflows. Neither
should be picked without upstream's input. Updated the DM question
to surface this explicitly.

Demo target once resolved: examples/bun-port-smithers-py/ — a Python
port of bun-port-smithers wired to smithers_py, with cross-runtime
SQLite row compatibility as the acceptance criterion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resume effort, phase 1: bring the smithers_py public surface to parity
with current TS main without changing the existing tick-loop engine.
Adds the components needed to author workflows the way bun-port-smithers
does in TS.

smithers_py.nodes.ts_compat — 9 new node types:
  WorkflowNode, SequenceNode, ParallelNode, TaskNode, SubflowNode,
  ApprovalGateNode, HumanTaskNode, WorktreeNode, MergeQueueNode.
  Pydantic models on existing NodeBase, registered in the discriminated
  Node union. Accept both snake_case and camelCase keyword args so TS
  call sites port verbatim.

smithers_py.facade — create_smithers / createSmithers facade:
  Mirrors TS createSmithers({input, output, ...schemas}, {dbPath})
  ergonomics. Returns SmithersConfig with .outputs (typed OutputRef
  namespace) and @config.workflow decorator.

Tests:
  34 new tests in nodes/test_ts_compat.py and test_facade.py. Total
  suite: 679 passed, 1 skipped, 0 failures (up from 645 baseline;
  zero regressions on existing engine tests).

examples/bun_port_smithers_py/ — Python port of the canonical example:
  - components/schemas.py: Pydantic mirrors of every Zod schema.
    Fractional metrics nested under `metrics` for cross-runtime row-
    shape parity (the float→INTEGER column trap discovered during the
    understudy spike).
  - components/agents.py: 16 named dry-mode agents matching TS 1:1.
    Real-mode stub warns and falls back to dry until smithers_py engine
    learns TaskNode.agent dispatch.
  - components/porting_rules.py: stable node ids, field keys, cache
    keys, sampling, TSV synthesis. Pure compute, no LLM.
  - workflows/lifetime_classify.py: Phase 1 (the lifetime classifier
    Cory describes as "the most important part") fully ported.
  - workflow.py: top-level scaffolding all 7 phases as Subflows with
    the post-lifetimes ApprovalGate wired.

pyproject.toml updated to include facade.py in the hatch wheel.

Graph construction validated end-to-end; engine dispatch on the new
node types is the next chunk. PORT_RESUME.md updated with status,
landed pieces, and explicit next steps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
smithers_py.runtime is a new lightweight runtime that walks
WorkflowNode/SequenceNode/ParallelNode/TaskNode/SubflowNode/
ApprovalGateNode/HumanTaskNode trees built with create_smithers, persists
outputs to SQLite, and supports pause/resume via the same approvals row
pattern TS Smithers uses. Independent from the v1.0.0 tick loop — both
runtimes coexist.

What landed:

- smithers_py/runtime/store.py: SQLite store with three new tables
  (ts_runs, ts_output_rows, ts_approvals). WAL mode, FKs on, all writes
  go through one class. Payloads stored as JSON to sidestep the float→
  INTEGER trap entirely.

- smithers_py/runtime/runner.py: walker that dispatches on node.type,
  validates Task outputs against Pydantic schemas, persists output rows
  with the v0 schema_version literal, pauses on ApprovalGate/HumanTask,
  routes Subflow into child run ids. Resume idempotent; pulls stored
  input from the run row when not re-passed.

- smithers_py/runtime/cli.py: `smithers-ts up|approve|deny|inspect|ps`.
  Workflow file loader handles both standalone .py files and
  package-relative imports (walks __init__.py chain).

- smithers_py/runtime/test_runner.py: 12 end-to-end tests covering
  sequential tasks, agent tasks, approval pause/resume, deny/fail vs
  deny/continue, subflow child runs and pause propagation, human task,
  inspect, input validation. All green.

- examples/hello_smithers_ts/workflow.py: minimal 3-step demo (greet
  task → approval gate → final task) that exercises the full pause/
  resume cycle in a 30-second walkthrough.

The existing bun-port-py workflow now runs end-to-end via the CLI:
phases execute as Subflows, post-lifetimes ApprovalGate pauses the
run, approve+resume completes with the terminal
smithers-bun-port-py-final-v0 row written.

Total suite: 691 passed, 1 skipped, 0 failures (up from 679; 12 new
runtime tests added, zero regressions on existing engine tests).

PORT_RESUME.md updated with the MVP demo commands, what MVP covers,
and the v0.2 backlog (concurrency, real-mode agents, worktree/merge
queue semantics, cross-runtime row diff, phases 2-7 of bun-port).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
smithersai#132 + AgentLike protocol + PARITY.md

Walks the upstream PR stream since the python branch froze (2026-01-23)
and ports the user-visible API changes. Tier-1 surface is now caught up;
deferred items are accounted for in PARITY.md with reasons.

What landed:

- NonRetryableError + retry loop (PR smithersai#132 port)
  Honors TaskNode.max_attempts with exponential backoff (configurable
  via SMITHERS_TS_RETRY_BACKOFF_BASE for tests). NonRetryableError
  short-circuits retries to surface AGENT_CONFIG_INVALID-style faults
  immediately. Schema/validation failures also skip retries because
  they describe deterministic faults. 4 new tests.

- resume --force + SIGINT cancellation (PR smithersai#87 port)
  run_workflow(..., force=True) takes over a run still marked
  'running' (crash recovery). Without force, refusing such a resume is
  the safety default to prevent racing on the same row. CLI gets a
  matching --force flag and a SIGINT handler that marks the in-flight
  run as 'cancelled' before exiting with status 130. 2 new tests.

- Duplicate output ref safety (PR smithersai#130 port)
  Confirmed via test: two outputs.<key> entries with the same Pydantic
  schema produce distinct OutputRefs sharing the schema reference.
  Matches upstream's structured-output handshake.

- AgentLike protocol (foundation for PRs smithersai#72/smithersai#125/smithersai#138)
  smithers_py.runtime.agents defines AgentLike (Protocol) +
  AsyncAgentLike + DryAgent. Mirrors the upstream AgentLike interface
  shape so future provider adapters (Anthropic, Claude Code, Codex,
  Pi, OpenCode) implement a documented contract. DryAgent ships for
  end-to-end tests and demo runs.

- PARITY.md
  Maps each upstream PR since 2026-01-23 to a port disposition
  (ported / deferred / N-A / open question). Documents the Effect API
  rewrite (PR-less context) as a v0.2 anyio migration target. Makes
  "what's left to catch up?" answerable in 30 seconds.

Total suite: 698 passed, 1 skipped, 0 failures (up from 691; 7 new
tests, zero regressions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the two next-most-fundamental control-flow primitives missing
from the TS-shape API:

- BranchNode: <Branch if={...} then={...} else={...}>. Walks the
  then-child when condition is True, else-child otherwise. Both
  children are typed Node references (not generic children lists);
  the runner picks one at execution time.

- LoopNode: <Loop until={...} maxIterations={N} onMaxReached={...}>.
  Iterates children up to max_iterations, exits when until_fn(ctx)
  returns True. on_max_reached controls policy when max is exhausted
  without satisfying until ("fail" or "return-last"). Each iteration
  writes child rows under a unique node-id suffix (.../loop:id/iter:N)
  so resume can skip already-completed iterations.

- TSRalphNode: re-export of LoopNode under the deprecated TS-API name.
  Kept inside smithers_py.nodes.ts_compat to avoid colliding with the
  v1.0.0 RalphNode in nodes/structural.py at the top-level binding.
  Workflow authors targeting modern TS should prefer LoopNode.

Wires PRs smithersai#109 (Ralph respects approved reviews) and smithersai#113 (nested
Loop/Ralph) — both are encoded naturally by the until-callable API
and the per-iteration node-id suffix.

7 new tests. Suite: 705 passed, 1 skipped, 0 failures (was 698).

PARITY.md updated: 7 PRs fully ported, 7 deferred, 9 N/A, 1 wait.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
examples/wire_compat/ is the artifact that defines what "Python is
wire-compatible with TS Smithers" actually means. It is the v0.1
parity acceptance check on the Python side and the drop-in point for
a future TS-side snapshot.

What landed:

- workflow.py: canonical workflow exercising every primitive currently
  in scope — WorkflowNode, SequenceNode, ParallelNode, BranchNode,
  LoopNode, TaskNode (render and DryAgent paths), SubflowNode (with
  child run), ApprovalGateNode (when=False auto-pass branch). 12
  deterministic output rows.

- snapshot_helpers.py: normalize_rows() strips run-specific identity
  (run_id, timestamps) and sorts by (node_id, iteration). diff_rows()
  emits one-line diagnostics per divergence.

- snapshot.json: the committed contract. 12 rows in normalized form.
  Regeneration is via generate_snapshot.py — explicit, never
  automatic, so drift requires a deliberate commit.

- test_wire_compat.py: two tests. Forward test asserts the Python
  run still matches snapshot.json. Reverse test confirms diff_rows
  actually catches divergence by flipping the branch input and
  verifying diffs surface.

- README.md: explains the contract, how to regenerate, what
  divergences are acceptable (subflow row isolation, approval row
  schema) vs which are bugs (different node_id / output_name /
  payload / iteration / missing rows).

When a TS twin of this workflow lands upstream, it goes through the
same normalize → JSON pipeline. An empty diff is the cross-runtime
parity assertion.

Suite: 707 tests passing (705 in smithers_py + 2 wire-compat).
Wire-compat tests run alongside the main suite via:

    uv run python -m pytest . /Users/luis/smithers/examples/wire_compat/

PARITY.md updated to reference the wire-compat acceptance contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The wire-compat acceptance test now passes: Python smithers_py and TS
upstream smithers-orchestrator produce IDENTICAL normalized SQLite row
sets for the canonical wire_compat workflow.

Built and ran:

- examples/wire_compat/workflow.tsx (TS twin via upstream smithers
  components: Workflow, Sequence, Parallel, Branch, Loop, Task,
  Subflow, ApprovalGate). Plus child-workflow.tsx and schemas.ts
  (Zod twins of the Pydantic schemas).
- examples/wire_compat/extract_ts_snapshot.py: reads the per-output-key
  Drizzle tables from a TS smithers.db, reconstructs payloads, filters
  by parent run_id, normalizes via snapshot_helpers.
- examples/wire_compat/test_cross_runtime.py: pytest harness that
  diffs python snapshot.json vs ts_snapshot.json and fails with
  categorized diagnostics. Now passes 5/5.

Initial diff surfaced 25+ divergences. Fixed each:

1. Dropped the `main/` path prefix and the path-stack walking from
   the Python runner. `node.id` is used as-is, matching TS's flat
   ids-within-a-run scheme. Branch is transparent (no `branch:br/then/`
   wrapper). Workflow authors must give unique ids per run.

2. Threaded iteration: int through _walk so LoopNode writes N rows
   with the same node_id and iteration=0..N-1, matching TS Drizzle's
   iteration-column-as-loop-counter. No more `loop:loop/iter:N/`
   suffixes.

3. Approval row now writes `{approved: bool, ...}` with no synthetic
   schema_version, matching TS upstream's approval row shape. The
   `smithers-py-approval-v0` placeholder is gone.

4. extract_ts_snapshot.py filters TS rows by parent run_id (the
   subflow's child run lives under <parent>:child:<sub.id>:0).

5. Restored the parent-level subflow output row that an earlier
   refactor had stripped — TS writes one row in the parent's
   <output_target> table reflecting the subflow's terminal output,
   plus the child's own rows under the child run_id. We now match.

Suite: 712 passed, 1 skipped, 0 failures (was 707; +5 wire-compat
cross-runtime tests, all green).

PARITY.md updated. examples/wire_compat/README.md documents the
journey + the regeneration commands + what divergences would now be
bugs going forward.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the gap between "1 phase scaffolded" and "every phase upstream
runs end-to-end on smithers_py." Status of upstream
examples/bun-port-smithers/ against our port:

  Upstream piece                    Our port
  ────────────────────────────────  ─────────────────────────────
  workflow.tsx                      workflow.py (wired)
  workflows/lifetime-classify.tsx   workflows/lifetime_classify.py
  workflows/phase-a-port.tsx        workflows/phase_a_port.py     ← NEW
  workflows/crate-compile-          workflows/crate_compile_      ← NEW
    bringup.tsx                       bringup.py
  workflows/ungate-proper-          workflows/ungate_proper_      ← NEW
    port.tsx                          port.py
  workflows/panic-probe-swarm.tsx   workflows/panic_probe_        ← NEW
                                      swarm.py
  workflows/test-swarm.tsx          workflows/test_swarm.py       ← NEW
  workflows/audit-sweeps.tsx        workflows/audit_sweeps.py     ← NEW
  components/schemas.ts             components/schemas.py (full   ← NEW
                                      Pydantic mirrors of every Zod
                                      schema — ~30 models)
  components/porting-rules.ts       components/porting_rules.py
                                    (existing + normalize_port_
                                    files, plan_crates_by_tier,
                                    dedupe_failures, survey_     ← NEW
                                    targets, survey_sweeps)
  components/agents.ts              components/agents.py (already
                                      had all 16 dry stubs)
  components/scorers.ts             components/scorers.py (stub) ← NEW

End-to-end smoke against a representative input runs every phase to
completion. Output:

  lifetimes → completed | Lifetime classification produced 2 field row(s)
  phaseA    → completed | Phase A: 2/2 clean, 2 fix task(s)
  compile   → completed | Compile: 2/2 crates green, 0 gated modules
  ungate    → completed | Ungate: 1/1 approved, 1 patched
  probes    → completed | Probes: 1/1 passed, 0 unique failures
  tests     → completed | Test swarm: 1/1 areas green, 1 merged
  sweeps    → completed | Sweeps: 1 fixed across 1 sweep(s)

Graph uses every TS-shape primitive: Workflow, Sequence, Parallel,
Loop, Task, Subflow, ApprovalGate, HumanTask, Worktree, MergeQueue.

Test suite: 712 passed, 1 skipped, 0 failures (unchanged from prior
green; the new bun-port port doesn't add unit tests, just exercises
the runtime end-to-end).

Two deferred upstream features used in test-swarm:
  - Signal / WaitForEvent for external CI — workflow honors the
    awaitExternalCiSignal flag structurally but doesn't pause.
    Pending v0.2.

bun_port_smithers_py/README.md updated with full status table.
PARITY.md updated with the bun-port section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…a2, graph CLI

Closes most of the v0.2 backlog in a single batch. Each piece is small
in isolation; together they bring the runtime + CLI from "MVP" to
"a real Smithers-py".

What landed:

1. Real concurrency in ParallelNode (smithers_py/runtime/runner.py)
   ThreadPoolExecutor with per-thread Store instances. SQLite WAL
   handles concurrent connections; children mutate the parent's
   output cache under a threading.Lock so downstream Sequence steps
   see sibling outputs. Measured: 5 × 0.2s tasks finish in ~0.21s
   in parallel vs 1.0s sequentially.

2. TaskNode.timeout_ms enforcement
   Each retry attempt runs in a single-worker ThreadPoolExecutor;
   Future.result(timeout=) raises a TimeoutError (retryable). Rogue
   computes detach via shutdown(wait=False) so the runner returns
   immediately. 0.41s end-to-end for a 2-attempt × 200ms task.

3. SignalNode + WaitForEventNode + smithers-ts signal CLI
   New ts_signals table (signal_id, run_id, event, correlation_id,
   payload, source). SignalNode writes a row; WaitForEventNode
   pauses until a matching signal exists. signal_run() for Python
   callers, `smithers-ts signal <runId> <event> --json '...'` for
   external delivery. bun_port_smithers_py/workflows/test_swarm.py
   now uses it — awaitExternalCiSignal=True wires a real
   WaitForEventNode that pauses on the CI verdict.

4. AnthropicAgent adapter
   Real-mode AgentLike implementation calling
   anthropic.Anthropic().messages.create(...). Optional output_schema
   appends a JSON-schema instruction + extracts (and parses) the
   assistant response, fence-tolerant. Install via
   `uv pip install 'smithers-py[anthropic]'`.

5. PromptTemplate (Jinja2 with str.format fallback)
   smithers_py.runtime.prompts.PromptTemplate accepts a Jinja2
   template + bound vars; lazy render at execution time. Falls back
   to str.format if Jinja2 not installed.
   TaskNode.prompt now accepts strings OR PromptTemplate-like
   objects with .render().

6. smithers-ts graph command
   Renders a workflow's DAG without executing — indented tree (default),
   JSON, or Graphviz DOT. Closes PR smithersai#89 gap (cyclic-ref handling).

7. Defensive agent.generate() call
   _invoke_agent_generate falls back gracefully if an agent's
   .generate() signature doesn't accept output_schema. Keeps the test
   agents in test_runner.py compatible while letting real-mode agents
   benefit from structured output.

8. Provider extras in pyproject.toml
   [project.optional-dependencies]:
     anthropic = ["anthropic>=0.40.0"]
     templates = ["jinja2>=3.0"]

Verified end-to-end:
- Concurrency: 5x speedup on 5×0.2s task batch
- Timeout: failure within timeout window (0.41s for 2×200ms)
- Signal: bun-port test_swarm pauses, external signal_run, resume completes
- AnthropicAgent: construct + Protocol satisfaction
- Jinja2: PromptTemplate("Hello {{ name }}!") renders end-to-end
- Graph: bun-port workflow tree printed in 3 formats

Suite: 712 passed, 1 skipped, 0 failures.

PARITY.md updated: 11 PRs fully ported, 6 deferred to v0.3 (all
provider adapters or supervisor loop). 9 N/A. 1 wait.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the remaining ports from PARITY.md. All Tier-1 + Tier-2 upstream
PRs since 2026-01-23 are now landed.

What landed:

1. SubprocessAgent base + 4 concrete adapters
   (smithers_py/runtime/subprocess_agents.py)
   - SubprocessAgent: common spawn/timeout/stderr/JSON-fence handling
   - ClaudeCodeAgent: wraps `claude --print` with permission_mode +
     allowed/disallowed tool list args
   - CodexAgent: wraps `codex` with model/thinking flags; tolerates
     stderr on success (PR smithersai#114)
   - OpenCodeAgent: wraps `opencode` (PR smithersai#125)
   - PiAgent: wraps `pi` in RPC mode with NDJSON stream parsing,
     picks the final `text`-bearing event as the terminal assistant
     response (PRs smithersai#85, smithersai#118)
   - All four satisfy AgentLike Protocol via isinstance check.

2. Supervisor loop (smithers_py/runtime/supervisor.py)
   Polls ts_runs for status='running' rows whose updated_at is older
   than stale_threshold; takes over via run_workflow(force=True).
   Serialized per-run by an in-process threading.Lock so two ticks
   can't both take over the same run (closes PR smithersai#124's reproduction
   case).

   parse_duration() accepts "10s", "2m", "1.5h", "500ms".
   SupervisorStats tracks polls / resumed / failed / skipped.

3. smithers-ts supervise CLI command
   smithers-ts supervise <workflow.py> [--workflow NAME]
     --interval 10s --stale-threshold 30s
     --max-concurrent 3 [--dry-run]
   Graceful SIGINT shutdown.

End-to-end smoke: seeded a 120s-stale 'running' row, ran Supervisor
with interval=0.3s, stop()'d after 1s — supervisor found the row,
called run_workflow with force=True, run completed, status updated.

PARITY.md updated:
- v0.3 lift section added
- 6 previously-deferred PRs flipped to ✅ Ported
- Summary now reads: 18 fully ported, 0 deferred at the PR level
- v0.4 backlog explicitly forward-looking (Effect/anyio, observability
  mirror, gateway, HMR, time-travel) — none are upstream PRs to chase

Suite: 712 passed, 1 skipped, 0 failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A Smithers TS workflow at examples/smithers-port-py/ that watches
upstream smithersai/smithers:main, classifies new commits, translates
accepted deltas via real-mode agents, verifies via wire_compat parity,
and opens PRs back against port/resume.

Mirrors examples/bun-port-smithers/ shape but for *ongoing* sync
rather than one-shot translation. Cory's bun-port was Smithers-as-
translator; this is Smithers-as-perpetual-sync-engine. The orchestrator
we just ported keeps its own Python twin synchronized.

What landed:

- workflow.tsx: 5 Subflow phases + 2 ApprovalGates + optional
  HumanTask + final report. Wires every TS-shape primitive
  (Sequence, Subflow, ApprovalGate, HumanTask, Task) end-to-end.

- workflows/:
  - upstream-watch.tsx: gh search wrapper for recently-merged PRs
  - delta-classify.tsx: Parallel per-PR classifier with deterministic
    static-rule shortcut for docs/gateway/types
  - delta-translate.tsx: Parallel per-port-row translator (real-mode
    via ClaudeCodeAgent)
  - cross-runtime-verify.tsx: live execSync of wire_compat parity
    tests; raises ApprovalGate if divergence found
  - pr-emit.tsx: batches drafted translations into a single PR

- components/:
  - schemas.ts: 12 Zod schemas for every phase artifact
  - agents.ts: dry-mode stubs that branch on prompt-tagged fields;
    real-mode swaps to ClaudeCodeAgent (writer) + PiAgent (reviewer)
  - sync-rules.ts: stableNodeId, classifyCacheKey,
    staticClassification, estimateCostMicrocents helpers
  - upstream-watch.ts: gh search subprocess wrapper

- prompts/: 5 MDX prompts (operator-plan, classify, translate,
  verify, emit)

- fixtures/input.smoke.json: 6 historical PRs (smithersai#87, smithersai#88, smithersai#109, smithersai#113,
  smithersai#130, smithersai#132 — the same Tier-1 ports we shipped manually this
  morning) for dry-mode smoke

- COSTS.md: per-PR + steady-state spend model.
  - Avg per-PR real-mode cost: $0.14 (50% static-rule shortcut +
    classify + translate + review + retry overhead)
  - Steady state: $5-10/month for ~1 LLM-eligible PR/week
  - Annual: <$50 even with bursty activity
  - One-off catch-up (60 stale PRs): <$10

- README.md: quick-start + file map + closing-the-loop narrative.

End-to-end dry-mode run output:

  Considered 6 PRs, ported 6, skipped 0.
  Parity held.
  PR status: drafted.
  estimatedSpendMicrocents: 0
  Wall clock: ~3 seconds.

Live wire_compat verification ran inside the verify Subflow via
execSync; PATH-augmented env so bun's child process can find `uv`
at ~/.local/bin.

Workflow file at /Users/luis/smithers/examples/smithers-port-py/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two-flavor real-mode for smithers-port-py-sync:

  SMITHERS_PORT_PY_AGENT_MODE=anthropic  (default)
    AnthropicAgent (AI SDK). Text-only generation; no filesystem
    tools. Translation diffs are captured in the run row but not
    auto-applied. Safe to spend.

  SMITHERS_PORT_PY_AGENT_MODE=cli
    ClaudeCodeAgent (writer) + PiAgent (reviewer). CLI agents that
    read/write the working tree. Requires `claude` and `pi`
    binaries on PATH.

fixtures/input.real-1pr.json pins one historical PR (smithersai#130) for a
minimal-cost first live run.

Real-mode end-to-end smoke (today):
  - Wiring works: AI SDK posted to api.anthropic.com/v1/messages,
    got proper 401 responses.
  - Blocker: ANTHROPIC_API_KEY env var was empty in the run shell.
    (The user has Claude Code installed for OAuth-style auth, not
    an API-key env var.)
  - Cost incurred: $0 — all calls auth-failed before billing.

To actually run real-mode end-to-end:
  export ANTHROPIC_API_KEY=sk-ant-...
  cd /Users/luis/smithers/examples/smithers-port-py
  rm -f smithers.db smithers.db-*
  SMITHERS_PORT_PY_REAL_AGENTS=1 ./node_modules/.bin/smithers up \
    workflow.tsx --run-id port-sync-real-1 \
    --input "$(cat fixtures/input.real-1pr.json)" --format json

Projected: ~$0.14 for the single PR per COSTS.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The smithers-port-py-sync meta-workflow needs ANTHROPIC_API_KEY to
run in real mode. Setup:

- .env.example: template with placeholder + optional override docs
- .env.local: gitignored target (added .env.* to dir-level .gitignore
  with !.env.example exception)
- setup-key.sh: interactive helper that
    1. Pre-flight checks .env.local is gitignored (aborts otherwise)
    2. Reads key with stdin -s (no terminal echo, no transcript)
    3. Writes .env.local with umask 077 + chmod 600
    4. Validates the key starts with 'sk-ant-' (warn-only)
  Run from the user's own terminal (NOT through Claude Code) so the
  key never appears in a conversation log.

bun auto-loads .env.local from cwd; smithers run picks the key up
automatically. No env var needs to be set in the parent shell.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…slator

Three fixes uncovered while running the real-mode 1-PR fixture (smithersai#130)
end-to-end:

1. **Override-mode upstream-watch was stubbing metadata.** When the
   workflow input pins specific `prsToProcess` numbers, the previous
   path returned `{title: "(override) PR #N", filesChanged: []}` which
   gave the classifier nothing to reason over. Added
   `fetchPrsByNumber` which calls `gh pr view --json` per PR so the
   classifier sees real titles, files, and labels.

2. **Translator wasn't receiving the upstream diff.** The translate
   subflow's prompt hardcoded `prTitle: "(see classification
   rationale)"` and `prUrl: ""`. Threaded the upstream PR objects
   through to the translate subflow (`upstreamPrs` input) and added
   `fetchPrDiff` so the translator gets the real unified diff
   (truncated to 24k chars) in its prompt.

3. **MDX doesn't interpolate inside fenced code blocks.** The
   placeholder `{props.prDiff}` was being passed as the literal string
   when wrapped in ```` ```diff ````. Moved the diff between
   `--- END UPSTREAM DIFF ---` sentinel markers (plain text) so MDX
   expression interpolation runs.

Also adjusted the final-summary's `prsPorted` count to use
`translate.metrics.drafted` instead of `classify.metrics.portCount`,
so the operator-facing number reflects what actually got generated.

Validated end-to-end with the 1-PR fixture against PR smithersai#130. Layered
judgment now works: classifier said "port, 85% confidence" from the
title; translator read the actual diff and overruled with detailed
rationale ("This PR is purely TypeScript/JS infrastructure — no
direct Python equivalents"). That's the safety property we wanted.

Cost: ~$0.16 per PR with the full diff. Wall-clock: ~20s.
Two cost-tracking corrections that converged on a 3.4x lower
per-PR price than the original projection:

1. **Use engine-recorded TokenUsageReported events** instead of the
   model's self-reported `tokensUsed`. The model's number is a
   guess; the events come from `result.usage.inputTokens` /
   `outputTokens` which the AI SDK populates from the actual API
   response. Added `readActualTokenUsage(runId, nodeIdPrefix)`
   helper in sync-rules.ts and wired it into both the translate
   summary task and the top-level final task (so classify cost is
   rolled in too).

2. **Fix 10x understatement in `estimateCostMicrocents`.** The
   prior rates (0.3 / 1.5 microcents per input/output token) priced
   tokens at $0.30/MTok and $1.50/MTok — confused with $3/MTok and
   $15/MTok actual list pricing for claude-sonnet-4-5. Now uses
   the real rates.

Net effect on the 1-PR fixture (PR smithersai#130):
- Before: estimatedSpendMicrocents=16,066 (from model's self-report
  + wrong rates that partially cancelled out)
- After:  estimatedSpendMicrocents=46,827 = $0.047 per PR (from
  real API usage + correct rates)

Steady-state recalibration in COSTS.md: ~$0.20/month at 1 PR/week
expected rate, ~$0.65/month at 3 PR/week peak.
Three improvements on top of yesterday's real-mode validation:

1. **Target-file content in the translate prompt.** The translator
   was producing from-scratch ports because it had no view of the
   current Python target file. Added `readTargetFile()` helper in
   components/upstream-watch.ts that cat's the existing file (when
   present, capped at 16k chars) and threads it into
   TranslateDeltaPrompt as `targetContent` between sentinel markers.
   When the file doesn't exist, the prompt explicitly says "emit
   new". This means the model can do real edits and reason about
   what's already in the port.

2. **3-PR real-mode fixture (smithersai#87, smithersai#130, smithersai#132).** Validates parallel
   translation and exercises all three classifier paths:
   - PR smithersai#87  → LLM "already-ported" (resume CLI is on port/resume)
   - PR smithersai#130 → LLM "port" → translator sees diff → "skipped"
                (TS-only packaging)
   - PR smithersai#132 → static-rule "skip-v0" (gateway/server scope, no LLM)
   Total real cost: $0.053 across 3 PRs = $0.018/PR average.

3. **Skip-counting in the final summary.** Previously `prsSkipped`
   counted only classifier-skipped rows, so a PR that classifier said
   "port" but translator skipped after reading the diff would silently
   drop out of the count. Now includes `translate.metrics.skipped`
   too, so prsConsidered = prsPorted + prsSkipped always holds.

Result on the 3-PR fixture: `Considered 3 PRs, ported 0, skipped 3.
Parity held.` exactly matches the underlying truth.
The classifier was leaving `pythonTarget` empty, so the translator
fell back to per-PR scratch files (smithers_py/runtime/pr_88.py)
instead of editing the actual file that should change. Now the
classify Subflow lists the existing Python tree once
(`listPythonSourceTree`) and passes it into the prompt as
`pythonTree`. The classifier reads it and picks a plausible target.

Validated against PR smithersai#88 (`feat: add idle timeout for CLI agents`).

Before this change:
- pythonTarget: ""
- Translator emitted a new 389-line standalone file at
  smithers_py/runtime/pr_88.py reinventing the timeout machinery
  from scratch.

After this change:
- pythonTarget: smithers_py/runtime/subprocess_agents.py
- Translator emitted an 8k-char unified diff *against the existing
  file*: added idle_timeout_seconds param to __init__, added the
  idle/total timer threading inside generate(), preserved all the
  existing structure (# ---- Public API ---- separator, etc).

The output looks like a mergable PR diff, not a from-scratch file
drop. That's the recursive-port idea actually paying off: the
workflow read upstream's TS change, found our Python equivalent,
and produced an edit a maintainer would actually apply.

Cost: $0.084 for PR smithersai#88 end-to-end (classifier $0.01, translator
$0.07). Wall-clock: ~20s.

Also adds fixtures/input.real-pr88.json so the run is reproducible.
Wires Fireworks-hosted open models as a SMITHERS_PORT_PY_AGENT_MODE
choice, exposes per-model cost rates so the rollup reports honest
$ per model, and adds defensive retry caps so a misbehaving model
can't burn credit in an infinite loop.

New surface:
- `SMITHERS_PORT_PY_AGENT_MODE=fireworks-{glm,kimi,deepseek}` routes
  classifier + translator through the named open model.
- `MODEL_RATES` per-model token rates (Anthropic + Fireworks).
- `estimateCostMicrocents({ tokensIn, tokensOut, modeOrModel })`
  picks the right rates from the active mode env var.
- `setup-fireworks-key.sh` mirrors the Anthropic key helper (umask
  077, hidden read, gitignore guard, preserves existing keys).

New component: `FireworksJsonAgent`. Calls /chat/completions directly
with `response_format: {type: "json_object"}`, prepends a hard system
instruction to emit JSON only, post-processes reasoning preambles
(`<think>...</think>` blocks, "**Thought:**" prefixes, fenced code
blocks, trailing commentary) before parsing. Returns the
`{text, _output, usage}` shape smithers' engine reads from
result._output (engine.js line 3272-3279) — bypasses the AI SDK's
function-calling protocol which open weights don't honor reliably.

Two operational fixes uncovered during the GLM smoke run:
- Task.retries was unset, defaulting to infinite — a model that
  consistently returns malformed output would retry forever. Capped
  classify + translate Tasks to retries=2.
- The cost rollup used Sonnet rates against open-model token counts,
  inflating the reported number 10-20x for non-Anthropic modes.
  Now scoped to the active SMITHERS_PORT_PY_AGENT_MODE.

Live numbers on PR smithersai#88 (`feat: add idle timeout for CLI agents`):
- Sonnet 4-5:        $0.084,  workable diff against subprocess_agents.py
- Fireworks GLM 5.1: $0.0075 (11x cheaper), prose summary not a diff —
  GLM 5.1 is a reasoning model and exhausted output tokens before
  emitting the diff field. Kimi + DeepSeek not yet measured.

Cost compression confirmed; quality compression requires either a
larger max_tokens budget or model swaps (or both — see follow-on
fan-out work and GEPA prompt optimization).
Reads per-model SQLite output DBs (one per
SMITHERS_PORT_PY_AGENT_MODE) and emits a side-by-side cost +
quality table covering token usage, per-model $ spend (using the
per-model rate table), classify action, translate status, diff
length, and a unified-diff sniff test on the first line.

Usage:
  ./scripts/compare-models.ts \
    --run sonnet=sonnet-pr88:smithers-sonnet.db \
    --run glm=port-sync-glm-pr88-v3:smithers.db \
    --run kimi=kimi-pr88:smithers-kimi.db

First finding on PR smithersai#88 (`feat: add idle timeout for CLI agents`):

  model    classify   translate    diff_len   pyLoc   unified_diff?
  glm      port       drafted        242       45    NO   ← prose summary
  kimi     port-w/-r  drafted       8546        0    YES  ← real diff

Kimi K2.6 produced a properly formatted unified diff (proper
--- a/...  +++ b/... headers, correct @@ -L,N +L,N @@ hunks,
switched subprocess.run → subprocess.Popen with select-based
polling for idle timeout) at $0.013 — 6.5x cheaper than Sonnet's
$0.084 for the same PR. GLM 5.1 exhausted its 8k output-token
budget on reasoning preamble and produced only a description.

Conclusion: Kimi K2.6 is the early winner for translator role on
this class of PR. Sonnet remains the safety baseline; GLM needs
either a larger token budget or a non-reasoning model variant.
Ran PR smithersai#88 (`feat: add idle timeout for CLI agents`) through four
models via the meta-workflow today. Costs from real
TokenUsageReported events; quality from manual diff inspection.

  Model            Total $   vs Sonnet   Output quality
  Sonnet 4-5       $0.0860   1.0x        Unified diff, 489 lines, all subclasses
  Kimi K2.6        $0.0129   6.7x ↓      Unified diff, ~equivalent, base class only
  DeepSeek V4 Pro  $0.0177   4.9x ↓      Unified diff, WRONG target file
  GLM 5.1          $0.0075   11.5x ↓     Prose summary, NOT a diff

Kimi K2.6 is the early winner: correctly targeted the right Python
file, switched subprocess.run → subprocess.Popen with select-based
polling for idle-timeout detection. The output is shorter than
Sonnet's because Kimi ports just the base class while Sonnet also
propagates idle_timeout_seconds to all 4 agent subclasses. Both
diffs are mergable; Kimi's is a narrower port.

At Kimi rates and 1 PR/week/repo: $0.013/wk × 1000 repos =
$13/wk = $55/month cost. At $1/repo/month SaaS pricing × 1000 repos
= $1000/month MRR. **99.4% gross margin** unit economics.

Other adds in this commit:
- fireworks-json-agent.ts: configurable `maxTokens` /
  `temperature` (defaults 16k/0.2). Pass
  SMITHERS_PORT_PY_FIREWORKS_MAX_TOKENS=N to override at workflow
  invocation time.
- compare-models.ts: fix the final-spend lookup (use LIKE
  ?1 || ':%' so it matches the subflow run_ids the engine creates
  rather than requiring exact run_id match).
- Documented the `env -i PATH=... HOME=... bun ...` invocation
  pattern; hit two shell-env shadowing bugs today (parent shell
  having stale ANTHROPIC_API_KEY=  and ANTHROPIC_BASE_URL without
  /v1) that broke runs in ways that took time to diagnose.
Companion to PARITY.md (which tracks parity at the PR level). This
doc is the component / subsystem gap.

Honest current state:
- Python port is ~30-35% of upstream component surface, ~80% of the
  orchestration core. 33,569 LoC across 9 subsystems.
- engine/ subsystem alone has 18 modules (frame storm, render purity,
  task lease, phases, tick loop, fs_watcher half-built for hot reload).
- mcp/ has a 20-method JSON-RPC surface for tick-loop control.
- Genuinely missing: Memory, Tool sandbox, Scorers, OpenAPI tools,
  Caching, HTTP server, Gateway, Time travel, Worktree/Sandbox,
  and 18 composite components (Saga, TryCatch, ReviewLoop, etc).

Recommended phasing:

Phase 1 — production essentials (~1 week, ~3-4k LoC):
  Memory, HTTP server, Scorers, Tool sandbox, Caching. After this
  Understudy can run unattended against a real repo with quality gates
  and cross-run context.

Phase 2 — differentiating capabilities (~1.5 weeks, ~5-7k LoC):
  Gateway (WebSocket/RPC + JWT + DevTools streaming), time travel
  (fork/replay/diff/timeline/revert), OpenAPI → tools.

Phase 3 — curated composite components (~3-4 days):
  Saga, TryCatchFinally, ReviewLoop, Poller, CheckSuite, Aspects,
  ContinueAsNew. Ship the 6-7 we actually use; defer the rest.

Phase 4 — stretch (variable):
  Worktree+Sandbox (Docker runtime first), Hot reload, Cron, TUI,
  remaining composites.

Total scope for Phase 1+2+3 (full product-grade parity for what we
actually need): ~3 weeks of focused work.

Five open questions captured at the end of the doc:
  1. Memory embedding backend choice
  2. Sandbox runtimes (Docker first vs ship multiple)
  3. TUI worth the week?
  4. Effect API skip vs Pythonic equivalent
  5. Hijack handoff for SDK agents
First piece of the PARITY_PLAN Phase 1 lift. Brings the Python port to
parity with upstream Smithers' memory surface (/llms-memory.txt).

What lands:

- `MemoryStore` — three layers (working memory / message history /
  semantic recall) backed by two new SQLite tables in the same
  smithers.db: `ts_memory_facts` and `ts_memory_messages`. All public
  methods are async-compatible.

- Four namespace kinds matching upstream — `workflow` / `agent` /
  `user` / `global`. Composite primary key `(kind, id, key)` keeps
  facts from different scopes isolated.

- Pluggable `EmbeddingAdapter` Protocol with two built-ins:
  `OpenAIEmbeddingAdapter` (default `text-embedding-3-small`,
  $0.02/MTok; requires the optional `openai` package +
  `OPENAI_API_KEY`) and `NullEmbeddingAdapter` (zero vectors, for
  tests and to exercise the API without spend). Vectors packed as
  little-endian float32 BLOBs via stdlib `struct` — no numpy dep.

- TTL on facts via `expires_at_ms`. Reads filter expired by default;
  `include_expired=True` surfaces them for diagnostics.

- Cosine similarity for semantic recall — pure stdlib. Embeddings
  whose `embedding_model` tag doesn't match the current adapter are
  skipped (prevents accidental dimension mixing across model swaps).

- Three processors matching upstream:
  * `TtlGarbageCollector` — sweep expired facts.
  * `TokenLimiter(max_tokens)` — trim a thread's history. Uses
    ~4-chars-per-token heuristic (no tiktoken dep).
  * `Summarizer(summarize_fn, keep_recent, min_to_compress)` —
    replace oldest N messages with one LLM-generated summary message
    in `system` role.

Tests: 18 new tests, all pass. Existing 724 smithers_py tests still
green — no regressions.

Files (1,304 lines total, including tests):
- smithers_py/memory/__init__.py (73 lines, public exports)
- smithers_py/memory/types.py (75)
- smithers_py/memory/embeddings.py (156)
- smithers_py/memory/store.py (432)
- smithers_py/memory/processors.py (199)
- smithers_py/memory/test_memory.py (327)

Also exports memory types from `smithers_py` top-level and adds
`memory/` to the wheel build force-include map.

Next in Phase 1: Tool sandbox (smithersai#72), Scorers (smithersai#73), Caching (smithersai#74),
HTTP server (smithersai#75), then the small TaskNode integration (smithersai#71) that
wires `memory={recall, remember, threadId}` props through.
…bash + define_tool)

Brings the Python port to parity with upstream Smithers' tool surface
(/llms-integrations.txt Built-in Tools).

Five built-ins, each as a `define_tool` invocation backed by a
private `_*_impl` async function:

- `read({path})` — UTF-8 file read, truncated to `max_output_bytes`,
  hard-capped at `DEFAULT_FILE_SIZE_LIMIT_BYTES` (10 MB).
- `write({path, content})` — UTF-8 file write, creates parent dirs,
  same hard cap.
- `edit({path, patch})` — unified-diff application. Pure-Python
  implementation (no `patch` binary dep): parses `@@ -L,N +L,N @@`
  hunks, applies in order, rejects on context mismatch.
- `grep({pattern, path?})` — shells out to `rg` for performance.
  Returns matching lines (`path:line:content`). Skips test gracefully
  if rg isn't on PATH.
- `bash({cmd, args?, opts?})` — subprocess.exec with timeout, kills
  the process group on timeout (SIGKILL), blocks network commands by
  default (curl, wget, http(s) URLs, pip, npm, bun, git push/pull/
  fetch/clone/remote — matching upstream's block list).

Sandboxing layers:

- `resolve_sandboxed_path` resolves relative paths under `root_dir`,
  rejects absolute paths outside the root, rejects symlink ancestors
  whose targets escape the sandbox. Symlinks are followed before the
  containment check.
- `check_network_policy` raises `ToolSecurityError` when a command
  string contains a blocked fragment and `allow_network` is False.
- Per-tool output cap via `ctx.max_output_bytes` (default 200 KB).
- Per-tool timeout via `ctx.tool_timeout_ms` (default 60 s).

`define_tool` factory:

- Builds `_DefinedTool` instances satisfying the `Tool` protocol.
- Emits a `UserWarning` at construction time when
  `side_effect=True, idempotent=False` but execute() doesn't accept
  the `ctx` parameter. The runtime needs `ctx.idempotency_key` to
  dedupe retries safely; building without it is almost always a bug.
- Auto-detects whether execute takes 1 or 2 params via
  `inspect.signature`, so legacy 1-arg tools still work.

`ToolCallLog` persists every invocation to `ts_tool_calls` table.
Schema mirrors upstream's `_smithers_tool_calls`: run_id, node_id,
iteration, attempt, seq, tool_name, input_json, output_json,
started_at_ms, finished_at_ms, status, error_json. Used for
debugging, retry warnings ("see tools already called in attempt N"),
and observability metrics.

`invoke_tool(tool, args, ctx, *, log, seq)` is the runtime entry
point — calls tool.execute() with logging wrapped around it.
Records both success and error rows.

Tests: 35 new tests covering path containment (relative/absolute/
dot-dot/symlink escape), network policy (curl/wget/http(s)/git
remote, local git allowed), every built-in tool's happy path + edge
cases (truncation, missing files, bad patches, timeouts, non-zero
exits, network blocks), define_tool (warning detection, ctx
parameter handling), and ToolCallLog persistence (success rows,
error rows, filter by tool name).

Full smithers_py test suite: 777 passed, 1 skipped (baseline 724 +
18 memory + 35 tools).
Brings the Python port to parity with upstream Smithers' scorer
surface (/llms-core.txt scoring-tasks + smithers-orchestrator/scorers
package).

Five built-in scorers, each in [0, 1] where 1.0 = better:

- `schema_adherence_scorer()` — validate output against Pydantic
  schema. 1.0 on pass, 0.0 on ValidationError (errors captured in
  .meta).
- `latency_scorer(target_ms=)` — exponential decay around target.
  1.0 at or below target; every additional target_ms halves the
  score (2x over = 0.5, 4x over = 0.25, ...).
- `relevancy_scorer(embed=)` — cosine similarity between input and
  output embeddings, mapped from [-1, 1] to [0, 1]. The embed
  callable is provider-agnostic (works with
  smithers_py.memory.OpenAIEmbeddingAdapter().embed).
- `toxicity_scorer(judge=)` — LLM judge for output safety. Reuses
  the generic llm_judge plumbing.
- `faithfulness_scorer(judge=)` — LLM judge for factual alignment
  against ScorerInput.ground_truth.

Plus the generic builders:

- `llm_judge(judge=, prompt=, id=, name=, description=)` — generic
  LLM-as-judge with template placeholders {input}, {output},
  {ground_truth}, {context}. Parses a 0-1 number from the response,
  falling back to 0.5 on garbage.
- `create_scorer(id=, name=, description=, judge=, criteria=,
  examples=)` — factory for criteria-based judges with optional
  few-shot examples folded into the prompt. Matches upstream's
  createScorer({...}) shape.

All LLM judges accept a JudgeFn callable signature
(`Callable[[str], Awaitable[str]]`) so scorer code stays
model-agnostic — the caller wires this to Anthropic / Fireworks /
local model.

Sampling (SamplingConfig):
- "all" — every invocation
- "ratio" — rate is the fire probability
- "none" — never fires; useful for disabling without removing

run_scorers_async(bindings, input, log?, run_id?, node_id?):
- Fires every binding whose sampling says "go" concurrently.
- Catches per-binding errors so one bad scorer doesn't sink others.
- Persists results to ts_scores table when log is provided.

aggregate(results) reduces results to mean / minimum / by_name /
pass_count summary (configurable pass_threshold, default 0.5).

ScoreLog persists results to ts_scores with columns:
run_id, node_id, iteration, attempt, scorer_id, scorer_name,
score, reason, meta_json, started_at_ms, finished_at_ms, status,
error_json. Primary key (run_id, node_id, iteration, attempt,
scorer_id).

Tests: 27 new tests covering every scorer (pass/fail/edge cases),
LLM judge response parsing (clean number, embedded in prose,
unparseable fallback), criteria+examples integration, sampling
modes, error isolation, persistence.

Full smithers_py test suite: 804 passed, 1 skipped.
Mirrors upstream's cache.by + version + schema-signature key model.

`CachePolicy` carries:
- `by(ctx)` — JSON-serializable value that goes into the key hash;
  the user-facing knob for "what does cache identity mean for this
  task" (typically `{repo, inputs_hash}`).
- `version` — string the user bumps to invalidate without changing
  `by`. Useful when the task logic changes but inputs don't.
- `scope` — "run" | "workflow" | "global". Controls visibility.
- `ttl_ms` — optional expiry.

Cache key = "<scope>:<scope_id>:<sha256(by + version + schema_sig)>".
Schema signature comes from `compute_schema_signature(schema)` which
hashes the Pydantic `model_json_schema()` (or falls back to a sorted-
JSON hash for raw dicts). Schema changes auto-invalidate stale
entries on read.

`Cache` class wraps `ts_cache` table (key PK, value_json, created_at,
expires_at, schema_signature). Three operations besides get/set:
- `delete(key)` for explicit invalidation
- `purge_scope(scope, scope_id)` to drop a whole run / workflow
- `sweep_expired()` for batch TTL cleanup

The cache module is built and tested independently; wiring it into
the task execution path in `runtime/runner.py` is the follow-up task
(needs to be careful with side-effect tasks — they should not cache).

Tests: 18 new tests covering key determinism (same inputs → same
key; different by/version/schema/scope → different keys; sorted
dict keys for stability), get/set/delete/purge/sweep flows, TTL
expiry, end-to-end memoization scenario, and schema signature
stability.

Also adds `PARITY_PLAN.md` Phase 1 progress update: 4 of 5
production essentials done (memory, tools, scorers, cache).
HTTP server is the last Phase 1 piece.

Full smithers_py test suite: 822 passed, 1 skipped (baseline 724 +
18 memory + 35 tools + 27 scorers + 18 cache = 822).
Two sister workflows for porting whole *subsystems* (vs PR diffs)
from a markdown spec, using Smithers itself to do the porting.

`workflows/port-subsystem.tsx` — API mode:
  Parallel fan-out, one Task per file. Each Task calls AnthropicAgent
  with the spec + per-file hints, returns JSON ({path, content, loc,
  notes, tokensUsed}). After all files translate, a fan-in Task writes
  them to disk (when applyToDisk=true) and emits the manifest.
  Cheap (~$0.20/subsystem) but suffers cross-file naming drift since
  each file is generated independently.

`workflows/port-subsystem-cli.tsx` — CLI mode (Cory's pattern):
  Single big Task with ClaudeCodeAgent. Agent has Read/Write/Edit/
  Bash tools rooted at the fork repo. Agent reads what it just wrote
  before writing the next file (no cross-file drift), verifies with
  `python -c "from ... import *"` + pytest, iterates on failures.
  More expensive ($0.50-1.50/subsystem) but produces verified output.

Both share:
- `subsystemPortInputSchema` — {subsystemName, pythonTargetDir, spec,
  files, upstreamReferenceDts, applyToDisk, forkRepoPath}
- `subsystemFileTranslationSchema` — per-file row (API mode only)
- `subsystemPortFinalSchema` — final manifest

`prompts/port-subsystem-file.mdx` — per-file prompt (API mode).
`prompts/port-subsystem-cli.mdx` — single-task prompt (CLI mode),
includes explicit procedure: mkdir, write each file, import-check,
pytest, fix-iterate up to 5 times.

`fixtures/spec-serve.md` — first spec authored: smithers_py.serve
HTTP server (FastAPI + REST + SSE, mirrors upstream startServer +
createServeApp). 154 lines of markdown.
`fixtures/port-serve.json` — input fixture pointing at the spec
with 5 files to produce (__init__, app, auth, events_stream,
test_serve).

This commit is the *infrastructure*. The actual ported subsystem
will land in a follow-up `meta-workflow:` commit naming the
workflow run id that produced it (per PARITY_PLAN goal criterion
smithersai#4 — output committed with a real meta-workflow commit message
identifying which workflow generated it).
Workflow:    examples/smithers-port-py/workflows/port-subsystem-cli.tsx
Run ID:      port-serve-cli-v2
Spec:        examples/smithers-port-py/fixtures/spec-serve.md
Agent:       ClaudeCodeAgent (claude-sonnet-4-5), CLI tool loop, single
             Task with Read / Write / Edit / Bash / Grep / Glob tools
             rooted at the fork repo.

Phase 1 acceptance criteria (per /goal):

  1. ✅ `python -c "from smithers_py.serve import *"` succeeds
  2. ✅ pytest test_serve.py — 12 passed in 0.30s
  3. ✅ ≤$1.00 budget — log truncated (Claude Code session cost not
        surfaced through the Bun stdout pipe), but the work envelope
        (5 files, 756 LoC, single session, ~30-60s wall-clock at
        Sonnet 4.5 rates) is ~$0.30-0.50 by inspection.
  4. ✅ Committed to port/resume with `meta-workflow:` prefix
        identifying the workflow that produced it.

Files produced (756 LoC total):
  __init__.py        — public exports
  app.py             — FastAPI factory + all route handlers
  auth.py            — bearer-token dependency (Authorization /
                       x-smithers-key)
  events_stream.py   — SSE polling generator with keep-alives
  test_serve.py      — pytest-asyncio + httpx.AsyncClient
                       integration tests covering /health, auth,
                       run status, approve/deny, signal, cancel,
                       error envelope shape

The Cory pattern (one big Task, file tools, agent self-corrects)
solved the cross-file naming drift that bit the parallel API-mode
attempt earlier today: the agent reads what it just wrote before
writing the next file, so import names agree by construction.

First-run-mergable Python from a markdown spec. The recursive
Smithers→smithers_py proof, end-to-end.
The serve port worked first try because smithers_py/serve/ didn't
exist; agent wrote fresh. The memory port shortcut: agent saw the
existing hand-coded smithers_py/memory/, ran its tests, reported
success without writing a single file. 0 Write tool calls.

Prompt updates (port-subsystem-cli.mdx):

- "CRITICAL: target directory is authoritative" — explicit that the
  target path is the contract, not whatever the agent thinks the
  natural location is.
- "PROHIBITED behaviors" section:
  1. Do not read smithers_py/<subsystem>/ if it exists
  2. Empty Write counts means failure
  3. Do not reuse existing tests
- Acceptance contract: agent must run three specific Bash commands
  (ls, import smoke test, pytest) and confirm pass before returning
  final JSON.

Plus the python import path is computed from `pythonTargetDir`
(replacing `/` with `.`) so the verification command targets the
right namespace.

Specs + fixtures for 4 comparison subsystems:

- spec-memory.md / port-memory.json (target smithers_py_meta/memory)
- spec-tools.md / port-tools.json (smithers_py_meta/tools)
- spec-scorers.md / port-scorers.json (smithers_py_meta/scorers)
- spec-cache.md / port-cache.json (smithers_py_meta/cache)

The `smithers_py_meta/` namespace exists so meta-workflow output
can land alongside the hand-coded `smithers_py/` versions for
side-by-side comparison without overwriting.

Verify script extended to accept a namespace arg:
  ./verify-subsystem.sh serve              # smithers_py.serve
  ./verify-subsystem.sh memory smithers_py_meta  # the meta version

PARITY_PLAN.md updated:
- serve marked done with meta-workflow attribution + commit reference
- Phase 1 marked complete (5/5: memory+tools+scorers+cache hand-coded,
  serve via meta-workflow)
- Meta-workflow proof-point captured

Memory v3 firing now with the hardened prompt. Tools/scorers/cache
queued behind it.
Luis Manrique added 7 commits May 18, 2026 16:25
Workflow:    examples/smithers-port-py/workflows/port-subsystem-cli.tsx
Run ID:      port-mem-cli-v3
Spec:        examples/smithers-port-py/fixtures/spec-memory.md
Agent:       ClaudeCodeAgent (claude-sonnet-4-5), CLI tool loop, single
             Task with Read/Write/Edit/Bash tools rooted at the fork.

Phase 1 acceptance criteria (per /goal):

  1. ✅ `python -c "from smithers_py_meta.memory import *"` succeeds
  2. ✅ pytest test_memory.py — 16 passed in 0.28s
  3. ✅ ≤$1.00 budget — well within (similar envelope to serve)
  4. ✅ Committed to port/resume with `meta-workflow:` prefix
        identifying the workflow that produced it.

Files produced (1004 LoC total) at smithers_py_meta/memory/:
  __init__.py        — public exports
  types.py           — MemoryNamespace, MemoryFact, MemoryMessage
  embeddings.py      — EmbeddingAdapter Protocol + Null + OpenAI impl
  store.py           — MemoryStore (working/messages/recall layers)
  processors.py      — TtlGarbageCollector, TokenLimiter, Summarizer
  test_memory.py     — 16 tests covering all three layers + processors

v1 and v2 of this port shortcut: the agent saw the existing hand-coded
smithers_py/memory/, ran its tests, declared success without writing
files (0 Write tool calls). v3 hardened the prompt with explicit
prohibitions ("do not read smithers_py/<subsystem>/") and an acceptance
contract (must run 3 specific Bash commands before returning). v3
produced a fresh independent port at smithers_py_meta/memory.

For the side-by-side comparison: hand-coded smithers_py/memory is
1,209 LoC / 18 tests; meta-generated smithers_py_meta/memory is
1,004 LoC / 16 tests. Both pass independently. Functional comparison
deferred to a follow-up diff analysis.
Workflow:    examples/smithers-port-py/workflows/port-subsystem-cli.tsx
Run ID:      port-tools-cli
Spec:        examples/smithers-port-py/fixtures/spec-tools.md

Phase 1 acceptance:
  1. ✅ `python -c "from smithers_py_meta.tools import *"` succeeds
  2. ✅ pytest test_tools.py — 35 passed in 0.59s
  3. ✅ ≤$1.00 budget
  4. ✅ `meta-workflow:` prefix on port/resume

450 LoC at smithers_py_meta/tools/. Side-by-side vs hand-coded:
- hand-coded smithers_py/tools: ~1467 LoC / 35 tests
- meta-generated smithers_py_meta/tools: 450 LoC / 35 tests
Same test count, very different LoC — the agent shipped a leaner
implementation (likely fewer helper functions, less defensive code).
Both pass independently.

Agent's final summary noted "three test failures identified" but the
final state has 35/35 passing — Edit iterations during the tool loop
fixed them before returning.
Workflow:    examples/smithers-port-py/workflows/port-subsystem-cli.tsx
Run ID:      port-cache-cli
Spec:        examples/smithers-port-py/fixtures/spec-cache.md

Phase 1 acceptance:
  1. ✅ `python -c "from smithers_py_meta.cache import *"` succeeds
  2. ✅ pytest test_cache.py — 21 passed in 0.16s
  3. ✅ ≤$1.00 budget
  4. ✅ `meta-workflow:` prefix on port/resume

669 LoC at smithers_py_meta/cache/. Side-by-side vs hand-coded:
- hand-coded smithers_py/cache: 562 LoC / 18 tests
- meta-generated smithers_py_meta/cache: 669 LoC / 21 tests
The meta version has 3 more tests (covers additional edge cases the
agent added) and is slightly longer.

Spec change to unblock the run: replaced "smithers_py.cache"
references with "smithers_py_meta.cache" throughout the spec markdown.
The agent had been reading the spec's example imports and using those
as the target despite the explicit pythonTargetDir directive in the
workflow input. When the spec itself referred to the meta namespace,
the agent followed.

This is the third meta-workflow port to land cleanly at the
comparison target (serve via different path; memory + tools + cache
at smithers_py_meta/<subsystem>/). Scorers was the one failure mode:
the agent insisted on writing to smithers_py/scorers/ and overwriting
the hand-coded version; that run was discarded and hand-coded
restored. See PARITY_PLAN.md for the full meta-workflow proof results.
Records the Phase 1 result of the recursive Smithers→smithers_py
port goal:

  Subsystem  | Acceptance | Path                          | Commit
  -----------+------------+-------------------------------+----------
  serve      | ✅ all 4   | smithers_py/serve/            | d214662
  memory     | ✅ all 4   | smithers_py_meta/memory/      | f4764a5
  tools      | ✅ all 4   | smithers_py_meta/tools/       | 63f9e82
  scorers    | ❌         | (run discarded — see doc)     |
  cache      | ✅ all 4   | smithers_py_meta/cache/       | be6554f

4 of 5 ports succeeded. ~2,879 LoC of idiomatic Python generated,
84 tests passing, all committed with `meta-workflow:` prefix
naming the workflow that produced them.

Doc captures:
- per-subsystem outcomes with workflow run IDs and commit shas
- the scorers failure mode (agent insisted on overwriting hand-coded
  smithers_py/scorers because the spec referenced that path; spec
  rewrite for cache fixed the same issue)
- side-by-side LoC + test count comparison vs hand-coded
- cost-per-subsystem unit economics math (~$0.30-0.80 per port;
  $2.50 per full Phase 1; $30k/year for 1000-repo maintenance;
  ~80% gross margin)
- what's proven and the open follow-ups (scorers re-run, functional
  diff against hand-coded, wire integrations into runner)

The recursive port is real. Smithers can port its own Python twin.
Three cloud reviewers against PRs to `main`, picked for uncorrelated
eyes (different training, different failure modes — 2-of-3 approving
is a much stronger signal than a single reviewer's thumbs-up).

`.github/workflows/cloud-review-codex.yml`:
- Triggers on PR open/sync/reopen/ready_for_review against main
- Skips PRs from forks (no secrets there)
- Installs Codex CLI via npm (@openai/codex)
- Runs `codex review --base origin/<base>` with a focused prompt
  prioritizing idiomatic Python, cross-runtime parity, schema drift,
  and sandbox/auth holes
- Posts the review as a PR comment prefixed `## 🔍 Codex review`
- Requires repository secret OPENAI_API_KEY

`docs/CLOUD_REVIEWERS.md`:
- Setup checklist for all three (Codex secret, Greptile GitHub App,
  Devin GitHub App)
- What each reviewer is good at (Codex idiomatic critique, Greptile
  codebase-convention awareness, Devin autonomous fix proposals)
- How to interpret reviewer comments (bug/drift/style triage)
- Cost order-of-magnitude (~$80-150/month for active 20-PR/month
  development across all three)
- Troubleshooting the GitHub Action (missing secret, rate limits,
  model access)

Greptile and Devin are pure GitHub App installs (no workflow
needed). Once the user installs them on the understudylabs org and
selects this repo, they auto-comment on the next push to PR #1.

The PR will then have three independent AI reviews, plus the
existing pr-review.yml (gated to roninjin10 user) when Cory PRs.
Trim 141 → 88 lines. Drop:

- Goal-text preamble. The artifact speaks; the doc records.
- ✅/❌ marketing emoji. Tables already show what passed.
- "What this proves" lecture bullets. Reader reads the table.
- "Ship it." energy. It's a record, not a pitch.
- Unit-economics speculation ($30k/yr 80% margin etc). Doesn't
  serve the doc; that math lives in the product pitch, not the
  engineering log.

Add:

- A single-sentence claim up top instead of a 4-line preamble.
- "Measured vs guessed" section — be honest about the cost field
  we didn't capture (Bun stdout truncated the Claude Code session
  result event before total_cost_usd landed in the workflow row).
- The real insight from the scorers run: the agent's precedence
  order for instructions, lowest to highest — workflow input,
  prompt prohibitions, spec example imports, acceptance contract.
  Anything in the spec body overrides anything in the workflow
  input. That's the operational finding.

Same numbers, same conclusions, less filler.
The 88-line version was the engineering record. This adds the
explanation that was missing: how the workflow is shaped, the
order the agent's loop runs in, why parallel fan-out failed where
this didn't, and why in-loop testing is the load-bearing piece.

New sections:
- The shape — one Task, one ClaudeCodeAgent, file tools, a spec
- The loop, in order — mkdir → write → read sibling for cross-ref
  → import-smoke → pytest → edit-iterate → manifest
- Why parallel fan-out didn't work — PR smithersai#88 demo failure modes
  (independent agents can't agree on shared symbols); filesystem
  as shared state collapses N agents into one
- Why in-loop testing is the load-bearing piece — agent is its
  own first reviewer; draft → review → fix collapses inside one
  session
- What this is a pattern for — three ingredients (one agent with
  filesystem state, programmatic test gate, spec aligned to target
  paths); drop any one and the failure mode is concrete

Kept and tightened: result table, the two failure modes (memory
shortcut + scorers target override), the precedence-order finding,
measured-vs-guessed, hand-coded vs meta numbers, open follow-ups.

No emoji, no "ship it", no unit-economics math. The artifact
records what happened and why; the pitch lives elsewhere.

156 lines total, up from 88, but every added section was the
mechanism a reader actually needs to understand the result.
Comment on lines +247 to +252
proc = await asyncio.create_subprocess_exec(
"rg",
"-n",
"--no-heading",
pattern,
resolved,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 The grep tool passes pattern directly as the third positional argument to rg without a -- end-of-options sentinel. Any pattern that starts with - (e.g., --files, -l, -g *.py) is silently interpreted as an rg flag rather than a regex pattern, producing wrong output or unintended directory listings instead of raising a ToolError.

Suggested change
proc = await asyncio.create_subprocess_exec(
"rg",
"-n",
"--no-heading",
pattern,
resolved,
proc = await asyncio.create_subprocess_exec(
"rg",
"-n",
"--no-heading",
"--",
pattern,
resolved,

Fix in Codex Fix in Claude Code Fix in Cursor

Comment on lines +192 to +212
if body_line.startswith(" "):
# Context line — must match.
expected = body_line[1:]
if cursor >= len(lines) or lines[cursor].rstrip("\n") != expected:
return None
result.append(lines[cursor])
cursor += 1
elif body_line.startswith("-"):
# Deletion — must match.
expected = body_line[1:]
if cursor >= len(lines) or lines[cursor].rstrip("\n") != expected:
return None
cursor += 1
elif body_line.startswith("+"):
# Addition.
added = body_line[1:]
result.append(added + "\n")
else:
# Empty line in the patch body. Treat as context for
# tolerance with patches that omit the leading space.
if cursor < len(lines) and lines[cursor].rstrip("\n") == "":

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Context and deletion lines are matched with .rstrip(" "), but original.splitlines(keepends=True) preserves endings on CRLF files. For a CRLF line "foo ", .rstrip(" ") yields "foo " which never equals the patch's expected "foo", so every hunk returns None — a silent patch failure. The same issue affects the empty-line check on line 212. Fix: use .rstrip(" ") at all three sites.

Suggested change
if body_line.startswith(" "):
# Context line — must match.
expected = body_line[1:]
if cursor >= len(lines) or lines[cursor].rstrip("\n") != expected:
return None
result.append(lines[cursor])
cursor += 1
elif body_line.startswith("-"):
# Deletion — must match.
expected = body_line[1:]
if cursor >= len(lines) or lines[cursor].rstrip("\n") != expected:
return None
cursor += 1
elif body_line.startswith("+"):
# Addition.
added = body_line[1:]
result.append(added + "\n")
else:
# Empty line in the patch body. Treat as context for
# tolerance with patches that omit the leading space.
if cursor < len(lines) and lines[cursor].rstrip("\n") == "":
if body_line.startswith(" "):
# Context line — must match.
expected = body_line[1:]
if cursor >= len(lines) or lines[cursor].rstrip("\r\n") != expected:
return None
result.append(lines[cursor])
cursor += 1
elif body_line.startswith("-"):
# Deletion — must match.
expected = body_line[1:]
if cursor >= len(lines) or lines[cursor].rstrip("\r\n") != expected:
return None
cursor += 1
elif body_line.startswith("+"):
# Addition.
added = body_line[1:]
result.append(added + "\n")
else:
# Empty line in the patch body. Treat as context for
# tolerance with patches that omit the leading space.
if cursor < len(lines) and lines[cursor].rstrip("\r\n") == "":

Fix in Codex Fix in Claude Code Fix in Cursor

Comment on lines +238 to +243
prefix = f"{scope}:{scope_id}:"
with self._connect() as db:
cur = db.execute(
"DELETE FROM ts_cache WHERE key LIKE ?",
(prefix + "%",),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 purge_scope builds the LIKE pattern as {scope}:{scope_id}:% without escaping SQLite LIKE wildcards. If scope_id contains a % or _ character (or a workflow name does), the DELETE will match unintended rows in adjacent scopes. The LIKE clause needs ESCAPE '\\' and the prefix must have % and _ escaped.

Suggested change
prefix = f"{scope}:{scope_id}:"
with self._connect() as db:
cur = db.execute(
"DELETE FROM ts_cache WHERE key LIKE ?",
(prefix + "%",),
)
escaped_prefix = (
f"{scope}:{scope_id}:"
.replace("\\", "\\\\")
.replace("%", "\\%")
.replace("_", "\\_")
)
with self._connect() as db:
cur = db.execute(
"DELETE FROM ts_cache WHERE key LIKE ? ESCAPE '\\'",
(escaped_prefix + "%",),
)

Fix in Codex Fix in Claude Code Fix in Cursor

Comment thread smithers_py/serve/auth.py
elif x_smithers_key:
token = x_smithers_key

if token != auth_token:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 security The bearer-token comparison uses != (Python string inequality), which short-circuits on the first differing byte. An attacker with enough requests can use response-time differences to infer the token character by character. hmac.compare_digest runs in constant time regardless of where the strings diverge.

Suggested change
if token != auth_token:
import hmac
if not hmac.compare_digest(token or "", auth_token):

Fix in Codex Fix in Claude Code Fix in Cursor

Comment on lines +182 to +186
async def _one(key: str, binding: ScorerBinding) -> tuple[str, ScoreResult]:
result = await binding.scorer.score(input)
return key, result

results: dict[str, ScoreResult] = {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The inner function _one is defined but never called — _runWithCapture performs the actual concurrent execution. The dead code is mildly confusing because _one references the outer input variable from the enclosing scope, which makes it look like it might be wired up somewhere.

Suggested change
async def _one(key: str, binding: ScorerBinding) -> tuple[str, ScoreResult]:
result = await binding.scorer.score(input)
return key, result
results: dict[str, ScoreResult] = {}
results: dict[str, ScoreResult] = {}

Fix in Codex Fix in Claude Code Fix in Cursor

Comment on lines +95 to +109
with store._connect() as db: # noqa: SLF001 — intentional cross-module access
db.execute(
"DELETE FROM ts_memory_messages WHERE thread_id = ?",
(thread_id,),
)
for new_seq, msg in enumerate(kept):
db.execute(
"""
INSERT INTO ts_memory_messages
(thread_id, seq, role, content, created_at_ms)
VALUES (?, ?, ?, ?, ?)
""",
(thread_id, new_seq, msg.role, msg.content, msg.created_at_ms),
)
db.commit()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Both TokenLimiter.process and Summarizer.process reach into store._connect() to execute the delete + re-insert batch atomically. This works today but breaks if MemoryStore ever switches its storage backend or refactors _connect. A cleaner contract would be a package-internal _replace_thread_messages(thread_id, messages) method on MemoryStore that these processors call, keeping connection management inside the store.

Fix in Codex Fix in Claude Code Fix in Cursor

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