Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 12 additions & 2 deletions .agents/evidence/motion.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ resolves every one against the source, so a renamed or deleted test fails the bu

| Invariant | Proven by |
| --- | --- |
| MOT-1 | `mot_1_one_deadline_serves_every_mover_and_late_wakes_coalesce`, `the_mark_cycle_divides_the_clock_cycle`, `effort_xhigh_labels_remain_static_without_an_animation_deadline`, `effort_animation_changes_only_visible_max_cells_and_stops_when_hidden` |
| MOT-2 | `mot_2_every_activity_mark_frame_is_one_cell`, `effort_animation_changes_only_visible_max_cells_and_stops_when_hidden` |
| MOT-1 | `launch_greets_once_and_leaves`, `the_first_message_ends_the_greeting_for_good`, `a_conversation_launch_did_not_greet_shows_no_mark`, `mot_1_one_deadline_serves_every_mover_and_late_wakes_coalesce`, `the_mark_cycle_divides_the_clock_cycle`, `effort_xhigh_labels_remain_static_without_an_animation_deadline`, `effort_animation_changes_only_visible_max_cells_and_stops_when_hidden` |
| MOT-2 | `the_mark_is_braille_one_cell_a_glyph`, `mot_2_every_activity_mark_frame_is_one_cell`, `effort_animation_changes_only_visible_max_cells_and_stops_when_hidden` |
| MOT-3 | `effort_animation_changes_only_visible_max_cells_and_stops_when_hidden` |

## Rendered

Launch's greeting over a new conversation, drawn by the workspace on its motion clock with
`cargo run -p plexmaton-tui --example greeting_preview -- <directory>`: the centre grown into a
circle at [120](../../crates/plexmaton-tui/frames/greeting/greeting-circle-120.svg), [88](../../crates/plexmaton-tui/frames/greeting/greeting-circle-88.svg) and
[60](../../crates/plexmaton-tui/frames/greeting/greeting-circle-60.svg), and centre and frame turned at [120](../../crates/plexmaton-tui/frames/greeting/greeting-turned-120.svg),
[88](../../crates/plexmaton-tui/frames/greeting/greeting-turned-88.svg) and [60](../../crates/plexmaton-tui/frames/greeting/greeting-turned-60.svg). The user watched it play in
the real binary in kitty on 2026-09-23; `cargo run -p plexmaton-tui --example mark_greeting` plays
it alone in any terminal.
11 changes: 6 additions & 5 deletions .agents/phases/phase-04-product-polish.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

