Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .claude/agents/cozy-crucible.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ You are the **Crucible**. Execute workflows, verify execution, validate
outputs. You are the test bench: every Forge build passes through you
before Vision sees it. You test but never modify or translate intent.

> **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.

Comment on lines +17 to +18

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.

## Owns
- workflow_execution
- execution_verification
Expand Down
2 changes: 2 additions & 0 deletions .claude/agents/cozy-provisioner.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ You are the **Provisioner**. Ensure all required assets are present before
build begins. Download, verify, and register models. You provision but never
modify workflows or execute them.

> **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.

## Owns
- model_provisioning
- asset_verification
Expand Down
2 changes: 2 additions & 0 deletions .claude/agents/cozy-scout.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ You are the **Scout**. Your job is **reconnaissance only**. Discover what
nodes, models, and custom nodes are available. Map the environment. Report
findings in a structured `recon_report` artifact. Never modify anything.

> **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.

## Owns
- environment_discovery
- model_identification
Expand Down
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,51 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [5.9.0] - 2026-07-18 — The Artist Verbs

The local-first CLI surface: five verbs an artist can drive without an account,
a network connection, or an API key — plus a browser canvas round-trip that
hands the graph to the artist and takes their edits back as validated,
undoable patches. All offline for core operation; the only sockets are
loopback to the local ComfyUI.

### Added
- **FIND** — `cozy models list` / `cozy nodes list` (✓/✗/⚠ health marks, pure
disk reads; degrades in plain words when ComfyUI is down) and `cozy find`,
a fuzzy command palette over the verb surface.
- **INTEND** — `cozy run --recipe dreamier|sharper|faster|…` applies a named
recipe to the session workflow as validated patches (keyless rail — recipe
runs never touch LLM credentials), reports every change old→new, then
validates and executes on the telemetry rail. `--recipe list` names the goal
vocabulary.
- **SEE** — `cozy see [workflow]` runs and renders what happened: braille
step-time sparkline, slowest nodes, VRAM bar. Partial telemetry still renders
when a run fails. New pure renderer in `agent/_render.py`.
- **OWN** — `cozy doctor` (one-key health sweep, CI-gateable exit codes),
`cozy stats` (on-device model/session/GPU numbers), `cozy model search`
(fuzzy, local disk only).
- **OPEN** — the canvas round-trip. `cozy open` pushes the session workflow
onto the live ComfyUI canvas and opens the browser; `cozy pull` folds the
artist's edits back in as ONE validated patch — node adds, deletes, rewires
and tweaks together, one undo away. The only refusal is a wire that goes
nowhere, named exactly, with the session untouched.
- **Cross-process session sidecar** — the cozy session (workflow, undo
history, baseline) survives between CLI invocations via an atomic snapshot
in `sessions/`, so open → edit → pull works across separate commands.
- `execute_with_progress` gains opt-in `include_progress_log` (downsampled,
truncation-marked) for telemetry consumers; success results stay compact by
default.

### Fixed (pre-release, caught by adversarial review before merge)
- Element-level RFC6902 paths no longer flatten into bogus literal inputs in
the patch engine fast-path — connection rewires land correctly everywhere.
- Glyph and sparkline output no longer dies in a UnicodeEncodeError on
redirected/piped stdout (cp1252): streams reconfigure to UTF-8/replace.
- UI-format workflow files report honestly in `models list` instead of a
false "no model references" all-clear.
- Partially-applied recipes render the stop reason and refuse to execute the
half-applied graph; multi-step recipes state their real undo depth.

## [5.8.2] - 2026-07-12 — Demo Ready

### Added
Expand Down
2 changes: 1 addition & 1 deletion agent/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""ComfyUI Agent — AI co-pilot for ComfyUI workflows."""

__version__ = "5.8.2"
__version__ = "5.9.0"


def tool_count() -> tuple[int, int, int]:
Expand Down
142 changes: 142 additions & 0 deletions agent/_render.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""Pure, deterministic terminal-render helpers for compact status glyphs.

These functions turn numeric series into small, always-legible strings —
Braille/eighth-block sparklines, a VRAM usage bar, and a per-step timing
line. They are stdlib-only, side-effect-free, and inert: nothing in the
agent imports or dispatches them yet. Same input always yields the same
output (He2025 determinism ethos — no wall-clock, no randomness).
"""

