Skip to content

[PILOT] v5.9.0 -- The Artist Verbs: local-first CLI + browser canvas round-trip - #98

Merged
JosephOIbrahim merged 16 commits into
masterfrom
forge/cli-orch
Jul 18, 2026
Merged

JosephOIbrahim merged 16 commits into
masterfrom
forge/cli-orch

Conversation

@JosephOIbrahim

@JosephOIbrahim JosephOIbrahim commented Jul 18, 2026 •

Copy link
Copy Markdown
Owner

v5.9.0 — The Artist Verbs: local-first CLI surface + browser canvas round-trip

Executes the ratified CLI-ORCH harness end-to-end: five artist verbs, all
offline for core operation (loopback-only to the local ComfyUI, zero accounts,
zero API keys on any verb rail), plus the canvas round-trip that hands the
graph to the artist in the browser and takes their edits back as validated,
undoable patches.

The surface

Verb Commands What the artist gets
FIND cozy models list · cozy nodes list · cozy find Disk inventory with ✓/✗/⚠ health marks; command palette
INTEND cozy run --recipe dreamier|sharper|… Name the goal, not the numbers — validated patches, keyless rail
SEE cozy see Braille step-time sparkline, slowest nodes, VRAM bar — even on failed runs
OWN cozy doctor · cozy stats · cozy model search One-key health sweep (CI-gateable), on-device numbers, local search
OPEN cozy open · cozy pull Session → browser canvas; artist edits (adds/deletes/rewires) → one validated patch, one undo away

A cross-process session sidecar makes the round-trip real across separate
cozy invocations: workflow, undo history, and baseline survive between
commands via an atomic snapshot.

