Module: Agentilda::ProgressLog

Defined in:
lib/agentilda/progress_log.rb

Overview

One line of run --log FILE, laid out as fixed-width columns.

The log file is the only record of a headless run, and it is shared: two agentilda run invocations pointed at the same --log both append to it, interleaved, and so does every round within one run. That is what the process id column is for. Without it a reader cannot tell one run's agents from another's, and "started 003.00" twice looks like a bug rather than two machines doing the same work.

Fixed columns are what make the file readable by eye and by awk alike. A value wider than its column is cut rather than allowed to push the columns out, and a value that is missing still occupies its width, so a round header with no plan or agent of its own lines up with the lines underneath it.

Examples:

ProgressLog.render("editing spec.md", plan: "003.00", status: "⭐️ Planned",
  agent: "yoda-writer", seconds: 42, round: "01", pid: 91_234)
#=> "[16:22:14 | 003.00 | ⭐️ Planned            | yoda-writer          | 01 |   91234 |    42s] editing spec.md"

Constant Summary collapse

TIME_FORMAT =

Wall clock only. A log line answers "when", and the date is the file's.

"%H:%M:%S"
TIME_WIDTH =

Width of TIME_FORMAT's output.

8
PLAN_WIDTH =

"003.00".

6
STATUS_WIDTH =

Derived from the widest state as Status#to_s renders it, emoji included, so a state added to STATUSES with a longer label cannot silently knock the columns after it out of line — then trimmed by five cells: the emoji already names the state, so the longest labels can afford to lose their tails to keep the line short.

STATUSES.map { |status| UI.display_width(status.to_s) }.max - 5
AGENT_WIDTH =

The longest specialist name in agents/ is palpatine-planner at 17, and the names are hyphenated words rather than a bounded vocabulary, so this leaves room for one more without a reflow.

20
PID_WIDTH =

Enough for a 32-bit process id, right justified.

7
ROUND_WIDTH =

"01". Rounds cap at two digits; a run that reaches a third has bigger problems than this column.

2
SECONDS_WIDTH =

" 907s". Anything longer than four digits of seconds is an agent nobody is still waiting on.

6
SEPARATOR =

Between columns, inside the brackets.

" | "
COLUMN_WIDTHS =

Cells from the opening bracket to the closing one, separators included. The message starts one space after this on every line, whatever fields that line happens to be missing, so a reader indenting a wrapped message has a number to indent by.

[TIME_WIDTH, PLAN_WIDTH, STATUS_WIDTH, AGENT_WIDTH, ROUND_WIDTH, PID_WIDTH,
SECONDS_WIDTH].freeze
LINE_WIDTH =

See Also:

COLUMN_WIDTHS.sum + (SEPARATOR.length * (COLUMN_WIDTHS.size - 1)) + 2

Class Method Summary collapse

Class Method Details

.duration(seconds) ⇒ String

Returns whole seconds with a trailing "s", or blank for nil.

Parameters:

  • seconds (Numeric, nil)

Returns:

  • (String)

    whole seconds with a trailing "s", or blank for nil



102
# File 'lib/agentilda/progress_log.rb', line 102

def duration(seconds) = seconds.nil? ? "" : "#{seconds.round}s"

.left(value, width) ⇒ String

Returns padded on the right to exactly width cells.

Parameters:

  • value (Object, nil)
  • width (Integer)

    terminal cells

Returns:

  • (String)

    padded on the right to exactly width cells



107
# File 'lib/agentilda/progress_log.rb', line 107

def left(value, width) = UI.fit(value, width)

.render(message, plan: nil, status: nil, agent: nil, seconds: nil, round: nil, pid: Process.pid, at: Time.now) ⇒ String

Render one log line: bracketed fixed-width columns, then the message.

No colour. The line goes to a file, where an escape sequence is neither readable nor the width it claims to be.

Parameters:

  • message (String)

    the free-form tail, the only variable-width part

  • plan (String, nil) (defaults to: nil)

    the plan's ordinal, e.g. "003.00"

  • status (String, nil) (defaults to: nil)

    emoji and label, e.g. "⭐️ Planned"

  • agent (String, nil) (defaults to: nil)

    the specialist's name, e.g. "yoda-writer"

  • seconds (Numeric, nil) (defaults to: nil)

    how long this agent has been alive

  • round (String, nil) (defaults to: nil)

    which pass over the tree, e.g. "01"

  • pid (Integer, nil) (defaults to: Process.pid)

    which run wrote the line

  • at (Time) (defaults to: Time.now)

    injectable, so a caller can render a fixed clock

Returns:

  • (String)

    one line, with no trailing newline



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/agentilda/progress_log.rb', line 83

def render(message, plan: nil, status: nil, agent: nil, seconds: nil,
  round: nil, pid: Process.pid, at: Time.now)
  columns = [
    left(at.strftime(TIME_FORMAT), TIME_WIDTH),
    left(plan, PLAN_WIDTH),
    left(status, STATUS_WIDTH),
    left(agent, AGENT_WIDTH),
    right(round, ROUND_WIDTH),
    right(pid, PID_WIDTH),
    right(duration(seconds), SECONDS_WIDTH)
  ]

  text = message.to_s
  prefix = "[#{columns.join(SEPARATOR)}]"
  text.empty? ? prefix : "#{prefix} #{text}"
end

.right(value, width) ⇒ String

UI.fit pads on the right, which is the wrong end for a number. Fit first so an over-long value is still truncated to the column, then move the padding to the front.

Parameters:

  • value (Object, nil)
  • width (Integer)

    terminal cells

Returns:

  • (String)

    padded on the left to exactly width cells



116
117
118
119
# File 'lib/agentilda/progress_log.rb', line 116

def right(value, width)
  text = UI.fit(value, width).rstrip
  UI.fit("", width - UI.display_width(text)) + text
end