import math

# Braille cell dot bit values (offset from U+2800), addressed bottom -> top so
# a column of height h lights the lowest h dots. Layout per cell:
# 1 4
# 2 5
# 3 6
# 7 8
_BRAILLE_LEFT = (0x40, 0x04, 0x02, 0x01) # dots 7,3,2,1 (bottom -> top)
_BRAILLE_RIGHT = (0x80, 0x20, 0x10, 0x08) # dots 8,6,5,4 (bottom -> top)

_BLOCKS = "▁▂▃▄▅▆▇█" # eighth blocks, 8 legible levels


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
Comment on lines +24 to +35

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

out.append(f)
return out


def _levels(values: list[float], levels: int) -> list[int]:
"""Normalize a finite series to integer bucket indices in 0..levels-1.

All-equal input maps to a mid bucket (no divide-by-zero). Negative values
are handled by ranging against the series min, so the lowest value is 0.
"""
lo = min(values)
hi = max(values)
span = hi - lo
out: list[int] = []
for v in values:
frac = 0.5 if span == 0 else (v - lo) / span
out.append(int(frac * (levels - 1) + 0.5))
return out


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)]


def _braille_column(height: int, is_left: bool) -> int:
"""Bitmask lighting the bottom `height` dots of one Braille column."""
dots = _BRAILLE_LEFT if is_left else _BRAILLE_RIGHT
mask = 0
for k in range(height):
mask |= dots[k]
return mask


def braille_sparkline(values: list[float], width: int | None = None) -> str:
"""Render a dense Braille sparkline (2 dot-columns per char, 4 rows).

Each column encodes one value as a height of 1..4 lit dots. When `width`
is given the series is resampled to 2*width points so the result is exactly
`width` characters; otherwise it is ceil(len/2) characters. None/NaN/Inf
entries are filtered; an empty (or all-invalid) series returns "".
"""
vals = _finite(values)
if not vals:
return ""
if width is not None:
width = max(1, int(width))
vals = _resample(vals, 2 * width)
heights = [idx + 1 for idx in _levels(vals, 4)] # 1..4
chars: list[str] = []
for i in range(0, len(heights), 2):
left = heights[i]
right = heights[i + 1] if i + 1 < len(heights) else 0
code = 0x2800 | _braille_column(left, True) | _braille_column(right, False)
chars.append(chr(code))
return "".join(chars)


def block_sparkline(values: list[float]) -> str:
"""Render an eighth-block sparkline (▁▂▃▄▅▆▇█), one char per value.

Values are normalized to 8 levels against the series min/max; all-equal
input renders a flat mid-level line. None/NaN/Inf entries are filtered;
an empty (or all-invalid) series returns "".
"""
vals = _finite(values)
if not vals:
return ""
return "".join(_BLOCKS[i] for i in _levels(vals, 8))


def vram_bar(used_gb: float, total_gb: float, width: int = 20) -> str:
"""Render a [#####-----] usage bar with a "used/total GB" label.

`width` is the cell count, clamped to >= 1. Fill fraction is clamped to
0..1, so negative `used_gb`, over-budget usage, and a non-positive
`total_gb` all degrade sanely instead of raising.
"""
width = max(1, int(width))
if total_gb <= 0:
frac = 0.0
else:
frac = used_gb / total_gb
frac = min(1.0, max(0.0, frac))
filled = min(width, max(0, int(frac * width + 0.5)))
bar = "#" * filled + "-" * (width - filled)
return f"[{bar}] {used_gb:.1f}/{total_gb:.1f} GB"


def format_step_times(elapsed_s: list[float]) -> str:
"""Summarize per-step elapsed seconds as "N steps · X.XX s/it avg · Y.Y s total".

None/NaN/Inf entries are filtered and negative durations are clamped to 0.
An empty (or all-invalid) series returns "".
"""
vals = [max(0.0, v) for v in _finite(elapsed_s)]
if not vals:
return ""
n = len(vals)
total = sum(vals)
avg = total / n
return f"{n} steps · {avg:.2f} s/it avg · {total:.1f} s total"
Loading
Loading