Verification

  • Full mocked suite: 5083 passed, 0 failed (from a 4821 baseline; +262
    tests across engines, CLI wiring, and regression guards). Ruff clean.
  • Adversarial pre-merge review (25 agents, 5 lenses, 2 skeptics per
    finding): 22 raw findings → 10 confirmed (8 distinct, 4 critical), all with
    live repros — then fixed in 4 file-disjoint lanes and re-verified 8/8
    dead by re-running the original repros.
    Highlights: element-level patch
    paths flattening into bogus inputs (rewire corruption), process-lifetime
    session state (round-trip broken across invocations), analysis-only loads on
    the recipe rail, UnicodeEncodeError on piped output.
  • Bright-line token scan on every push delta: 0 hits.
  • The 11 pre-existing commands are byte-compatible; everything is additive.
    agent/stage/** untouched.

Deliberate deferrals

  • Bundled offline model index (inherited OQ-4) — model search is local-disk only.
  • Recipe mutation batching to a single undo step (touches agent/recipes).
  • Targeted-tab canvas push (v1 broadcasts to all open tabs, stated in the message).
  • --steps 4 unicode image preview (SEE's remaining slice).

16 commits, engine-per-lane history. Harness: HARNESS_CLI_ORCH_20260718.md
(local control doc); run ledger in tooling/harness/cliorch/LEDGER.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ln2LbRfjDzDgE5Ab6x69Un

Summary by CodeRabbit

  • New Features
    • Added artist-focused CLI commands for discovering models and nodes, applying recipes, opening/pulling workflows, viewing run telemetry, and checking local health and statistics.
    • Added cross-invocation session persistence, including undo history.
    • Added terminal visualizations for progress, timing, and VRAM usage.
    • Added optional progress logs for successful executions.
  • Bug Fixes
    • Improved workflow patching, Unicode output, model-reference reporting, and partial recipe failure messages.
  • Documentation
    • Updated release notes and version to 5.9.0.

Joseph Ibrahim and others added 16 commits July 18, 2026 10:43
…cozy read/test agents

Add an explicit git-prohibition clause to cozy-scout, cozy-provisioner, and
cozy-crucible. These three carry a load-bearing Bash grant (probes / downloads /
pytest) that the tools: frontmatter cannot narrow to "Bash minus git", so an
unrestricted Bash reaches git push despite read-only prose -- the 2026-07-08
push-boundary incident. The clause documents the enforced boundary; conductor-only
orchestration (ORCH.L1) is the actual control. tools: frontmatter left unchanged.

Baseline: 4821 passed, 2 skipped, 0 failed (tests/ -m "not integration").
Working tree equals master @ fb50917; the bootstrap/middleware refactor is parked
in a local stash per OQ-B, not committed. Miles 2-8 remain BLOCKED-BY-FREEZE
pending the demo outcome / PASS 5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…k sparkline, VRAM bar, step times)

Add agent/_render.py -- four stdlib-only, deterministic render functions the SEE
verb will consume: braille_sparkline (dense 2x4-dot cells, optional width via
nearest-neighbor resample), block_sparkline (eighth-block fallback), vram_bar
(clamped [#---] usage bar), format_step_times (count / s-it avg / total). Edge
cases handled explicitly: empty and all-invalid series return "", all-equal maps
to a flat mid level (no divide-by-zero), None/NaN/Inf filtered, width clamped,
negatives normalized or clamped.

Inert by construction: lives outside agent/tools/, exports no TOOLS, absent from
every registry name list -- consumed by nothing until WP-SEE-B wires it into
`cozy run` post-freeze (CLI.L3). 39 tests in tests/test_render.py; collection
intact at 4907.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…te (offline-first)

agent/verbs/find.py -- build_models_report / build_nodes_report / command_palette
with the ratified glyph legend (checkmark installed, x missing, warning attention).
Listings are pure local disk reads reusing the comfy_inspect handlers; the one
liveness seam (workflow missing-node check via find_missing_nodes) is loopback-only
and degrades to a human-worded note when ComfyUI is down. Palette scores by prefix
> substring > difflib, deterministic ordering. Plain-text renderers; structured
dicts left for the CLI layer to dress up. 28 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…enderer

agent/verbs/see.py -- StepTimeCollector subscribes to the TriggerRegistry singleton
as a second consumer (idempotent, observe-only, fail-soft; mirrors the diagnosis
subscriber), deriving step durations from event timestamps rather than the
mixed-clock elapsed_ms. render_run_summary draws braille sparkline + step-time
line + top slow nodes + a vram_bar from one /system_stats poll, degrading in
human words when ComfyUI is down. VRAM-over-time and loss series stay CUT per the
ratified WP-SEE scope. Consumes agent/_render.py as-is. 27 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…wser canvas

agent/verbs/open_canvas.py -- open_canvas() checks ComfyUI liveness, pushes the
session workflow through the existing push_workflow_to_canvas bridge (never
re-implements the POST), then opens the local canvas via webbrowser.open on
COMFYUI_URL (loopback only). Returns a structured status dict whose message
carries the v1 realities -- positions re-laid-out by the frontend importer,
broadcast to every open tab -- in artist words. Push failure degrades to
open-with-message. First half of the canvas round-trip; ingest (pull) is Mile 6.
7 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…wired into the CLI

agent/cli.py -- five additive commands on the existing Typer app: models list /
nodes list (sub-apps, lazy engine imports, degradation notes verbatim at exit 0),
find (palette), open (--no-push; exit 0 iff the canvas opened), see (collector
install -> execute_with_progress -> summary; partial telemetry still renders on
failed runs). The 11 existing commands are untouched. Help text in artist words.
agent/tools/comfy_execute.py -- one additive success-dict key (progress_log),
recommended by the SEE contract so post-hoc telemetry works without re-running.
26 CLI tests; full suite 4948 passed, 0 failed (baseline 4821 held); ruff clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…with what-changed

agent/verbs/intend.py -- resolve_recipe (exact name -> trigger regex -> difflib
hints, no-raise), apply_recipe_to_session routing verbatim through the existing
apply_recipe handler (validated set_input patches, undo preserved, zero engine
duplication), what-changed computed from before/after session snapshots so even
adjust deltas report old->new honestly. Deterministic rail only -- never
orchestrate/stage (CLI.L4). 23 tests; 100-test neighbor run green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…0 offsite)

agent/verbs/own.py -- doctor_report (5 fixed-order checks, each a finding with
artist-worded note + fix hint; ComfyUI-down is a finding, never a crash),
stats_report (reuses the Mile-2 disk scan; session outcome counts; VRAM snapshot
when live), search_models (fuzzy over the LOCAL scan only -- the bundled offline
index stays deferred per OQ-4). Glyphs single-sourced from the find engine.
19 tests; 47-test combined run green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…odel search

agent/cli.py -- --recipe/-w additive on the EXISTING run command per WP-INTEND
(recipe short-circuits before any LLM/credential code: keyless rail); applied
recipes validate then execute on the same telemetry rail as cozy see (shared
_execute_with_telemetry helper; braille sparkline subscribes with zero extra
wiring; see behavior pinned by its unmodified suite). cozy doctor (exit 0/1,
CI-gateable), cozy stats, cozy model search (singular sub-app; models search
additive alias). 24 CLI tests; full suite 5014 passed, 0 failed; ruff clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…alidated undoable patch

agent/verbs/pull_canvas.py -- pull_canvas() folds the artist's browser graph into
the session strictly per ORCH.L8: jsonpatch.make_patch(current, artist) applied
through the apply_workflow_patch handler (its lock/undo/rollback reused, never
re-implemented); load_workflow_from_data only on an empty session where there is
no history to destroy. Adds/deletes/rewires are accepted freedom; the only
refusal is a wire that goes nowhere -- caught by a pre-apply structural trace
(apply_workflow_patch validates patch shape, not graph structure) that names the
exact node and input BEFORE any session mutation. UI-format files convert via
_ui_to_api; unmappable packs refused with names, never guessed. Deterministic
sorted summaries in artist words. 16 tests incl. undo-stack growth + restore.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…und-trip closes

agent/cli.py -- `cozy pull [--file PATH]`: live-canvas ingest by default, saved
workflow files (UI or API format) via --file. Exit 0 on applied / initial load /
already-match; exit 1 with a human-worded line (always ending in the
session-untouched guarantee where true) on unreachable/stale/broken-DAG.
Round-trip acceptance: open -> edit in browser -> pull = 2 commands, artist
node-add lands as a validated, undoable session patch. 11 CLI tests; full suite
5041 passed, 0 failed; ruff clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ession seam

pull_canvas no longer hands raw jsonpatch.make_patch output to the engine --
_build_patch_ops emits whole-value RFC6902 ops (max 3 segments, RFC6901-escaped)
so rewires land as real connection changes instead of the reproduced bogus flat
'model/0' input. Defense in depth: _patches_to_mutations now returns None for
any /inputs/ path deeper than 3 segments (element-level paths must not flatten),
forcing the correct jsonpatch fallback for every caller. New public session
seam: export_session_state()/import_session_state() -- deterministic deep-copied
snapshot (history capped at 10) restored under the session lock with a fresh
engine, malformed input rejected in artist words with the live session
untouched. 7 engine-active regression tests incl. the exact reproduced rewire.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ompact

execute_with_progress success dicts carry progress_log only when the caller
passes include_progress_log=true, downsampled to the last 500 progress entries
with a progress_log_truncated marker -- a long video render no longer injects
hundreds of KB into every MCP tool result. Error/timeout dicts keep the full
log as before. 5 gating tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… undo count

find: build_models_report runs _extract_api_format first -- ui_with_api files
resolve their real references, ui_only gets "re-export with Save (API Format)"
instead of the reproduced false all-clear. intend: a partial recipe (error set)
renders "Applied N of M steps, then stopped: <reason>" -- never the success
header; multi-step recipes report their real undo depth instead of promising
one undo. Recipe mutation batching into a single patch: deferred (touches
agent/recipes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… + encoding guard

agent/verbs/_cli_state.py -- the CLI session sidecar: persist() snapshots via
the new workflow_patch seam to sessions/_cli_verbs.session.json (atomic,
deterministic); restore() rehydrates it, corrupt files degrade to a human note.
open/pull/see/run --recipe restore first and persist after mutations, so the
canvas round-trip and the undo promise are true across separate cozy
invocations -- reproduced live with two real processes. _recipe_run loads
--workflow through the session seam (_load_workflow) instead of the
analysis-only parser; the test that mocked the bug in is unmocked. stdout/
stderr reconfigure to utf-8/replace when the stream can't encode the glyphs --
cozy doctor > file.txt no longer dies in a UnicodeEncodeError traceback, so the
CI-gating promise holds. Partial recipes exit 1 without executing; telemetry
rail passes include_progress_log=true. Full suite 5083 passed, 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 18, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The release adds artist-facing FIND, INTEND, OPEN, OPEN-IN, SEE, and OWN CLI verbs; persistent workflow sessions; deterministic terminal telemetry rendering; safer RFC6902 patch conversion; opt-in progress logs; UTF-8 stream handling; release metadata; and read-only Git rules for agent instructions.

Artist verb engines

Layer / File(s) Summary
FIND and OWN inventory reports
agent/verbs/find.py, agent/verbs/own.py, tests/test_verbs_find.py, tests/test_verbs_own.py
Adds local model/node discovery, palette search, health checks, statistics, model search, degraded reporting, and text renderers.
INTEND recipe execution
agent/verbs/intend.py, tests/test_verbs_intend.py
Adds deterministic recipe resolution, application, workflow diffing, partial-failure reporting, and undo-aware rendering.
OPEN and OPEN-IN canvas round-trip
agent/verbs/open_canvas.py, agent/verbs/pull_canvas.py, tests/test_verbs_open.py, tests/test_verbs_pull.py, tests/test_cli_pull.py
Adds browser/canvas integration, workflow acquisition, graph validation, whole-value patches, initial loading, and undoable updates.
SEE telemetry and progress logs
agent/verbs/see.py, agent/tools/comfy_execute.py, tests/test_verbs_see.py, tests/test_comfy_execute.py
Adds progress timing collection, VRAM snapshots, terminal summaries, and opt-in capped success logs.
CLI command wiring
agent/cli.py, tests/test_cli_verbs.py, tests/test_cli_verbs2.py
Registers the verb commands, connects session restoration/persistence, preserves recipe execution flow, and handles degraded output and encoding cases.

Session and rendering support

Layer / File(s) Summary
Workflow patching and session persistence
agent/tools/workflow_patch.py, agent/verbs/_cli_state.py, tests/test_workflow_patch.py, tests/test_cli_state.py
Adds element-level patch fallback behavior and versioned, bounded, atomic session snapshots with validation and restoration.
Terminal rendering and release metadata
agent/_render.py, agent/__init__.py, CHANGELOG.md, tests/test_render.py
Adds deterministic sparklines, VRAM bars, step summaries, Unicode stream safeguards, and the v5.9.0 release entry.

Agent boundaries

Layer / File(s) Summary
Conductor-only Git mutations
.claude/agents/cozy-crucible.md, .claude/agents/cozy-provisioner.md, .claude/agents/cozy-scout.md
Restricts the listed agents to read-only Git inspection and reserves staging, commits, tags, pushes, and other mutations for the conductor.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Artist
  participant CLI
  participant VerbEngine
  participant SessionSidecar
  participant ComfyUI
  Artist->>CLI: Run artist verb
  CLI->>SessionSidecar: Restore saved workflow state
  CLI->>VerbEngine: Execute FIND, INTEND, OPEN, PULL, SEE, or OWN
  VerbEngine->>ComfyUI: Read canvas, validate, execute, or query telemetry
  VerbEngine->>SessionSidecar: Persist updated workflow state
  SessionSidecar-->>CLI: Return persistence note
  CLI-->>Artist: Render structured result
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: new local-first artist verbs and the browser canvas round-trip in v5.9.0.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch forge/cli-orch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (8)
agent/verbs/own.py-126-139 (1)

126-139: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Report unreadable Custom_Nodes separately from a missing bridge pack.

Line 128 treats permission and I/O failures as an empty directory, producing the incorrect “install the pack” remedy. Return a human-readable access error instead.

Proposed fix
     try:
         packs = [p.name for p in cn_dir.iterdir() if p.is_dir()]
-    except OSError:
-        packs = []
+    except OSError:
+        return _check(
+            "bridge_pack",
+            False,
+            f"Could not read the Custom_Nodes folder at {cn_dir}.",
+            "Check that the folder exists and is readable, then run `cozy doctor` again.",
+        )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/verbs/own.py` around lines 126 - 139, Update the Custom_Nodes directory
handling around the packs discovery in the bridge-pack check to distinguish an
unreadable directory from an empty or missing one: catch the OSError details and
return a human-readable access/error result with an appropriate remedy instead
of treating it as no installed pack. Preserve the existing bridge_pack success
and missing-pack checks for readable directories.
agent/verbs/see.py-215-236 (1)

215-236: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Synchronize the trigger lifecycle and retain failed subscriptions for retry.

Concurrent install() calls can register two callbacks while retaining only one ID. Additionally, uninstall() clears the ID even when unregistering raises, permanently leaking the callback. Protect _trigger_id with a lifecycle lock and clear it only after successful removal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/verbs/see.py` around lines 215 - 236, Update the SEE subscriber
lifecycle in install() and uninstall() to synchronize access to _trigger_id with
a lifecycle lock, ensuring concurrent installs register at most one callback and
retain the active ID. In uninstall(), clear _trigger_id only after unregister
succeeds; leave it unchanged when removal raises so a later call can retry.
agent/tools/comfy_execute.py-877-882 (1)

877-882: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject invalid boolean values instead of enabling progress logs.

None, "invalid", lists, and dictionaries currently coerce to True, unexpectedly adding up to 500 log entries to the result. Accept explicit booleans and recognized true/false strings, otherwise return a human-readable validation error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/tools/comfy_execute.py` around lines 877 - 882, Update the
include_progress_log parsing near _raw_log and include_progress_log to accept
booleans and recognized true/false string values only; reject None, invalid
strings, lists, and dictionaries with a human-readable validation error instead
of enabling progress logging, while preserving the existing valid-value
behavior.
agent/verbs/see.py-67-78 (1)

67-78: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not report multi-step gaps as a single step duration.

When progress jumps from value 1 to 3, the two-step interval is appended as one step, producing an incorrect count and average. Only accept consecutive values; a gap should reseed telemetry.

Proposed fix
-        if curr.node_id == prev.node_id and curr.value > prev.value:
+        if curr.node_id == prev.node_id and curr.value == prev.value + 1:
             out.append(max(0.0, curr.at_s - prev.at_s))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/verbs/see.py` around lines 67 - 78, Update _durations to accept a
sample pair only when curr.value is exactly prev.value + 1, rather than any
increase; when a value gap occurs, reseed telemetry so the skipped interval
produces no duration and later consecutive steps remain measurable.
agent/verbs/_cli_state.py-87-99 (1)

87-99: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Include snapshot serialization in the guarded operation.

allow_nan=False can raise ValueError on workflow data before the try, contradicting persist()’s “Never raises” contract and skipping its user-facing warning.

Proposed fix
-    snapshot = export_session_state()
-    content = json.dumps(snapshot, indent=2, sort_keys=True, allow_nan=False)
-    path = sidecar_path()
     try:
+        snapshot = export_session_state()
+        content = json.dumps(snapshot, indent=2, sort_keys=True, allow_nan=False)
+        path = sidecar_path()
         path.parent.mkdir(parents=True, exist_ok=True)
         _atomic_write(path, content)
-    except (OSError, ValueError) as exc:
+    except (OSError, TypeError, ValueError) as exc:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/verbs/_cli_state.py` around lines 87 - 99, Move snapshot serialization,
including json.dumps in persist(), inside the existing try block so ValueError
is handled by the established warning and user-facing fallback. Preserve the
current atomic write behavior and “Never raises” contract for both serialization
and filesystem failures.
agent/_render.py-130-142 (1)

130-142: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the singular “step” when n == 1.

Line 142 produces 1 steps; tests/test_render.py Line 217 currently codifies that user-facing typo.

Proposed fix
     total = sum(vals)
     avg = total / n
-    return f"{n} steps · {avg:.2f} s/it avg · {total:.1f} s total"
+    unit = "step" if n == 1 else "steps"
+    return f"{n} {unit} · {avg:.2f} s/it avg · {total:.1f} s total"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/_render.py` around lines 130 - 142, Update format_step_times to use
“step” when n == 1 and retain “steps” for all other counts; adjust the
corresponding test expectation in tests/test_render.py.
agent/_render.py-56-63 (1)

56-63: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve the final observation when downsampling.

For m > n, int(i * m / n) never selects the final item; a render ending in a spike can therefore hide it.

Proposed fix
 def _resample(values: list[float], n: int) -> list[float]:
     """Nearest-neighbor resample to exactly n points (deterministic)."""
     m = len(values)
     if m == 0 or n <= 0:
         return []
     if m == n:
         return list(values)
-    return [values[min(m - 1, int(i * m / n))] for i in range(n)]
+    if n == 1:
+        return [values[m // 2]]
+    scale = (m - 1) / (n - 1)
+    return [values[int(i * scale + 0.5)] for i in range(n)]

Add a regression asserting that downsampling includes the final sample.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/_render.py` around lines 56 - 63, Update _resample so downsampling when
m > n always selects the final input observation while still returning exactly n
points and preserving nearest-neighbor behavior. Add a regression test asserting
that the final sample appears in the downsampled result, including a case where
the final value is a spike.
agent/cli.py-1266-1270 (1)

1266-1270: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Catch UnicodeError in workflow loaders _read_workflow_json already handles OSError, but both it and _load_workflow still miss UnicodeError from read_text(encoding="utf-8"), so non-UTF-8 files can leak a traceback. Add regression tests for the direct load path and cozy run --recipe.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/cli.py` around lines 1266 - 1270, Update the workflow JSON loaders,
including _read_workflow_json and _load_workflow, to catch UnicodeError
alongside the existing file-read and JSON parsing exceptions when decoding UTF-8
content, preserving the current user-facing error and exit behavior. Add
regression tests covering both the direct loading path and cozy run --recipe
with non-UTF-8 workflow files.

Source: Coding guidelines

🧹 Nitpick comments (2)
agent/verbs/intend.py (1)

40-51: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Complete the Python type annotations across the INTEND implementation and tests.

  • agent/verbs/intend.py#L40-L51: parameterize structured mappings, such as dict[str, Any], throughout the module.
  • tests/test_verbs_intend.py#L32-L107: annotate fixture returns, nested helper parameters, fixture arguments, and test returns.

As per coding guidelines, “Use Python 3.10+ type hints throughout the Python codebase.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/verbs/intend.py` around lines 40 - 51, Complete Python 3.10+
annotations throughout the INTEND implementation: update _recipe_info and other
structured mapping return types in agent/verbs/intend.py to use parameterized
mappings such as dict[str, Any]. In tests/test_verbs_intend.py, annotate fixture
returns, nested helper parameters, fixture arguments, and test return types
across the specified range; apply the required changes at both affected sites.

Source: Coding guidelines

tests/test_verbs_find.py (1)

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

Add Python 3.10+ annotations to the new test callables.

Annotate fixture parameters and returns, helper and nested mock signatures, and test methods with -> None.

  • tests/test_verbs_find.py#L18-L67: annotate all fixtures and their tmp_path parameters.
  • tests/test_verbs_find.py#L75-L389: annotate test methods and fixture parameters.
  • tests/test_verbs_own.py#L19-L102: annotate fixtures, helpers, and fake_handle.
  • tests/test_verbs_own.py#L110-L352: annotate test methods and fixture parameters.

As per coding guidelines, **/*.py: “Use Python 3.10+ type hints throughout the Python codebase.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_verbs_find.py` around lines 18 - 67, Add Python 3.10+ type
annotations throughout the specified test callables: annotate fixture parameters
such as tmp_path, fixture return values, helper and nested mock signatures,
fake_handle, and all test methods with -> None where applicable. Apply these
updates in tests/test_verbs_find.py ranges 18-67 and 75-389, and
tests/test_verbs_own.py ranges 19-102 and 110-352; preserve the existing test
behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/agents/cozy-crucible.md:
- Around line 17-18: Add a conductor/harness enforcement gate that blocks
state-mutating Git commands while allowing the documented read-only inspection
commands, and align the agent wording with that enforced contract. Apply the
corresponding documentation update at .claude/agents/cozy-crucible.md lines
17-18, .claude/agents/cozy-provisioner.md lines 17-18, and
.claude/agents/cozy-scout.md lines 17-18; all three sites require the same
alignment, while the root enforcement belongs in the existing runtime validation
path.

In `@agent/_render.py`:
- Around line 24-35: Broaden the input annotations for _finite,
braille_sparkline, block_sparkline, and format_step_times from list[float] to a
sequence-based type such as Sequence[Any], reflecting their support for None and
other float()-coercible values while preserving their existing behavior.

In `@agent/cli.py`:
- Around line 155-192: Annotate the new Typer callback run in agent/cli.py with
its appropriate return type, and add -> None to every new test method and
fixture in agent/cli.py, tests/test_render.py lines 20-22,
tests/test_cli_verbs2.py lines 23-28, tests/test_cli_pull.py lines 21-26, and
tests/test_cli_verbs.py lines 21-25. Preserve all existing behavior.

In `@agent/tools/workflow_patch.py`:
- Around line 1327-1364: Validate the snapshot’s current_workflow,
base_workflow, and every history entry with the normal graph/DAG validation
before acquiring state._lock or replacing live state. Reject empty or malformed
graphs and invalid history entries using the existing validation path, while
preserving the current restore behavior only for fully valid snapshots.
- Around line 462-472: Update the path parsing in the mutation logic around
parts and mutations.setdefault so it preserves JSON Pointer token boundaries:
split the original path without stripping slashes, accept only the exact
/node/inputs/param shape, reject element-level or empty-token paths including
trailing-slash variants, and decode JSON Pointer tokens (~0 and ~1) before using
node_id and param_name.

In `@agent/verbs/find.py`:
- Around line 317-352: The workflow inventory in the on-disk indexing and
reference evaluation flow must retain each model’s existing status, including
zero-byte files, instead of storing only the model type. Update the on_disk
entries and the lookup in the refs loop so that a zero-byte model status is
propagated before applying family-mismatch logic, preserving the existing
attention status rather than reporting it as ok.

In `@agent/verbs/intend.py`:
- Around line 245-251: Update the error rendering in the result-handling block
to sanitize engine errors before interpolating them into the “Applied … then
stopped” message. Preserve known domain errors, but replace traceback-shaped or
multiline internal values with a concise human-readable fallback; keep the
existing step counts and stop-reason behavior unchanged.
- Around line 183-205: The exception path around recipe application must
distinguish decoding failures after mutation from failures before any change.
Use the existing before snapshot and _session_workflow() inside the except block
to compute landed changes; report those changes and a partial-failure status
when mutations are observable, while retaining the unchanged “nothing was
changed” response only when the diff is empty.

In `@agent/verbs/own.py`:
- Around line 237-249: Update the outcome-counting loop over *_outcomes.jsonl to
read each file incrementally line by line instead of using
path.read_text(encoding="utf-8").splitlines(). Preserve skipping blank lines,
ignoring malformed JSON, counting valid records, and continuing on OSError.

In `@agent/verbs/pull_canvas.py`:
- Around line 350-395: The pull flow in agent/verbs/pull_canvas.py, centered on
_get_state(), _build_patch_ops(), and handle("apply_workflow_patch"), must
protect the workflow snapshot, comparison, and load or patch application as one
atomic operation, or use revision-based compare-and-swap; update
agent/verbs/open_canvas.py at lines 64-72 to copy the workflow while holding
_get_state()._lock before passing it to the bridge.
- Around line 44-50: Update _normalize_graph to validate every graph entry and
reject the entire pull when any node is not a dictionary or lacks class_type,
rather than filtering malformed nodes out. Propagate the validation failure
through the complete pull flow, including the path around lines 318-329, so
malformed existing nodes cannot be interpreted as deletions.
- Around line 134-151: Restrict pull_canvas’s diff generation to existing nodes
with unchanged node IDs, types, and non-input fields: reject or fail structural
additions, removals, replacements, and node-type changes instead of emitting
whole-node operations, while retaining parameter and connection input updates.
In tests/test_verbs_pull.py:50-84, update coverage to assert structural edits
are refused while input changes succeed; in tests/test_verbs_pull.py:281-296,
remove the expectation that adding node 9 succeeds after link repair and assert
rejection instead.
- Around line 53-68: Extend graph validation beyond _broken_links to detect
cycles in the connection graph before accepting changes. Ensure both the
initial-load path and the patch-application path reject cyclic workflows before
storing the graph or creating the optional engine, while preserving existing
dangling-link validation and error reporting.
- Around line 248-254: Update the JSON loading block around Path.read_text and
json.loads to catch OSError and UnicodeError in addition to JSONDecodeError,
returning a user-readable error tuple for each file-access or decoding failure
instead of allowing raw exceptions to escape. Preserve the existing
truncated-file guidance for JSONDecodeError.

In `@agent/verbs/see.py`:
- Line 81: Complete the Python 3.10+ annotations across all specified sites: in
agent/verbs/see.py:81, update step_durations_from_progress_log to use dict[str,
Any]; in agent/tools/comfy_execute.py:363-368, parameterize workflow and result
mappings; in tests/test_verbs_see.py:64, add -> None to tests and parameterize
helper results; and in tests/test_comfy_execute.py:358-360, type the fixture
argument and add return annotations to tests.

---

Minor comments:
In `@agent/_render.py`:
- Around line 130-142: Update format_step_times to use “step” when n == 1 and
retain “steps” for all other counts; adjust the corresponding test expectation
in tests/test_render.py.
- Around line 56-63: Update _resample so downsampling when m > n always selects
the final input observation while still returning exactly n points and
preserving nearest-neighbor behavior. Add a regression test asserting that the
final sample appears in the downsampled result, including a case where the final
value is a spike.

In `@agent/cli.py`:
- Around line 1266-1270: Update the workflow JSON loaders, including
_read_workflow_json and _load_workflow, to catch UnicodeError alongside the
existing file-read and JSON parsing exceptions when decoding UTF-8 content,
preserving the current user-facing error and exit behavior. Add regression tests
covering both the direct loading path and cozy run --recipe with non-UTF-8
workflow files.

In `@agent/tools/comfy_execute.py`:
- Around line 877-882: Update the include_progress_log parsing near _raw_log and
include_progress_log to accept booleans and recognized true/false string values
only; reject None, invalid strings, lists, and dictionaries with a
human-readable validation error instead of enabling progress logging, while
preserving the existing valid-value behavior.

In `@agent/verbs/_cli_state.py`:
- Around line 87-99: Move snapshot serialization, including json.dumps in
persist(), inside the existing try block so ValueError is handled by the
established warning and user-facing fallback. Preserve the current atomic write
behavior and “Never raises” contract for both serialization and filesystem
failures.

In `@agent/verbs/own.py`:
- Around line 126-139: Update the Custom_Nodes directory handling around the
packs discovery in the bridge-pack check to distinguish an unreadable directory
from an empty or missing one: catch the OSError details and return a
human-readable access/error result with an appropriate remedy instead of
treating it as no installed pack. Preserve the existing bridge_pack success and
missing-pack checks for readable directories.

In `@agent/verbs/see.py`:
- Around line 215-236: Update the SEE subscriber lifecycle in install() and
uninstall() to synchronize access to _trigger_id with a lifecycle lock, ensuring
concurrent installs register at most one callback and retain the active ID. In
uninstall(), clear _trigger_id only after unregister succeeds; leave it
unchanged when removal raises so a later call can retry.
- Around line 67-78: Update _durations to accept a sample pair only when
curr.value is exactly prev.value + 1, rather than any increase; when a value gap
occurs, reseed telemetry so the skipped interval produces no duration and later
consecutive steps remain measurable.

---

Nitpick comments:
In `@agent/verbs/intend.py`:
- Around line 40-51: Complete Python 3.10+ annotations throughout the INTEND
implementation: update _recipe_info and other structured mapping return types in
agent/verbs/intend.py to use parameterized mappings such as dict[str, Any]. In
tests/test_verbs_intend.py, annotate fixture returns, nested helper parameters,
fixture arguments, and test return types across the specified range; apply the
required changes at both affected sites.

In `@tests/test_verbs_find.py`:
- Around line 18-67: Add Python 3.10+ type annotations throughout the specified
test callables: annotate fixture parameters such as tmp_path, fixture return
values, helper and nested mock signatures, fake_handle, and all test methods
with -> None where applicable. Apply these updates in tests/test_verbs_find.py
ranges 18-67 and 75-389, and tests/test_verbs_own.py ranges 19-102 and 110-352;
preserve the existing test behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cb853971-d29b-40a7-8667-63aafdf534ad

📥 Commits

Reviewing files that changed from the base of the PR and between fb50917 and 7631071.

📒 Files selected for processing (30)
  • .claude/agents/cozy-crucible.md
  • .claude/agents/cozy-provisioner.md
  • .claude/agents/cozy-scout.md
  • CHANGELOG.md
  • agent/__init__.py
  • agent/_render.py
  • agent/cli.py
  • agent/tools/comfy_execute.py
  • agent/tools/workflow_patch.py
  • agent/verbs/__init__.py
  • agent/verbs/_cli_state.py
  • agent/verbs/find.py
  • agent/verbs/intend.py
  • agent/verbs/open_canvas.py
  • agent/verbs/own.py
  • agent/verbs/pull_canvas.py
  • agent/verbs/see.py
  • tests/test_cli_pull.py
  • tests/test_cli_state.py
  • tests/test_cli_verbs.py
  • tests/test_cli_verbs2.py
  • tests/test_comfy_execute.py
  • tests/test_render.py
  • tests/test_verbs_find.py
  • tests/test_verbs_intend.py
  • tests/test_verbs_open.py
  • tests/test_verbs_own.py
  • tests/test_verbs_pull.py
  • tests/test_verbs_see.py
  • tests/test_workflow_patch.py

Comment on lines +17 to +18
> **Git is conductor-only (ORCH.L1).** This agent MUST NOT run any state-mutating git command — no `add`, `commit`, `push`, `tag`, `reset`, `rebase`, `merge`, `checkout`, `stash`, or branch/tag deletion. Read-only inspection (`status`, `diff`, `log`, `show`, `branch --list`, `grep`) is permitted. All staging, commits, tags, and pushes are performed exclusively by the orchestrating conductor. Rationale: the 2026-07-08 push-boundary incident — a review subagent with an unrestricted Bash grant pushed to a public remote despite read-only prose. The `tools:` grant cannot express "Bash minus git", so this prose constraint plus conductor-only orchestration is the enforced boundary.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Files ==\n'
git ls-files '.claude/agents/cozy-crucible.md' '.claude/agents/cozy-provisioner.md' '.claude/agents/cozy-scout.md' '.claude' | sed -n '1,200p'

printf '\n== Outline: agent files ==\n'
for f in .claude/agents/cozy-crucible.md .claude/agents/cozy-provisioner.md .claude/agents/cozy-scout.md; do
  echo "--- $f ---"
  wc -l "$f"
  sed -n '1,120p' "$f" | nl -ba | sed -n '1,80p'
done

printf '\n== Search for conductor / git boundary enforcement ==\n'
rg -n --hidden --glob '!.git' -e 'conductor-only|state-mutating git|Bash minus git|push-boundary|read-only prose|git.*mutat|deny.*git|forbid.*git|git.*deny|shell.*git' .claude . 2>/dev/null | sed -n '1,240p'

Repository: JosephOIbrahim/Comfy-Cozy

Length of output: 1040


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in .claude/agents/cozy-crucible.md .claude/agents/cozy-provisioner.md .claude/agents/cozy-scout.md; do
  echo "===== $f ====="
  nl -ba "$f" | sed -n '1,80p'
  echo
done

Repository: JosephOIbrahim/Comfy-Cozy

Length of output: 247


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- cozy-crucible ---\n'
sed -n '1,60p' .claude/agents/cozy-crucible.md | nl -ba
printf '\n--- cozy-provisioner ---\n'
sed -n '1,60p' .claude/agents/cozy-provisioner.md | nl -ba
printf '\n--- cozy-scout ---\n'
sed -n '1,60p' .claude/agents/cozy-scout.md | nl -ba

printf '\n--- policy refs ---\n'
rg -n --hidden --glob '!.git' -e 'constitution|ORCH\.L1|Bash minus git|conductor-only|state-mutating git|read-only inspection|tools:' .claude . 2>/dev/null | sed -n '1,240p'

Repository: JosephOIbrahim/Comfy-Cozy

Length of output: 254


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in .claude/agents/cozy-crucible.md .claude/agents/cozy-provisioner.md .claude/agents/cozy-scout.md; do
  echo "===== $f ====="
  awk 'NR>=1 && NR<=40 {printf "%4d  %s\n", NR, $0}' "$f"
  echo
done

Repository: JosephOIbrahim/Comfy-Cozy

Length of output: 6850


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Constitution outline =="
wc -l .claude/COZY_CONSTITUTION.md
sed -n '1,220p' .claude/COZY_CONSTITUTION.md | awk '{printf "%4d  %s\n", NR, $0}'

echo
echo "== Search for enforcement / git policy =="
rg -n --hidden --glob '!.git' -e 'git|push|commit|tag|reset|rebase|merge|checkout|stash|conductor|ORCH\.L1|human_gates|_IRREVERSIBLE_ACTIONS|deny|forbid|enforce|tools:' .claude | sed -n '1,260p'

Repository: JosephOIbrahim/Comfy-Cozy

Length of output: 27034


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate runtime files =="
git ls-files | rg '(^|/)(agent/stage/constitution\.py|agent/stage/ratchet\.py|agent/stage/|CLAUDE\.md|\.claude/settings\.json|\.claude/dispatch-phase5b\.md)$' | sed -n '1,200p'

echo
echo "== grep for pre/post checks and git authority map =="
rg -n --hidden --glob '!.git' -e 'run_pre_checks|run_post_checks|Git Authority Map|requires per-call approval|git push|git add|git commit|Bash' agent .claude | sed -n '1,260p'

echo
echo "== any tool gate definitions mentioning Bash/Git =="
rg -n --hidden --glob '!.git' -e 'allowed_tools|tools:' .claude/agents .claude/commands .claude  | sed -n '1,260p'

Repository: JosephOIbrahim/Comfy-Cozy

Length of output: 10251


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== run_pre_checks / run_post_checks =="
sed -n '400,560p' agent/stage/constitution.py | awk '{printf "%4d  %s\n", NR+399, $0}'

echo
echo "== git references in constitution.py =="
rg -n 'git|push|commit|tag|reset|rebase|merge|checkout|stash|branch' agent/stage/constitution.py | sed -n '1,200p'

Repository: JosephOIbrahim/Comfy-Cozy

Length of output: 4870


Make the Git boundary real in the conductor/harness, not just in agent prose.
The current runtime checks cover role isolation, human gates, and persistence, but not Git mutation, so these three files still depend on a prose-only restriction:

  • .claude/agents/cozy-crucible.md#L17-L18
  • .claude/agents/cozy-provisioner.md#L17-L18
  • .claude/agents/cozy-scout.md#L17-L18

Add an enforcement gate for state-mutating Git commands and keep the agent wording aligned with the harness contract.

📍 Affects 3 files
  • .claude/agents/cozy-crucible.md#L17-L18 (this comment)
  • .claude/agents/cozy-provisioner.md#L17-L18
  • .claude/agents/cozy-scout.md#L17-L18
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/agents/cozy-crucible.md around lines 17 - 18, Add a
conductor/harness enforcement gate that blocks state-mutating Git commands while
allowing the documented read-only inspection commands, and align the agent
wording with that enforced contract. Apply the corresponding documentation
update at .claude/agents/cozy-crucible.md lines 17-18,
.claude/agents/cozy-provisioner.md lines 17-18, and .claude/agents/cozy-scout.md
lines 17-18; all three sites require the same alignment, while the root
enforcement belongs in the existing runtime validation path.

Comment thread agent/_render.py
Comment on lines +24 to +35
def _finite(values: list[float]) -> list[float]:
"""Return only the finite numeric entries, dropping None/NaN/Inf/non-numbers."""
out: list[float] = []
for v in values:
if v is None:
continue
try:
f = float(v)
except (TypeError, ValueError):
continue
if math.isnan(f) or math.isinf(f):
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

ast-grep outline agent/_render.py --items all --type function --view expanded
rg -n -C2 'None|non-numbers|def (_finite|braille_sparkline|block_sparkline|format_step_times)' agent/_render.py

Repository: JosephOIbrahim/Comfy-Cozy

Length of output: 2300


🏁 Script executed:

sed -n '1,220p' agent/_render.py
printf '\n--- tests ---\n'
rg -n -C3 'braille_sparkline|block_sparkline|format_step_times|_finite|_resample' -g '*test*' -g '*.py'

Repository: JosephOIbrahim/Comfy-Cozy

Length of output: 5134


🏁 Script executed:

rg -n -C2 '\b(_finite|braille_sparkline|block_sparkline|format_step_times)\b' agent . --glob '*.py'

Repository: JosephOIbrahim/Comfy-Cozy

Length of output: 15893


Broaden the renderer helper annotations. These helpers accept None and other float()-coercible values at runtime, so list[float] understates the contract. Use a broader input type like Sequence[Any] across _finite, braille_sparkline, block_sparkline, and format_step_times.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/_render.py` around lines 24 - 35, Broaden the input annotations for
_finite, braille_sparkline, block_sparkline, and format_step_times from
list[float] to a sequence-based type such as Sequence[Any], reflecting their
support for None and other float()-coercible values while preserving their
existing behavior.