| Field | Value |
| --- | --- |
| Status | Active; stages 1–9 and 11–34 complete; stage 10 branding planned and unstarted |
| Status | Active; stages 1–34 complete; exit gate not yet assessed |
| Parent roadmap | [Plexmaton Roadmap](../roadmap.md) |
| Product contract | [UI/UX](../ui-ux.md) |
| Depends on | Phase 01 interaction ownership; Phase 02 journal/accounting/budget projections; Phase 03 durable collaboration, owned child scheduling, Handoff/Stop, passive recovery and canonical Attention |
Expand Down Expand Up @@ -48,10 +48,11 @@ evidence file the frames and tests. Git holds how each was validated.
9. **Composer skill picker — complete.** [SKP-1–SKP-4](../specs/composer-menu.md): `$` discovery
above the primary input with keyboard and pointer completion; literal variables, currency and
prose stay text.
10. **Branding — unstarted.** The mark, a rounded-square frame around a circular centre, drawn in
cells at the odd size nearest square for the measured cell, first centred in an empty
conversation and moving on the one [motion](../specs/motion.md) clock. The
[stage plan](../plans/phase-04-stage-10-branding.md) owns the order.
10. **Branding — complete.** Launch greets a new conversation with the mark once: drawn in braille,
a rounded-square frame around a round centre, it plays for about 2.5 s on the one
[motion](../specs/motion.md) clock and leaves the conversation blank (ui-ux §input). Chosen
in kitty from the [spike](../spikes/conversation-chrome/README.md)'s previews, after Grok's
braille logo; [frames](../evidence/motion.md#rendered).
11. **Composer menu and Drawer — complete.** A Command is a slash command typed into the
conversation it addresses ([CMC-1–CMC-3](../specs/composer-menu.md)); the Drawer, pulled by
`Ctrl-P` ([DRW-1–DRW-4](../specs/drawer.md)), holds what outlives a Session. One
Expand Down
43 changes: 0 additions & 43 deletions .agents/plans/phase-04-stage-10-branding.md

This file was deleted.

2 changes: 1 addition & 1 deletion .agents/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ table in `AGENTS.md` allows.
| 01 | One real agent in the workspace: a thin loop over one provider, and the transcript grammar against its output | Closed 2026-09-03: delivered one live OpenAI-compatible agent, five bounded native tools with explicit approval and cancellation, and the reviewed responsive transcript grammar |
| 02 | Canonical session state, persistence, durable permission policy, provider transports, context projection and project instructions | Closed 2026-09-12 by scoped evidence: delivered JSONL sessions and recovery, four provider dialects, compaction, durable permissions, AGENTS.md and conversation tree/rewind; unproven recovery/readmission acceptance inherited by Phase 03, independent export/import by Phase 04; MCP optional |
| 03 | Durable multi-agent mailbox and runtime ownership | Closed 2026-09-16: delivered ordered durable delegation/mail, owned child scheduling, Stop and Handoff control, passive, process-kill and in-app switch recovery, canonical Attention and the accepted responsive agent workspace |
| 04 | Product polish, performance hardening, math in production, and extensibility | Active; stages 1–9 and 11–34 complete; stage 10 branding planned and unstarted |
| 04 | Product polish, performance hardening, math in production, and extensibility | Active; stages 1–34 complete; exit gate not yet assessed |

## Locked

Expand Down
8 changes: 6 additions & 2 deletions .agents/specs/motion.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

| Field | Value |
| --- | --- |
| Status | Implemented for the effort rail and the activity line's mark |
| Status | Implemented for the effort rail, the activity line's mark and launch's greeting |
| Owns | When anything in the workspace moves, what a moving frame may change, and the one clock it runs on |
| Depends on | FR-1 and FR-3 in [frame-loop](./frame-loop.md); EFF-4 in [reasoning-effort](./reasoning-effort.md) as the first thing that moves |
| Proven by | `plexmaton-tui::workspace` effort tests and `plexmaton-tui::render` activity-mark tests |
| Proven by | `plexmaton-tui::workspace` effort and greeting tests and `plexmaton-tui::render` activity-mark and mark tests |

## Invariants

Expand All @@ -28,3 +28,7 @@ clock was doing.
The phase is a count, not a time: renderers derive their frame from it, so two moving things on
screen stay in step. The effort rail's marker and the activity line's mark each complete a cycle every
1.6 s, the mark over seven frames holding unequal numbers of its 24 phases.

Launch's greeting is not a cycle. It counts the clock's phases from the instant launch greeted,
plays its 38 once (ui-ux §input), and then, or at the conversation's first entry, stops for good
and wakes the clock no more; the braille mark it draws is one cell a glyph (MOT-2).
12 changes: 7 additions & 5 deletions .agents/spikes/conversation-chrome/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
| Field | Value |
| --- | --- |
| Question | How the working agent, the user's turn and the mark should look, decided from rendered candidates |
| Feeds | [Phase 04](../../phases/phase-04-product-polish.md) stage 34, complete, and the [stage 10](../../plans/phase-04-stage-10-branding.md) plan |
| Feeds | [Phase 04](../../phases/phase-04-product-polish.md) stages 34 and 10, both complete |
| Method | HTML mockups in the palette, opened for the user; two kitty scripts for what a browser cannot show |

Every decision below was made by the user on a rendered frame, 2026-09-21 and 2026-09-22. The
generators are in [`mockups/`](./mockups/); each writes one HTML page to
`plexmaton-conversation-chrome/` under the system temporary directory, outside this corpus, and
`spin.py`, `mark-lively.py`, `logo_term.py` and `cellsize.py` run in a terminal. Regenerate rather than trusting a description of them.
`spin.py`, `mark-lively.py`, `mark-braille.py`, `mark-braille-frame.py`, `logo_term.py` and `cellsize.py` run in a terminal. Regenerate rather than trusting a description of them.

## What the user chose

Expand All @@ -18,9 +18,10 @@ generators are in [`mockups/`](./mockups/); each writes one HTML page to
| Activity line composition | mark, label, muted `· 32s · high effort`, `· quiet for 31s` once nothing has arrived; no token count | a red stall tell; estimated tokens |
| Activity mark | H: a dot grows into a circle, turns into a rounded square and a square, spins into a diamond and shrinks away through a smaller one, lingering on whole shapes, 1.6 s a cycle; chosen 2026-09-23 in kitty (`mark-lively.py`) after the user found N2 rigid and wanted it to grow from a point | N2, the circle, rounded square and square outline then filled, chosen 2026-09-22 and later found rigid; one size changing shape, and even steps retracing their path, likewise; a slice-by-slice sweep, which reads as progress; C2′ and C2″, which shook because mixed-size Unicode shapes centre differently; filled squares; braille; quarter circles; Claude Code's star |
| User message | band of lifted ground with a blue `›`, continuation indented | the accent bar; a bare prefix; a hairline bar; a band alone |
| Mark placement | centred in the empty conversation, name beneath, gone with the first message | a header or drawer slot, not yet asked for |
| Mark motion | K4 as the base: frame breathes between weights and drifts between palette slots; centre morphs circle, ring, rounded square, square, solid and outline | K1 to K3 as they stood |
| Mark glyphs | the Material Design family the Nerd Font carries, one cell each, drawn together so they centre together | Unicode geometric shapes from mixed blocks, which jump between fallback fonts |
| Mark placement | a greeting: at launch onto an empty conversation it plays once at the centre, name beneath, and leaves after about 2.5 s: frame fades in for 0.25 s, the F3 cycle runs in 2 s with one 0.8 s sheen, frame and name fade out in 0.3 s; chosen 2026-09-23 after the user asked for it to be less aggressive and quicker | staying and moving until the first message, too insistent; 3.7 s, too slow; a header or drawer slot, not yet asked for |
| Mark drawing | braille, the way Grok draws its logo: two by four dots a cell, a rounded-square frame around a round centre as one small bitmap, square once corrected for the terminal's cell; chosen 2026-09-23 (`mark-braille.py`) | box drawing at three weights around a one-cell Nerd Font centre, which the user found too coarse to look good at any size |
| Mark motion | F3: the centre grows from a dot into a circle, becomes a rounded square and a square, turns into a diamond while the frame turns with it, and shrinks away, 3.2 s a cycle; Grok's sheen sweeps it every 4 s; chosen 2026-09-23 (`mark-braille-frame.py`) after the user asked for the outside to move as well as the inside | the frame breathing and drifting through palette slots (F1); frame and centre in counterpoint (F2); all of it at once (F4); K4's shape-only morph, rigid |
| Activity mark glyphs | the Material Design family the Nerd Font carries, one cell each, drawn together so they centre together | Unicode geometric shapes from mixed blocks, which jump between fallback fonts |

The activity indicator and the mark are two things: the row has one cell, the empty conversation
has a block. They share the clock and the glyph family, not the geometry.
Expand Down Expand Up @@ -60,3 +61,4 @@ the family's, so they cannot join its cycle; `▢`, `◼` and `⬤` are not in t
| Stall | glyph and verb fade to red after 3 s without tokens | | phase timer never resets on payload churn |
| User turn | band, subtle `❯`, no hanging indent, text colour unchanged | band blended 12 % toward white from the real terminal background, dim `›`, two-space indent, never truncated | band, accent `❯`, indent, folds past three lines |
| Clock | one shared interval, halved when the window is blurred, off when scrolled away | | demand-driven; idle parks with no wakeups; every frame unit-tested to one column |
| Logo | | | braille art at seven or five rows by window height, none below; a sheen sweeps bottom-left to top-right in 1.3 s of every 4, over a slow pulse, redrawn at 12 fps |
124 changes: 124 additions & 0 deletions .agents/spikes/conversation-chrome/mockups/mark-braille-frame.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""The braille mark with the frame moving too, around the centre that grows and turns. Ctrl-C to stop."""
import math, sys, time

sys.path.insert(0, __import__("os").path.dirname(__file__))
base = __import__("mark-braille") # noqa: E402

DRIFT = [(130, 180, 240), (139, 92, 246), (217, 70, 239), (139, 92, 246), (130, 180, 240), (120, 210, 205)]
CYCLE = 3.2
ease = lambda k: 0.5 - 0.5 * math.cos(math.pi * max(0.0, min(1.0, k)))


def drift(t, period=4.8):
x = (t / period * len(DRIFT)) % len(DRIFT)
i = int(x)
return base.mix(DRIFT[i], DRIFT[(i + 1) % len(DRIFT)], x - i)


def phase(t):
return (t % CYCLE) / CYCLE


def squareness(t):
"""0 while the centre is a circle, 1 once it has become a square (follows growing_core)."""
p = phase(t)
if p < 0.30:
return 0.0
if p < 0.48:
return ease((p - 0.30) / 0.18)
return 1.0 if p < 0.84 else 1.0 - ease((p - 0.84) / 0.16)


def turned(t):
"""0 upright, 1 turned a quarter of a right angle further (the diamond), following the centre."""
p = phase(t)
if p < 0.60:
return 0.0
if p < 0.78:
return ease((p - 0.60) / 0.18)
return 1.0 if p < 0.84 else 1.0 - ease((p - 0.84) / 0.16)


def frame(t, breathe=False, counterpoint=False, turn=False):
"""(inside-the-ring test, colour) for the frame at time t."""
thickness = 0.075
if breathe:
thickness = 0.05 + 0.06 * (0.5 - 0.5 * math.cos(math.tau * t / 1.6))
radius = 0.36
if counterpoint:
radius = 0.20 + 0.66 * squareness(t) # square-ish around a circle, a circle around a square
angle = turned(t) * math.pi / 4 if turn else 0.0
scale = 1.0 / (1.0 + (math.sqrt(2) - 1.0) * math.sin(angle * 2) ** 2) if turn else 1.0
half = 0.9 * scale

def ring(x, y):
c, s = math.cos(angle), math.sin(angle)
x, y = x * c + y * s, -x * s + y * c
return abs(base.rounded_box(x, y, half, min(radius, half))) < thickness
return ring


def draw(mark, core, ring, frame_ink, shine=None):
lines = []
w, h = mark.cols * 2, mark.rows * 4
cx, cy = w * mark.dw / 2, h * mark.dh / 2
for row in range(mark.rows):
line = ""
for col in range(mark.cols):
bits, frame_dots, core_dots = 0, 0, 0
for dy in range(4):
for dx in range(2):
px = ((col * 2 + dx + 0.5) * mark.dw - cx) / mark.half
py = ((row * 4 + dy + 0.5) * mark.dh - cy) / mark.half
on_ring, inside = ring(px, py), core(px, py)
if on_ring or inside:
bits |= base.DOTS[dy][dx]
frame_dots += on_ring
core_dots += inside and not on_ring
ink = frame_ink if frame_dots >= core_dots else base.PURPLE
if shine:
ink = base.mix(ink, base.TEXT, shine(col, row, mark.cols, mark.rows))
line += f"\x1b[38;2;{ink[0]};{ink[1]};{ink[2]}m{chr(0x2800 + bits)}" if bits else " "
lines.append(line + "\x1b[0m")
return lines


CANDIDATES = [
("F1 the frame breathes and drifts, with the sheen", lambda t: (frame(t, breathe=True), drift(t), base.grok_shine(t))),
("F2 inside and outside in counterpoint, with the sheen", lambda t: (frame(t, counterpoint=True), base.BLUE, base.grok_shine(t))),
("F3 they turn together, with the sheen", lambda t: (frame(t, turn=True), base.BLUE, base.grok_shine(t))),
("F4 all of it: breathe, drift, counterpoint, turn, sheen", lambda t: (frame(t, True, True, True), drift(t), base.grok_shine(t))),
]


def main():
cell = base.cell_px()
marks = [base.Mark(7, cell), base.Mark(5, cell)]
print(f"\n cell {cell[0]:.1f}×{cell[1]:.1f} px; marks {marks[0].cols}×7 and {marks[1].cols}×5 cells\n")
height = 7 + 3
sys.stdout.write("\x1b[?25l")
start = time.time()
try:
while True:
t = math.floor((time.time() - start) * base.FPS) / base.FPS
core = base.growing_core(t)
for name, make in CANDIDATES:
ring, ink, shine = make(t)
blocks = [draw(m, core, ring, ink, shine) for m in marks]
for row in range(7):
small = blocks[1][row - 1] if 1 <= row <= 5 else " " * marks[1].cols
sys.stdout.write(f" {blocks[0][row]} {small}\x1b[K\n")
name_row = f"\x1b[38;2;230;233;240m{'Plexmaton':^{marks[0].cols}}\x1b[0m"
sys.stdout.write(f" {name_row} \x1b[38;2;142;162;196m{name}\x1b[0m\x1b[K\n\n\n")
sys.stdout.write(f"\x1b[{height * len(CANDIDATES)}A")
sys.stdout.flush()
time.sleep(1 / base.FPS / 2)
except KeyboardInterrupt:
pass
finally:
sys.stdout.write(f"\x1b[{height * len(CANDIDATES)}B\x1b[?25h\x1b[0m\n")


if __name__ == "__main__":
main()
Loading
Loading