Source: Coding guidelines

Comment thread agent/cli.py
Comment on lines 155 to 192
def run(
session: str = typer.Option(
None, "--session", "-s",
None,
"--session",
"-s",
help="Named session for memory persistence",
),
verbose: bool = typer.Option(
False, "--verbose", "-v",
False,
"--verbose",
"-v",
help="Show debug logging (API timing, tool execution, context management)",
),
model: str = typer.Option(
None, "--model",
None,
"--model",
help="Model alias or id to use (e.g. 'nemotron', 'claude'). See list_models_available.",
),
provider: str = typer.Option(
None, "--provider",
None,
"--provider",
help="LLM provider: anthropic|openai|gemini|ollama|nvidia|custom",
),
recipe: str = typer.Option(
None,
"--recipe",
help=(
"Apply a named look ('dreamier', 'sharper', 'faster'...) to the workflow "
"and run it -- no chat, no API key. 'list' shows every recipe."
),
),
workflow: str = typer.Option(
None,
"--workflow",
"-w",
help="With --recipe: the workflow JSON file to load and run the recipe on.",
),
):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

ast-grep run --pattern 'def $FUNC($$$): $$$' --lang python \
  agent/cli.py tests/test_render.py tests/test_cli_verbs2.py \
  tests/test_cli_pull.py tests/test_cli_verbs.py

Repository: JosephOIbrahim/Comfy-Cozy

Length of output: 50382


🏁 Script executed:

sed -n '140,210p' agent/cli.py
printf '\n---\n'
sed -n '1,120p' tests/test_render.py
printf '\n---\n'
sed -n '1,80p' tests/test_cli_verbs.py
printf '\n---\n'
sed -n '1,80p' tests/test_cli_pull.py
printf '\n---\n'
sed -n '1,80p' tests/test_cli_verbs2.py

Repository: JosephOIbrahim/Comfy-Cozy

Length of output: 15315


🏁 Script executed:

ast-grep outline agent/cli.py --view expanded
printf '\n---\n'
ast-grep outline tests/test_render.py --view expanded
printf '\n---\n'
ast-grep outline tests/test_cli_verbs.py --view expanded
printf '\n---\n'
ast-grep outline tests/test_cli_pull.py --view expanded
printf '\n---\n'
ast-grep outline tests/test_cli_verbs2.py --view expanded

Repository: JosephOIbrahim/Comfy-Cozy

Length of output: 8481


🏁 Script executed:

sed -n '380,520p' agent/cli.py
printf '\n---\n'
sed -n '520,760p' agent/cli.py
printf '\n---\n'
sed -n '760,980p' agent/cli.py
printf '\n---\n'
sed -n '980,1160p' agent/cli.py
printf '\n---\n'
sed -n '1160,1510p' agent/cli.py

Repository: JosephOIbrahim/Comfy-Cozy

Length of output: 42222


Add the missing return annotations

  • Add return annotations to the new Typer callbacks in agent/cli.py.
  • Add -> None to the new test methods and fixtures in tests/test_render.py, tests/test_cli_verbs.py, tests/test_cli_pull.py, and tests/test_cli_verbs2.py.
📍 Affects 5 files
  • agent/cli.py#L155-L192 (this comment)
  • tests/test_render.py#L20-L22
  • tests/test_cli_verbs2.py#L23-L28
  • tests/test_cli_pull.py#L21-L26
  • tests/test_cli_verbs.py#L21-L25
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/cli.py` around lines 155 - 192, Annotate the new Typer callback run in
agent/cli.py with its appropriate return type, and add -> None to every new test
method and fixture in agent/cli.py, tests/test_render.py lines 20-22,
tests/test_cli_verbs2.py lines 23-28, tests/test_cli_pull.py lines 21-26, and
tests/test_cli_verbs.py lines 21-25. Preserve all existing behavior.

Source: Coding guidelines

Comment on lines 462 to 472
parts = path.strip("/").split("/")
if len(parts) < 3 or parts[1] != "inputs":
return None
if len(parts) > 3:
# Element-level paths (e.g. /2/inputs/model/0) must not flatten
# into a bogus literal param name — force the jsonpatch fallback.
return None

node_id = parts[0]
param_name = "/".join(parts[2:]) # Handle nested paths
param_name = parts[2]
mutations.setdefault(node_id, {})[param_name] = value

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Preserve JSON Pointer token boundaries.

strip("/") makes /3/inputs/model/ indistinguishable from /3/inputs/model, so an element-level path with an empty final token can incorrectly overwrite the whole input and report success. Split without stripping, require the exact /node/inputs/param shape, and decode ~0/~1 tokens.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/tools/workflow_patch.py` around lines 462 - 472, Update the path
parsing in the mutation logic around parts and mutations.setdefault so it
preserves JSON Pointer token boundaries: split the original path without
stripping slashes, accept only the exact /node/inputs/param shape, reject
element-level or empty-token paths including trailing-slash variants, and decode
JSON Pointer tokens (~0 and ~1) before using node_id and param_name.

Comment on lines +1327 to +1364
current = snapshot.get("current_workflow")
if current is not None and not isinstance(current, dict):
return "That session snapshot is damaged: the workflow inside it isn't a graph."
base = snapshot.get("base_workflow")
if base is not None and not isinstance(base, dict):
return "That session snapshot is damaged: its original workflow isn't a graph."
history = snapshot.get("history", [])
if not isinstance(history, list) or any(not isinstance(h, dict) for h in history):
return "That session snapshot is damaged: its undo history isn't readable."

state = _get_state()
with state._lock:
if current is None:
state["current_workflow"] = None
state["base_workflow"] = None
state["history"] = deque(maxlen=_MAX_HISTORY)
state["validated_since_mutation"] = False
_set_engine(None)
return None

state["current_workflow"] = copy.deepcopy(current)
state["base_workflow"] = (
copy.deepcopy(base) if base is not None else copy.deepcopy(current)
)
restored = deque(maxlen=_MAX_HISTORY)
for entry in history:
restored.append(copy.deepcopy(entry))
state["history"] = restored
state["validated_since_mutation"] = bool(
snapshot.get("validated_since_mutation", False)
)

# Mirror load_workflow_from_data: fresh engine from the restored graph.
try:
_set_engine(_create_engine(state["current_workflow"]))
except Exception as exc: # import already succeeded, engine is optional
log.warning("Could not create engine on import: %s. Engine disabled.", exc)
_set_engine(None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Validate every restored graph before replacing live state.

A schema-1 snapshot with current_workflow: {} or malformed base_workflow/history dictionaries passes these checks. State is replaced, engine failure is suppressed, and restore reports success; malformed history can likewise surface during undo. Run the normal graph/DAG validation over current, base, and every history entry before acquiring the mutation lock.

As per coding guidelines, “Never delete all nodes, replace an entire workflow JSON, or modify node types; only change inputs and connections, and refuse changes that break the DAG.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/tools/workflow_patch.py` around lines 1327 - 1364, Validate the
snapshot’s current_workflow, base_workflow, and every history entry with the
normal graph/DAG validation before acquiring state._lock or replacing live
state. Reject empty or malformed graphs and invalid history entries using the
existing validation path, while preserving the current restore behavior only for
fully valid snapshots.

Source: Coding guidelines

Comment on lines +53 to +68
def _broken_links(graph: dict) -> list[str]:
"""Return human-worded descriptions of connections that break the DAG.

A connection whose source node id is absent from the graph is a dangling
link — executing it would fail, so ingest refuses and explains. Uses the
same connection tracer the validator uses; output order is deterministic.
"""
problems: list[str] = []
for conn in _trace_connections(graph):
if str(conn["from_node"]) not in graph:
problems.append(
f"{conn['to_class']} (node {conn['to_node']}) input "
f"'{conn['to_input']}' points at node {conn['from_node']}, "
"which is not in the graph"
)
return problems

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Validate cycles before accepting the graph.

_broken_links only checks whether source IDs exist. A cycle such as 1 → 2 → 1 passes, and the initial-load branch stores it before optional engine creation can reject it. Add explicit cycle detection before either load or patch application.

As per coding guidelines, workflow changes must be refused when they break the DAG.

Also applies to: 332-356

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/verbs/pull_canvas.py` around lines 53 - 68, Extend graph validation
beyond _broken_links to detect cycles in the connection graph before accepting
changes. Ensure both the initial-load path and the patch-application path reject
cyclic workflows before storing the graph or creating the optional engine, while
preserving existing dangling-link validation and error reporting.

Source: Coding guidelines

Comment on lines +134 to +151
for nid in sorted(set(current) | set(artist)):
if nid not in current:
ops.append({"op": "add", "path": f"/{_esc(nid)}", "value": artist[nid]})
continue
if nid not in artist:
ops.append({"op": "remove", "path": f"/{_esc(nid)}"})
continue
cur_node, art_node = current[nid], artist[nid]
if cur_node == art_node:
continue
cur_rest = {k: v for k, v in cur_node.items() if k != "inputs"}
art_rest = {k: v for k, v in art_node.items() if k != "inputs"}
cur_in = cur_node.get("inputs")
art_in = art_node.get("inputs")
if cur_rest != art_rest or not isinstance(cur_in, dict) or not isinstance(art_in, dict):
# class_type (or any non-input member) changed, or inputs aren't
# a plain dict on one side — swap the whole node in one honest op.
ops.append({"op": "replace", "path": f"/{_esc(nid)}", "value": art_node})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

The implementation and tests permit structural edits outside the allowed workflow mutation contract.

  • agent/verbs/pull_canvas.py#L134-L151: reject node-ID, node-type, and non-input changes instead of emitting whole-node operations.
  • tests/test_verbs_pull.py#L50-L84: test parameter and connection updates separately from structural edits, which should be refused.
  • tests/test_verbs_pull.py#L281-L296: remove the expectation that adding node 9 succeeds after repairing its link.

As per coding guidelines, “Never delete all nodes, replace an entire workflow JSON, or modify node types; only change inputs and connections.”

📍 Affects 2 files
  • agent/verbs/pull_canvas.py#L134-L151 (this comment)
  • tests/test_verbs_pull.py#L50-L84
  • tests/test_verbs_pull.py#L281-L296
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/verbs/pull_canvas.py` around lines 134 - 151, Restrict pull_canvas’s
diff generation to existing nodes with unchanged node IDs, types, and non-input
fields: reject or fail structural additions, removals, replacements, and
node-type changes instead of emitting whole-node operations, while retaining
parameter and connection input updates. In tests/test_verbs_pull.py:50-84,
update coverage to assert structural edits are refused while input changes
succeed; in tests/test_verbs_pull.py:281-296, remove the expectation that adding
node 9 succeeds after link repair and assert rejection instead.

Source: Coding guidelines

Comment on lines +248 to +254
try:
data = json.loads(Path(file).read_text(encoding="utf-8"))
except json.JSONDecodeError:
return None, (
f"{file} isn't valid JSON — it may be truncated. "
"Re-save the workflow from ComfyUI and try again."
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Translate file-access and decoding failures into user-readable errors.

read_text can raise OSError or UnicodeError after validation—for example, if the file disappears, lacks permission, or is not UTF-8. Those currently escape as raw tracebacks.

Proposed fix
     try:
         data = json.loads(Path(file).read_text(encoding="utf-8"))
     except json.JSONDecodeError:
         return None, (
             f"{file} isn't valid JSON — it may be truncated. "
             "Re-save the workflow from ComfyUI and try again."
         )
+    except UnicodeError:
+        return None, f"{file} isn't valid UTF-8 text."
+    except OSError as exc:
+        return None, f"Couldn't read {file}: {exc}"

As per coding guidelines, “Never expose raw tracebacks; translate errors into human-readable messages.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
data = json.loads(Path(file).read_text(encoding="utf-8"))
except json.JSONDecodeError:
return None, (
f"{file} isn't valid JSON — it may be truncated. "
"Re-save the workflow from ComfyUI and try again."
)
try:
data = json.loads(Path(file).read_text(encoding="utf-8"))
except json.JSONDecodeError:
return None, (
f"{file} isn't valid JSON — it may be truncated. "
"Re-save the workflow from ComfyUI and try again."
)
except UnicodeError:
return None, f"{file} isn't valid UTF-8 text."
except OSError as exc:
return None, f"Couldn't read {file}: {exc}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/verbs/pull_canvas.py` around lines 248 - 254, Update the JSON loading
block around Path.read_text and json.loads to catch OSError and UnicodeError in
addition to JSONDecodeError, returning a user-readable error tuple for each
file-access or decoding failure instead of allowing raw exceptions to escape.
Preserve the existing truncated-file guidance for JSONDecodeError.

Source: Coding guidelines

Comment on lines +350 to +395
current = workflow_patch._get_state()["current_workflow"]

# No session workflow: initial ingest. There is no undo history, base, or
# consent flag to destroy — loading here is legitimate (and the only path).
if current is None:
origin = "<canvas pull>" if source == "canvas" else str(file)
load_err = workflow_patch.load_workflow_from_data(artist_graph, source=origin)
if load_err:
return _result(
ok=False,
pulled=True,
applied=False,
source=source,
node_count=node_count,
message=f"Couldn't start a session from the pulled graph: {load_err}",
)
return _result(
ok=True,
pulled=True,
applied=True,
initial_load=True,
source=source,
node_count=node_count,
message=(
f"No session workflow was loaded, so your graph ({node_count} nodes) "
"just became the session baseline. Nothing was overwritten — the "
"session simply starts from what you built."
),
)

# Live session: edits re-enter ONLY as a validated patch (ORCH.L8),
# built at whole-value granularity — never element-level (defect A1).
patch_ops = _build_patch_ops(current, artist_graph)
if not patch_ops:
return _result(
ok=True,
pulled=True,
applied=False,
source=source,
node_count=node_count,
message="Canvas and session already match — nothing to pull.",
)

summary = _summarize_changes(current, artist_graph)
apply_result = json.loads(
workflow_patch.handle("apply_workflow_patch", {"patches": patch_ops})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Session workflow reads and writes are not consistently protected by the session lock.

  • agent/verbs/pull_canvas.py#L350-L395: make snapshot, comparison, and load/patch application one atomic operation or use revision-based compare-and-swap.
  • agent/verbs/open_canvas.py#L64-L72: copy the workflow under _get_state()._lock before passing it to the bridge.

As per coding guidelines, “Protect shared mutable state with threading.Lock in workflow patching [and] orchestration.”

📍 Affects 2 files
  • agent/verbs/pull_canvas.py#L350-L395 (this comment)
  • agent/verbs/open_canvas.py#L64-L72
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/verbs/pull_canvas.py` around lines 350 - 395, The pull flow in
agent/verbs/pull_canvas.py, centered on _get_state(), _build_patch_ops(), and
handle("apply_workflow_patch"), must protect the workflow snapshot, comparison,
and load or patch application as one atomic operation, or use revision-based
compare-and-swap; update agent/verbs/open_canvas.py at lines 64-72 to copy the
workflow while holding _get_state()._lock before passing it to the bridge.

Source: Coding guidelines

Comment thread agent/verbs/see.py
return out


def step_durations_from_progress_log(entries: list[dict]) -> list[float]:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Complete the new Python type annotations across this layer.

  • agent/verbs/see.py#L81-L81: parameterize progress-log mappings with dict[str, Any].
  • agent/tools/comfy_execute.py#L363-L368: parameterize workflow and result mappings.
  • tests/test_verbs_see.py#L64-L64: add -> None to tests and parameterize helper results.
  • tests/test_comfy_execute.py#L358-L360: type the fixture argument and add test return annotations.

As per coding guidelines, “Use Python 3.10+ type hints throughout the Python codebase.”

📍 Affects 4 files
  • agent/verbs/see.py#L81-L81 (this comment)
  • agent/tools/comfy_execute.py#L363-L368
  • tests/test_verbs_see.py#L64-L64
  • tests/test_comfy_execute.py#L358-L360
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/verbs/see.py` at line 81, Complete the Python 3.10+ annotations across
all specified sites: in agent/verbs/see.py:81, update
step_durations_from_progress_log to use dict[str, Any]; in
agent/tools/comfy_execute.py:363-368, parameterize workflow and result mappings;
in tests/test_verbs_see.py:64, add -> None to tests and parameterize helper
results; and in tests/test_comfy_execute.py:358-360, type the fixture argument
and add return annotations to tests.

Source: Coding guidelines

@JosephOIbrahim
JosephOIbrahim merged commit a95a163 into master Jul 18, 2026
1 of 11 checks passed
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