Skip to content

feat(runner): stream fullsend's console live under the progress prefix - #286

Open
astefanutti wants to merge 6 commits into
mainfrom
feat/fullsend-runner-stream-progress
Open

astefanutti wants to merge 6 commits into
mainfrom
feat/fullsend-runner-stream-progress

Conversation

@astefanutti

Copy link
Copy Markdown
Collaborator

Why

With a log prefix (batch mode's eval, eval:<case> per case) the fullsend runner printed only Running: <cmd> and OK/FAIL (Ns), so a CI job log is silent for the whole run: 13 minutes for two cases, up to two hours for a full batch, indistinguishable from a stalled barrier or a hung sandbox until the eval timeout. The claude-code runner streams its progress under the same <prefix> | lines (_extract_progress); this gives the fullsend runner the same behaviour.

What

  • FullsendRunner._run reads fullsend's stdout and stderr concurrently (one daemon thread per pipe), keeps the full text for RunResult.stderr exactly as communicate() returned it (stdout, then stderr; the provenance parse of the Image: line reads the same text), and forwards a filtered subset live: section and step lines (→ • ✓ ✗ !) with their indented detail lines (plan block, → Results), agent activity lines (💬 ⚙ ⏳), plain Error/Warning/fatal lines and Claude Code's Ignoring N permissions.allow entries … line, each cut at 200 characters, with credential assignments (…token=, …secret=, …password=, …api_key=) shown as KEY=***. Not forwarded: the per-finding ! Sanitized […] lines, the assistant's thinking (🧠, which the claude-code runner does not stream either) and multi-line text, and pre/post-script output. The real two-case console (1,096 lines) forwards 360 lines, 30 KB, so a 26-case batch stays well inside a GitLab runner's default 4 MB output limit.
  • Draining never depends on printing: a failed forward (harness stdout closed, marker not encodable) stops forwarding only, and the reader keeps emptying the pipe. Each reader closes its pipe at EOF.
  • The wait keeps communicate(timeout)'s contract (both pipes at EOF and the process exited within the timeout, else the group is torn down with the leader still unreaped). terminate_process_group grows a keyword-only capture_output=False for a caller whose own threads read the pipes. A descendant outside the group still holding a pipe after the bounded drain is noted in the case's stderr (Console pipes still open after the group kill). Undecodable console bytes become U+FFFD instead of raising out of the case.
  • No config knob: streaming follows the harness's progress logging, as for the claude-code runner. Docs: website/reference/config/runner.md "Live progress", website/concepts/runners.md.

Tests

tests/test_fullsend_runner.py: the fake binary replays a recorded fullsend console; new TestConsoleFilter and TestFullsendConsoleStreaming cover lines forwarded under the prefix in order (stdout and stderr), dropped lines, byte-for-byte capture with provenance still parsing Image:, no streaming without a prefix, both pipes drained past the pipe buffer (1.8 MB each), a forward failure that keeps draining (ASCII sink, broken pipe), the timeout path (streams, then terminates; pipes held past the group kill), pipes closed once drained, thinking not forwarded, credential redaction. tests/test_config.py: terminate_process_group(capture_output=False). Full suite: 2788 passed, 8 skipped. mkdocs build --strict passes.

Not exercised against a live fullsend binary or a GitLab job yet; the line shapes come from a fullsend 0.51 console. A shape fullsend changes later falls back to "dropped" (still captured), never to an error. The first consumer is the rfe-creator-eval fullsend job (fullsend-ai/rfe-creator eval-profiles/fullsend.yaml).

With a log prefix (batch mode's "eval", "eval:<case>" per case) the fullsend
runner printed only "Running: <cmd>" and "OK/FAIL (Ns)", so a CI job log was
silent for the whole run: 13 minutes for two cases, up to two hours for a
full batch. The claude-code runner streams its progress under the same
"  <prefix> | " lines.

_run now reads fullsend's stdout and stderr concurrently (one daemon thread
per pipe), keeps the full text for RunResult.stderr as communicate()
returned it (stdout, then stderr; the provenance parse of the Image: line
reads the same text), and forwards a filtered subset as it arrives: the
section and step lines (→ • ✓ ✗ !) with the detail lines indented under
them (plan block, → Results), the agent activity lines (💬 ⚙ ⏳), plain
Error/Warning/fatal lines and Claude Code's "Ignoring N permissions.allow
entries" line (the one behind the eval permission denials), each cut at 200
characters with credential assignments (…token=, …secret=, …password=,
…api_key=) shown as KEY=***. The per-finding "! Sanitized […]" lines, the
assistant's thinking (🧠, which the claude-code runner does not stream
either) and multi-line text and the pre/post script output are not
forwarded; the capture keeps everything, unredacted. The real two-case
console (1,096 lines) forwards 360 lines, 30 KB.

Draining never depends on printing: a forward that fails (the harness's
stdout closed, or unable to encode a marker) stops the forwarding and the
reader keeps emptying the pipe, so fullsend cannot stall on a full pipe
until the timeout. Each reader closes its pipe at EOF, as communicate() did.

The wait keeps communicate(timeout)'s contract (both pipes at EOF and the
process exited within the timeout, else the group is torn down with the
leader still unreaped), and terminate_process_group grows a keyword-only
capture_output=False for a caller whose own threads read the pipes, so the
teardown only reaps instead of competing for them. A descendant outside the
group still holding a pipe after the bounded drain is noted in the case's
stderr ("Console pipes still open after the group kill"), as the
communicate(timeout=5) it replaces reported it. Undecodable console bytes
become U+FFFD instead of raising out of the case.

No config knob: streaming follows the harness's progress logging, as it
does for the claude-code runner; no runner has a console-verbosity key.

Signed-off-by: Antonin Stefanutti <antonin@stefanutti.fr>
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Stream fullsend console progress in runner logs

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Stream selected fullsend steps and agent activity under the harness progress prefix.
• Preserve complete console capture while bounding forwarded lines and redacting credential
 assignments.
• Keep pipe draining and process-group teardown safe across timeouts and forwarding failures.
Diagram

sequenceDiagram
    participant H as Harness
    participant R as FullsendRunner
    participant P as fullsend
    participant C as ConsoleCapture
    participant F as ConsoleFilter
    participant L as ProgressLog
    participant T as GroupTeardown
    H->>R: Execute case
    R->>P: Start process
    R->>C: Read both pipes
    par stdout reader
        P-->>C: stdout lines
    and stderr reader
        P-->>C: stderr lines
    end
    C->>F: Select lines
    F-->>L: Prefix safe progress
    alt timeout
        R->>T: Terminate group
        T-->>R: Reap leader
        R->>C: Bounded drain
    end
    C-->>R: Complete console
    R-->>H: RunResult
Loading
High-Level Assessment

The two-reader approach fits the existing synchronous runner: it drains both pipes without blocking on live printing and retains the console capture contract. Merging pipes would lose stdout-then-stderr capture; an asynchronous subprocess would require a broader runner redesign.

Files changed (6) +672 / -22

Enhancement (2) +200 / -8
base.pyAllow process-group teardown without consuming pipes +13/-3

Allow process-group teardown without consuming pipes

• Adds a keyword-only capture_output option so callers with their own pipe readers can terminate and reap the process without calling communicate. Existing callers retain output capture by default.

agent_eval/agent/base.py

fullsend.pyCapture and selectively stream fullsend console output +187/-5

Capture and selectively stream fullsend console output

• Drains stdout and stderr on separate threads, preserves their complete text for results and provenance, and forwards filtered progress under the log prefix. Redacts credential assignments and clips forwarded lines; timeout handling bounds the final drain and reports pipes held open after teardown.

agent_eval/agent/fullsend.py

Tests (2) +441 / -13
test_config.pyTest teardown with caller-owned pipes +16/-0

Test teardown with caller-owned pipes

• Verifies that capture_output=False reaps the process while leaving its pipes open for the caller's readers.

tests/test_config.py

test_fullsend_runner.pyCover console filtering, streaming, and teardown +425/-13

Cover console filtering, streaming, and teardown

• Extends the fake fullsend binary to replay console files and tests selected and dropped lines, redaction, complete capture, and prefix behavior. Also covers concurrent draining, forwarding failures, timeouts, held-open pipes, pipe closure, and interrupt cleanup.

tests/test_fullsend_runner.py

Documentation (2) +31 / -1
runners.mdDescribe fullsend's live console progress +4/-1

Describe fullsend's live console progress

• Explains that fullsend console output remains in stderr while selected progress streams live during a run.

website/concepts/runners.md

runner.mdDocument live-progress selection and limits +27/-0

Document live-progress selection and limits

• Details forwarded line types, redaction, clipping, unfiltered capture, and the lack of a separate configuration knob. Notes delayed delivery of unterminated lines and the diagnostic for pipes still open after a timeout.

website/reference/config/runner.md

@fullsend-ai-review

fullsend-ai-review Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 4:03 PM UTC · Completed 4:10 PM UTC

Commit: c1b8ce6 · View workflow run →

Runtime: pi · Model: sonnet → claude-sonnet-5-5 · Effort: high · Cost: $1.83

@qodo-code-review

qodo-code-review Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Interrupts leave console readers running ✗ Dismissed
Description
_run calls terminate_process_group(..., capture_output=False) on BaseException and re-raises
without joining or closing the reader-owned pipes. If a descendant outside the killed group retains
a pipe, its daemon reader stays blocked with that pipe open after the interrupted run has unwound.
Code

agent_eval/agent/fullsend.py[481]

+            terminate_process_group(proc, self._term_grace_s, capture_output=False)
Relevance

●●● Strong

Recent PR #229 accepted terminating, reaping, and draining subprocess output during exceptional
cleanup.

PR-#229

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new teardown mode waits only for the process, while the interruption branch immediately
re-raises. Reader threads are daemons that block in pipe iteration until EOF, and the test
demonstrates how a descendant outside the group can keep those pipes open.

agent_eval/agent/fullsend.py[478-482]
agent_eval/agent/fullsend.py[594-605]
agent_eval/agent/base.py[163-167]
agent_eval/agent/base.py[192-195]
tests/test_fullsend_runner.py[2660-2677]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The interruption path abandons reader-owned pipes and daemon threads when a surviving descendant keeps a pipe open.
## Fix Focus Areas
- agent_eval/agent/fullsend.py[478-482]
- agent_eval/agent/fullsend.py[588-634]
## Recommended Fix
Add bounded reader cleanup to the interruption path and a safe way to stop forwarding and release reader-owned resources when EOF cannot be reached. Cover interruption with a descendant outside the process group that retains a pipe.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Timed-out cases print into later case logs ✗ Dismissed
Description
_run returns after the bounded drain when a pipe remains open, but _ConsoleCapture._pump keeps
forwarding from that pipe under the completed case’s prefix. If a descendant outside fullsend’s
process group writes another line later, it can appear after the case’s FAIL line and among
subsequent cases’ progress.
Code

agent_eval/agent/fullsend.py[R467-471]

+            if not console.join(_PIPE_DRAIN_S):
+                # A descendant outside the group still holds a pipe (what
+                # communicate(timeout) reported as the termination hanging):
+                # the console may be cut short and its reader is still up.
+                hung = hung or "Console pipes still open after the group kill"
Relevance

●●● Strong

Recent PR #229 accepted bounded pipe draining for descendants holding inherited stdout after process
termination.

PR-#229

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The timeout path explicitly returns when a reader is still alive; that reader retains its prefix and
print path. The new test establishes that a descendant in another session can keep the pipe open
beyond the drain deadline.

agent_eval/agent/fullsend.py[465-476]
agent_eval/agent/fullsend.py[600-619]
tests/test_fullsend_runner.py[2660-2684]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A reader can continue printing after a timed-out case returns because a surviving descendant still holds its pipe.
## Fix Focus Areas
- agent_eval/agent/fullsend.py[467-476]
- agent_eval/agent/fullsend.py[600-619]
## Recommended Fix
Disable forwarding for both readers before returning from an incomplete drain, using synchronization that also prevents an in-flight print from occurring after the case's final status line. Keep draining or safely close the reader-owned pipes as appropriate, and test a surviving descendant that writes after the bounded drain.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 89 rules
✅ Skills: 5 invoked
  eval-analyze
  eval-anova
  eval-mlflow
  eval-run
  eval-dataset
✅ Cross-repo context — repo relationships
Review mode: Auto: ⚖️ Balanced: Downgraded extended -> standard: change is below the extended eligibility bar (hunks 15/18, lines 694/200; both must reach the floor). Router rationale: Concurrent streaming, teardown, filtering, security redaction, and capture add many independent failure modes.

Grey Divider

Tip of the day
💡 Did you know, you can route each severity your way: inline, summary, both, or drop

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread agent_eval/agent/fullsend.py Resolved
Comment thread agent_eval/agent/fullsend.py Resolved
@fullsend-ai-review fullsend-ai-review Bot added the risk/moderate PR risk: moderate label Oct 28, 2026
@fullsend-ai-review

fullsend-ai-review Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

<!-- fullsend:risk-assessment -->
Risk Assessment: moderate (2/5)

Details

Moderate risk change expanding live console streaming across 6 files and 1,652 lines with solid test coverage, zero protected or security-sensitive paths, and moderate churn keeping the assessment anchored at moderate.

Previous run

<!-- fullsend:risk-assessment -->
Risk Assessment: moderate (2/5)

Details

Moderate risk change expanding live console streaming across 6 files and 1,647 lines with solid test coverage, zero protected or security-sensitive paths, and moderate churn keeping the assessment anchored at moderate.

Previous run (2)

<!-- fullsend:risk-assessment -->
Risk Assessment: moderate (2/5)

Details

Moderate risk change expanding live console streaming across 6 files and 1,049 lines with solid test coverage, zero protected or security-sensitive paths, and moderate churn keeping the assessment anchored at moderate.

Previous run (3)

<!-- fullsend:risk-assessment -->
Risk Assessment: moderate (2/5)

Details

Moderate change adding live console streaming across 6 files and ~700 lines with solid test coverage, zero protected or security-sensitive paths, and moderate churn.

@fullsend-ai-review

fullsend-ai-review Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Review

Findings

Medium

  • [secret-exposure] agent_eval/agent/fullsend.py:197 — Authorization redaction misses JSON-quoted Basic and token values. _BEARER_RE needs authorization: with the colon right after the name, which a JSON key such as "Authorization": does not have, and its standalone branch only covers bearer. _SECRET_KEY_RE does not count authorization as a credential key, so the field redactor skips it too. A forwarded line containing {"Authorization":"Basi..."} or "Authorization":"***" goes out unredacted. Environment-value masking does not cover credentials read from files or created during the run. This also carries the open credential-redaction finding on the _present pipeline (~line 855), which names no separate defect in the current code.
    Remediation: Treat Authorization and Proxy-Authorization as credential keys whatever the scheme: add them to _SECRET_KEY_RE, or let _BEARER_RE accept an optionally quoted key followed by any scheme. Add tests for Basic, Bearer, token and custom schemes, as plain headers and as JSON.

  • [secret-exposure] agent_eval/agent/fullsend.py:970 — _mask_userinfo keeps the username verbatim when the userinfo contains a colon (userinfo.split(':', 1)[0] + ':***'). In token-as-username URLs such as https://<token>:x-oauth-basic@github.com/org/repo the token is forwarded and only the placeholder password is masked, unless the token is also the value of a credential-named environment variable of 8+ characters.
    Remediation: Return the scheme, *** and the terminator for any userinfo, dropping the username, including the clipped-URL form. Add tests for a token used as the username with a placeholder password.

Low

  • [secret-exposure] agent_eval/agent/fullsend.py:917 — Environment-value masking skips values of credential-named variables shorter than _MIN_MASKED_VALUE_CHARS (8). A short password or PIN that appears in a forwarded line without a credential-labelled key (for example Error: login rejected for short) is not masked. The threshold is a documented trade-off, and key-based redaction still covers password=short, --password short and password: short.
    Remediation: Optional: mask short credential values only as whole words, and add a test for a short password appearing without its key.

  • [edge-case] agent_eval/agent/fullsend.py:748 — Live forwarding calls print() while holding the global _print_lock. If the harness's stdout is backpressured, the reader blocks inside print() and keeps the lock. _ConsoleCapture.detach() (line ~781), called from _run's finally block, takes the same lock, so _run cannot return within the timeout plus the bounded drain until the sink drains. A stdout that blocks forever already stalls the harness's other prints under the same lock, so impact is narrow.
    Remediation: Optional: keep the blocking print out of the lock detach() needs (for example a bounded queue drained by a separate printer thread) and add a regression test with a blocked stdout sink.

Earlier findings

  • agent_eval/agent/fullsend.py:748, injection-vuln, reclassified (info) — each forwarded line starts with the harness-supplied prefix, so a :: workflow command is not recognized.

Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

1 finding(s) below the low severity threshold were not posted.

Previous run

<!-- fullsend:review-findings-v2:eyJ2ZXJzaW9uIjoyLCJhY3Rpb24iOiJyZXF1ZXN0LWNoYW5nZXMiLCJmaW5kaW5ncyI6W3sic2V2ZXJpdHkiOiJtZWRpdW0iLCJjYXRlZ29yeSI6InNlY3JldC1leHBvc3VyZSIsImZpbGUiOiJhZ2VudF9ldmFsL2FnZW50L2Z1bGxzZW5kLnB5IiwiaWQiOiJmXzRlNzY2Y2Q5NTYxN2E1ZWMiLCJsaW5lIjo4NTUsImFjdGlvbmFibGUiOnRydWV9LHsic2V2ZXJpdHkiOiJsb3ciLCJjYXRlZ29yeSI6ImVkZ2UtY2FzZSIsImZpbGUiOiJhZ2VudF9ldmFsL2FnZW50L2Z1bGxzZW5kLnB5IiwiaWQiOiJmXzk1MWUxZDM4Y2E0YzRhYzEiLCJsaW5lIjo3NDgsImFjdGlvbmFibGUiOmZhbHNlfSx7InNldmVyaXR5IjoiaW5mbyIsImNhdGVnb3J5IjoiaW5qZWN0aW9uLXZ1bG4iLCJmaWxlIjoiYWdlbnRfZXZhbC9hZ2VudC9mdWxsc2VuZC5weSIsImlkIjoiZl8wZTgwZWJiZTZkNWMyNDEzIiwibGluZSI6NzQ4LCJhY3Rpb25hYmxlIjpmYWxzZX0seyJzZXZlcml0eSI6ImxvdyIsImNhdGVnb3J5IjoibG9naWMtZXJyb3IiLCJmaWxlIjoiYWdlbnRfZXZhbC9hZ2VudC9mdWxsc2VuZC5weSIsImlkIjoiZl8xOTVhZDg0Y2I0YWVkNjVkIiwibGluZSI6MTQxLCJhY3Rpb25hYmxlIjp0cnVlfV0sImRpc3Bvc2l0aW9ucyI6W3siaWQiOiJmXzRlNzY2Y2Q5NTYxN2E1ZWMiLCJzdGF0dXMiOiJvcGVuIn0seyJpZCI6ImZfMTk1YWQ4NGNiNGFlZDY1ZCIsInN0YXR1cyI6InJlc29sdmVkX2J5X2NoYW5nZSJ9LHsiaWQiOiJmXzBlODBlYmJlNmQ1YzI0MTMiLCJzdGF0dXMiOiJyZWNsYXNzaWZpZWQifV19 -->

Review

Findings

Medium

  • [secret-exposure] agent_eval/agent/fullsend.py:855 — Credential redaction of the forwarded console lines (_present / _redact) still has gaps, on both stdout and stderr. (1) Error: {"Authorization":"Basi..."} is forwarded unchanged: _BEARER_RE needs an unquoted authorization: name, and Authorization is not in _SECRET_KEY_RE, so the compact-JSON field value is kept. (2) _mask_userinfo keeps the part before the first :, so https://ghp_example:x-oauth-basic@github.com/repo is shown as https://ghp_example:***@... and a token used as the username stays visible. (3) Known environment secrets shorter than _MIN_MASKED_VALUE_CHARS (8) are not replaced, so DB_PASSWORD=hunter2 followed by Error: login rejected for hunter2 leaks; this one is a deliberate threshold and is the weakest of the three. The examples are under the 200-character cut, so clipping does not help. These lines go to CI job logs.
    Remediation: Treat authorization as a credential key (or let _BEARER_RE accept a quoted header name and any scheme). Mask the whole URL userinfo, not just the password part. Consider lowering or dropping the minimum masked length. Add regression tests for these inputs on both pipes.

Low

  • [edge-case] agent_eval/agent/fullsend.py:748 — The reader calls print(..., flush=True) while holding the module-wide _print_lock (lines 741-748), and _ConsoleCapture.detach() (line 781) takes the same lock with no timeout. If the harness's stdout stays open but stops draining, the reader blocks in print, stops draining fullsend's pipe, and the timeout path's detach() then blocks as well. The existing status prints (the Running: line and the per-case status line) already take the same lock, so a stalled stdout already hangs the harness. This PR adds one more place to block rather than a new kind of failure. Optional hardening.
    Remediation: Let detach() set the flag without waiting on the print lock (or use a bounded acquire), and/or forward through a bounded queue that drops lines when the sink stalls.

Earlier findings

  • agent_eval/agent/fullsend.py, logic-error — resolved: the credential-assignment regex was replaced by a bounded forward-scanning redaction, with regression tests for pathological lines.
  • agent_eval/agent/fullsend.py, injection-vuln — reclassified to info: every forwarded line is printed as {log_prefix} | {shown}, so child text can never start a line with ::. shown also has control characters stripped, and log_prefix is set by the harness rather than by the child.

Labels: The PR adds a new live-streaming capability to the fullsend runner.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

1 finding(s) below the low severity threshold were not posted.

Previous run (2)

<!-- fullsend:review-findings-v2:eyJ2ZXJzaW9uIjoyLCJhY3Rpb24iOiJyZXF1ZXN0LWNoYW5nZXMiLCJmaW5kaW5ncyI6W3sic2V2ZXJpdHkiOiJtZWRpdW0iLCJjYXRlZ29yeSI6InNlY3JldC1leHBvc3VyZSIsImZpbGUiOiJhZ2VudF9ldmFsL2FnZW50L2Z1bGxzZW5kLnB5IiwiaWQiOiJmXzRlNzY2Y2Q5NTYxN2E1ZWMiLCJsaW5lIjo3NTQsImFjdGlvbmFibGUiOnRydWV9LHsic2V2ZXJpdHkiOiJsb3ciLCJjYXRlZ29yeSI6ImxvZ2ljLWVycm9yIiwiZmlsZSI6ImFnZW50X2V2YWwvYWdlbnQvZnVsbHNlbmQucHkiLCJpZCI6ImZfMTk1YWQ4NGNiNGFlZDY1ZCIsImxpbmUiOjE0MSwiYWN0aW9uYWJsZSI6dHJ1ZX0seyJzZXZlcml0eSI6ImxvdyIsImNhdGVnb3J5IjoiaW5qZWN0aW9uLXZ1bG4iLCJmaWxlIjoiYWdlbnRfZXZhbC9hZ2VudC9mdWxsc2VuZC5weSIsImlkIjoiZl8wZTgwZWJiZTZkNWMyNDEzIiwibGluZSI6NjYzLCJhY3Rpb25hYmxlIjp0cnVlfV0sImRpc3Bvc2l0aW9ucyI6W3siaWQiOiJmXzE5NWFkODRjYjRhZWQ2NWQiLCJzdGF0dXMiOiJyZWNsYXNzaWZpZWQifSx7ImlkIjoiZl80ZTc2NmNkOTU2MTdhNWVjIiwic3RhdHVzIjoib3BlbiJ9LHsiaWQiOiJmXzBlODBlYmJlNmQ1YzI0MTMiLCJzdGF0dXMiOiJvcGVuIn1dfQ== -->

Review

Findings

Medium

  • [secret-exposure] agent_eval/agent/fullsend.py:754 — Credential redaction before console lines are forwarded to CI logs is still incomplete. _SECRET_ASSIGNMENT_RE only redacts KEY=value where the key contains token/secret/password/api_key. The new commit fixes quoted values that fullsend's 120-rune cut leaves unterminated, and escaped quotes. Two kinds of exposure remain. (1) Partial redaction: TOKEN=ab"c d, TOKEN=abc\ def and "api_key=ab cd" fall to the bare \S+ branch, so the part after the first blank stays visible (the code comment at lines 136-138 concedes this). (2) No redaction: space-separated flags such as --password swordfish, Authorization: Bearer ..., JSON "token": "value", URL userinfo, and keys such as AWS_ACCESS_KEY_ID. For example, ⚙ Bash: curl --password swordfish https://example.test is selected by the activity filter and printed unchanged. The raw capture is unredacted by design.
    Remediation: Do not forward raw command arguments unless they are safely rendered. Cover shell word concatenation, space-separated credential flags and header/structured forms. Consider masking known secret values taken from the environment independently of their labels.

Low

  • [logic-error] agent_eval/agent/fullsend.py:141 — The key matcher \b(\w*(?:token|secret|passw(?:or)?d|api[_-]?key)\w*=) backtracks quadratically on one long word that repeats the credential name and has no = (e.g. Error: + token * N). _present runs the regex before the 200-character clip, so the clip does not bound the work. The reviewer measured about 1.3 s at 50 KB and 5.4 s at 100 KB, which can stall both pipe readers and delay timeout handling while the GIL is held. Severity is low: it is quadratic rather than catastrophic and needs a pathological unbroken line.
    Remediation: Find candidate keys once (e.g. \b(\w+)=), then test the captured key for a credential substring in a callback. Add a long-line regression test.

  • [injection-vuln] agent_eval/agent/fullsend.py:663 — _CONTROL_RE now strips CSI/OSC/DCS/SOS/PM/APC/ESC sequences and C0/C1 controls (CR/LF included). It still lets Unicode bidi controls (U+202A–U+202E, U+2066–U+2069) and U+2028/U+2029 through, so a forwarded line can visually reorder or spoof text in CI log viewers.
    Remediation: Strip or escape bidi controls and line/paragraph separators in forwarded lines, and add tests.

Earlier findings

  • agent_eval/agent/fullsend.py:129, logic-error — reclassified from medium to low; the regex backtracking is real but is bounded, quadratic and needs a pathological line (see Low above).

Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (3)

<!-- fullsend:review-findings-v2:eyJ2ZXJzaW9uIjoyLCJhY3Rpb24iOiJyZXF1ZXN0LWNoYW5nZXMiLCJmaW5kaW5ncyI6W3sic2V2ZXJpdHkiOiJtZWRpdW0iLCJjYXRlZ29yeSI6ImxvZ2ljLWVycm9yIiwiZmlsZSI6ImFnZW50X2V2YWwvYWdlbnQvZnVsbHNlbmQucHkiLCJpZCI6ImZfMTk1YWQ4NGNiNGFlZDY1ZCIsImxpbmUiOjEyOSwiYWN0aW9uYWJsZSI6dHJ1ZX0seyJzZXZlcml0eSI6Im1lZGl1bSIsImNhdGVnb3J5Ijoic2VjcmV0LWV4cG9zdXJlIiwiZmlsZSI6ImFnZW50X2V2YWwvYWdlbnQvZnVsbHNlbmQucHkiLCJpZCI6ImZfNGU3NjZjZDk1NjE3YTVlYyIsImxpbmUiOjY4OSwiYWN0aW9uYWJsZSI6dHJ1ZX0seyJzZXZlcml0eSI6ImxvdyIsImNhdGVnb3J5IjoiaW5qZWN0aW9uLXZ1bG4iLCJmaWxlIjoiYWdlbnRfZXZhbC9hZ2VudC9mdWxsc2VuZC5weSIsImlkIjoiZl8wZTgwZWJiZTZkNWMyNDEzIiwibGluZSI6NjY1LCJhY3Rpb25hYmxlIjp0cnVlfV19 -->

Review

Findings

Medium

  • [logic-error] agent_eval/agent/fullsend.py:129 — _SECRET_ASSIGNMENT_RE does not handle escaped quotes or an unterminated quoted value. With no closing quote the \S+ fallback redacts only the first fragment, so PASSWORD="first second... is forwarded as PASSWORD=*** second.... fullsend already cuts displayed commands at 120 runes before this filter sees them, so a missing closing quote can occur in practice.
    Remediation: Handle escaped quotes. When a quoted value has no closing quote, redact through the end of the line. Add regression tests for both cases.

  • [secret-exposure] agent_eval/agent/fullsend.py:689 — The same redaction gap, seen from the security side. _present applies the regex, and the forwarded lines go to the harness stdout, which can be a CI job log. Credential fragments from truncated or escaped quoted assignments are therefore printed live.
    Remediation: Treat an unterminated quoted credential value as sensitive through the end of the displayed line, and handle escaped delimiters. Test commands truncated inside a quoted credential.

Low

  • [injection-vuln] agent_eval/agent/fullsend.py:665 — _ANSI_RE strips only CSI sequences. OSC/DCS sequences, other ESC sequences and non-printing control characters in forwarded 💬/⚙/Error lines reach the harness terminal unchanged. This matches the existing claude-code and codex runners, which also print agent-derived progress text unsanitized, so severity is low.
    Remediation: Strip terminal control sequences (OSC/DCS, remaining ESC sequences, non-printing control characters) from forwarded text before printing. Add tests on both pipes.

Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

fullsend-ai-review[bot]

This comment was marked as outdated.

…cut quoted credential whole, strip terminal control

Review findings on the live fullsend console (#286).

A pipe a descendant outside fullsend's process group still held after the
bounded drain kept its reader forwarding under the finished case's prefix:
the descendant's later lines landed after the case's FAIL line and among
the next cases' progress. _ConsoleCapture.detach() now ends the forwarding
and the keeping of lines once the run is over; it is taken under the print
lock the readers print under and the caller prints its status line under,
so a forwarded line in flight has landed before detach() returns and none
follows. The reader goes on emptying the pipe (a read blocked on it cannot
be interrupted) and closes it at EOF, keeping nothing. The timeout path
detaches after its drain; the interrupt path, which re-raised without
touching the readers, now gets the same bounded drain and detach before
the interrupt propagates.

_SECRET_ASSIGNMENT_RE redacted an unterminated quoted value only up to its
first blank, so a command fullsend cut at 120 runes inside the value was
forwarded as PASSWORD=*** second third. A quoted value now runs over
escaped quotes (\", \') to its closing quote and, with none on the line,
to the end of the line; a bare value is unchanged, as is a non-credential
KEY=value.

_ANSI_RE stripped only CSI sequences from a forwarded line. _CONTROL_RE
drops CSI, OSC (BEL or ST terminated), DCS/SOS/PM/APC strings, the
single-character and nF escapes, their 8-bit C1 forms, and the other C0/C1
control characters except the tab; a string with no terminator runs to the
end of the line. The capture, stderr.log and the provenance parse keep the
console as it was written.

Tests: unterminated double and single quotes, escaped quotes in a quoted
value, a command cut mid-value with blanks after the cut, non-secret
assignments untouched, control sequences in the filter and on both pipes
of a run, a descendant outside the group writing after the bounded drain
of a timeout and of an interrupt (nothing forwarded or kept, the pipe
closed at its EOF). Live progress in runner.md states the redaction rule,
the control stripping and what the timeout note now means.

Signed-off-by: Antonin Stefanutti <antonin@stefanutti.fr>
…detach the console readers on every exit

Follow-up to the #286 review of the live fullsend console.

A displayed command cut right after a backslash inside a quoted credential
(or a shell line continuation) left the quoted alternatives of
_SECRET_ASSIGNMENT_RE unable to reach the end of the line: a lone trailing
backslash matched neither branch, so the bare-value fallback redacted only
to the first blank and PASSWORD="abc def\ was forwarded as PASSWORD=*** def\.
The escape in both quoted branches is now optional, so the value runs
through the end of the line as the rule says; every other input of the
redaction tests, escaped backslashes included, renders as before. The
comment also says what the quoted rule does not cover: a quote further in
or around the whole assignment is a bare value, redacted to the first blank.

A Ctrl-C landing inside the timeout handler (the term-grace wait, the pipe
drain) propagated past the sibling interrupt clause and skipped the
readers' detach, so a reader could still print under the finished case's
prefix while the interrupt unwound. One outer try/finally now detaches on
every exit; the inner try/finally on the interrupt path goes.

The holder tests give the leader three seconds (not one) to start Python,
spawn its holder and print the pid, and _holder_pid fails with a message
instead of an IndexError when it did not get to.

Signed-off-by: Antonin Stefanutti <antonin@stefanutti.fr>
@fullsend-ai-review

fullsend-ai-review Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 5:30 PM UTC · Completed 5:44 PM UTC

Commit: 9e99925 · View workflow run →

Runtime: pi · Model: sonnet → claude-sonnet-5-5 · Effort: high · Cost: $2.61

fullsend-ai-review[bot]

This comment was marked as outdated.

astefanutti added a commit that referenced this pull request Oct 30, 2026
… after the clip, strip bidi controls

Second review round of the live fullsend console (#286).

A ⚙ line forwarded its raw argument, so a command or a prompt fullsend
displayed — "⚙ Bash: curl --password swordfish …" — reached the CI log
with its credential, and the assignment rule covered one shape only:
TOKEN=ab"c d", TOKEN=abc\ def and "api_key=ab cd" leaked the part after
the first blank; --password x, Authorization: Bearer x, "token": "x", URL
userinfo and AWS_ACCESS_KEY_ID= were not redacted at all. _present now
renders a ⚙ line without its raw argument: a file tool (Read, Write, Edit,
MultiEdit, Glob, Grep, NotebookEdit, LS) keeps its path, Bash shows its
program name alone (the first shell word after any leading NAME=value
assignments, when it is a plain program name) and every other tool —
Agent, Task, Skill, WebFetch, WebSearch, an MCP tool, an unknown name — its
name alone. Every forwarded line then has the values of the
credential-named variables of fullsend's environment masked (os.environ
with execution.env, runner.env and the hook env over it; a name matching
_SECRET_KEY_RE, a value of eight characters or more, longest first; read
once per run in _run, kept on the console filter, never printed) and is
redacted by pattern: KEY=value, --flag value and key: value / "key": value
whose key names a credential (a credential word of its own, a name ending
in key, token, secret or password, or containing credential — fullsend's
Tokens: line is none of these), the value being one whole shell word
(quoted parts, escaped blanks and concatenations included, an unterminated
quote to the end of the line, an assignment opened inside a quoted word
through that word), Authorization: Bearer|Basic|token <x> and a bare
Bearer <x>, and URL userinfo (user:***@, ***@ for a bare token).

_SECRET_ASSIGNMENT_RE backtracked quadratically on one long word repeating
a credential name with no "=", and ran before the clip (1.3 s at 50 KB,
5.4 s at 100 KB, holding the GIL). The line is now clipped first, and each
keyed form is found with one forward scan — \b(\w+)=, a flag or a field key
anchored at a word start — whose capture is tested in a callback, the value
consumed from there; the URL pattern is anchored the same way. Clipping
first is safe: a bare value cut short is the rest of its word, a quoted one
runs to the end of the line, a userinfo cut before its @ is taken for one
(the cut form of the pattern), and a line the redaction lengthened is cut
again. 100 KB of any shape clears select() in milliseconds.

_CONTROL_RE left the Unicode bidi controls (U+061C, U+200E/F, U+202A–E,
U+2066–9), the line and paragraph separators (U+2028/9) and the zero-width
characters (U+200B–D, U+2060–4, U+FEFF) in a forwarded line, which can
reorder or hide text in a log viewer; they are stripped now, ordinary
non-ASCII text and the markers' U+FE0F kept.

The capture (RunResult.stderr, stderr.log, the provenance parse) is still
byte for byte what communicate() would have returned; timeout, interrupt
and detach are untouched; nothing streams without a log prefix; no new
dependency (shlex for the program name).

Tests: the sample console gains Write/Agent/Skill calls and a long 💬 line
(the ⚙ Bash expectations become "⚙ Bash: python3"); tool-call rendering of
every kind; shell-concatenated values; flags, headers, fields and URLs;
environment values masked, as a unit and on both pipes of a run;
clip-then-redact leaving no fragment of a cut credential; a 100 KB
pathological line of ten shapes under a second, clipped and unclipped;
bidi and zero-width characters in the filter and on both pipes. The
existing redaction tests move from ⚙ Bash lines to the 💬/Error lines
that keep their text. Live progress in runner.md states what a ⚙ line
shows, the redaction rules, the environment masking and the
clip-then-redact order; the module docstring follows.

Signed-off-by: Antonin Stefanutti <antonin@stefanutti.fr>
@fullsend-ai-review

fullsend-ai-review Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 11:41 AM UTC · Completed 11:52 AM UTC

Commit: 7336d04 · View workflow run →

Runtime: pi · Model: sonnet → claude-sonnet-5-5 · Effort: high · Cost: $2.71

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot added the kind/enhancement New feature or request label Oct 30, 2026
… after the clip, strip bidi controls

Second review round of the live fullsend console (#286).

A ⚙ line forwarded its raw argument, so a command or a prompt fullsend
displayed — "⚙ Bash: curl --password <pw> …" — reached the CI log
with its credential, and the assignment rule covered one shape only:
TOKEN=ab"c d", TOKEN=abc\ def and "api_key=ab cd" leaked the part after
the first blank; --password x, Authorization: Bearer x, "token": "x", URL
userinfo and AWS_ACCESS_KEY_ID= were not redacted at all. _present now
renders a ⚙ line without its raw argument: a file tool (Read, Write, Edit,
MultiEdit, Glob, Grep, NotebookEdit, LS) keeps its path, Bash shows its
program name alone (the first shell word after any leading NAME=value
assignments, when it is a plain program name) and every other tool —
Agent, Task, Skill, WebFetch, WebSearch, an MCP tool, an unknown name — its
name alone. Every forwarded line then has the values of the
credential-named variables of fullsend's environment masked (os.environ
with execution.env, runner.env and the hook env over it; a name matching
_SECRET_KEY_RE, a value of eight characters or more, longest first; read
once per run in _run, kept on the console filter, never printed) and is
redacted by pattern: KEY=value, --flag value and key: value / "key": value
whose key names a credential (a credential word of its own, a name ending
in key, token, secret or password, or containing credential — fullsend's
Tokens: line is none of these), the value being one whole shell word
(quoted parts, escaped blanks and concatenations included, an unterminated
quote to the end of the line, an assignment opened inside a quoted word
through that word), Authorization: Bearer|Basic|token <x> and a bare
Bearer <x>, and URL userinfo (user:***@, ***@ for a bare token).

_SECRET_ASSIGNMENT_RE backtracked quadratically on one long word repeating
a credential name with no "=", and ran before the clip (1.3 s at 50 KB,
5.4 s at 100 KB, holding the GIL). The line is now clipped first, and each
keyed form is found with one forward scan — \b(\w+)=, a flag or a field key
anchored at a word start — whose capture is tested in a callback, the value
consumed from there; the URL pattern is anchored the same way. Clipping
first is safe: a bare value cut short is the rest of its word, a quoted one
runs to the end of the line, a userinfo cut before its @ is taken for one
(the cut form of the pattern), and a line the redaction lengthened is cut
again. 100 KB of any shape clears select() in milliseconds.

_CONTROL_RE left the Unicode bidi controls (U+061C, U+200E/F, U+202A–E,
U+2066–9), the line and paragraph separators (U+2028/9) and the zero-width
characters (U+200B–D, U+2060–4, U+FEFF) in a forwarded line, which can
reorder or hide text in a log viewer; they are stripped now, ordinary
non-ASCII text and the markers' U+FE0F kept.

The capture (RunResult.stderr, stderr.log, the provenance parse) is still
byte for byte what communicate() would have returned; timeout, interrupt
and detach are untouched; nothing streams without a log prefix; no new
dependency (shlex for the program name).

Tests: the sample console gains Write/Agent/Skill calls and a long 💬 line
(the ⚙ Bash expectations become "⚙ Bash: python3"); tool-call rendering of
every kind; shell-concatenated values; flags, headers, fields and URLs;
environment values masked, as a unit and on both pipes of a run;
clip-then-redact leaving no fragment of a cut credential; a 100 KB
pathological line of ten shapes under a second, clipped and unclipped;
bidi and zero-width characters in the filter and on both pipes. The
existing redaction tests move from ⚙ Bash lines to the 💬/Error lines
that keep their text. Live progress in runner.md states what a ⚙ line
shows, the redaction rules, the environment masking and the
clip-then-redact order; the module docstring follows.

The credentials in the test fixtures are placeholders (<pw>, <jwt>, <b64>, <pat>):
a value a secret scanner reads as a credential, a base64 user:pass pair or a
provider's key prefix, is an alert even in a test.

Signed-off-by: Antonin Stefanutti <antonin@stefanutti.fr>
…from the cut command

Second-round review of the console redaction.

A bare Bearer value ran to the next blank regardless of quotes, so in
compact JSON it swallowed the following field's key and left that field's
value past its first blank outside any key context:
{"Authorization":"Bearer <t>","password":"p w"} forwarded "w". The
bare alternative now ends at a quote as well as a blank; a quoted
Authorization header keeps its closing quote (the one previous expectation
that changes).

A ⚙ Bash line's program name was read by shlex, a per-character lexer, from
the whole command before the clip (6 s at 1 MB in the reader thread), which
contradicted the "cost bounded by the cut" documentation. It is read from
the command's first 200 characters: the first word of a command displayed
whole is the same, a split word is cut again, a split quote is no program.

Smaller review notes taken: passphrase and signature are credential words
(--passphrase x, PASSPHRASE=x, X-Amz-Signature=…); an environment variable
named by auth/oauth is masked too (DOCKER_AUTH_CONFIG's auth field is none
of the key rule's words — masking only ever hides); a credential field's
bare value runs to the next , ; } ] or the end of the line, so a multi-word
password in prose is hidden whole; the linear-time test measures CPU time
and feeds the ⚙ Bash shapes. The joiner strip (U+200C/D) stays: it runs
before the redaction and defeats a credential name split by a joiner,
which the tests and runner.md now say.

The captured console is untouched; the forwarded copy only.

Signed-off-by: Antonin Stefanutti <antonin@stefanutti.fr>
@astefanutti

Copy link
Copy Markdown
Collaborator Author

Force-pushed the two round-2 commits (4f6622c → ff49fa2, 7336d04 → fa500c0). GitHub secret scanning raised an alert on the fixture of test_credential_flags_headers_fields_and_urls_redacted: its Authorization: Basic dXNlcjpwYXNz is the base64 of user:pass, which the HTTP Basic Authentication Header pattern reads as a live credential, and the same fixture carried a URL password, a ghp_ userinfo and an AKIA key id.

Only tests/test_fullsend_runner.py differs from the previous tip: the credential-shaped values are now placeholders (<pw>, <jwt>, <b64>, <pat>, <t>, <id>, <aws-key-id>, <db-passwd>, <api-key>, <oauth-value>, <longpassword>, "p w"), with a class comment on TestConsoleFilter saying why. Each placeholder is removed by the same rule as the value it replaces (the rules go by the key, the flag or the header, never by the value's shape; the clip case keeps a 14-character password so the cut still lands inside it). agent_eval/agent/fullsend.py and the docs are byte-identical to the previous tip. Full suite: 2806 passed. The literals are gone from the rewritten history; the two commit messages use the same placeholders.

@astefanutti
astefanutti force-pushed the feat/fullsend-runner-stream-progress branch from 7336d04 to fa500c0 Compare October 30, 2026 12:15
@fullsend-ai-review

fullsend-ai-review Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 12:18 PM UTC · Completed 12:28 PM UTC

Commit: fa500c0 · View workflow run →

Runtime: pi · Model: sonnet → claude-sonnet-5-5 · Effort: high · Cost: $2.83

@fullsend-ai-review fullsend-ai-review 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.

See the review comment for full details.

Comment thread agent_eval/agent/fullsend.py Resolved
Comment thread agent_eval/agent/fullsend.py Resolved
Comment thread agent_eval/agent/fullsend.py Resolved
Comment thread agent_eval/agent/fullsend.py Resolved
The review's secret-exposure finding: a quoted header name was not
redacted ({"Authorization":"Basic x"} forwarded x) and a URL's username
was kept, so https://<token>:x-oauth-basic@github.com forwarded the
token.

_redact_authorization replaces the Authorization regex with a small
parser. The name is found bare (after a dot, a dash, a Markdown mark or
an escaped line break; after a "/" it is a path), quoted as a JSON or
dict key (a bytes key too) or opening a quoted word, followed by ":",
"=" or "=>". A quoted value, or the value of a header that opened a
quoted word, is hidden through the closing quote, the scheme kept when
known; a string holding the scheme alone hides the operand concatenated
after it (a word, a quoted string, a $(...) substitution, after a "+"
too, a quoted scheme alone chaining once more). A bare value is hidden
as one word after a token68 scheme (or its name=value list, Rails'
Token token="..."), as its name=value list after a parameter scheme
(Digest, OAuth, AWS4-HMAC-SHA256, Hawk, SCRAM; a list that does not
parse whole), inside its brackets when it opens some (a Go header dump,
a JSON list), and whole, through the next delimiter or a quote glued to
a word, otherwise: a scheme the rule does not know may carry a
multi-word credential. The bare Bearer rule leaves a *** already there
with its delimiter, and a quote a blank follows opens no credential.
The URL userinfo is masked whole. The field rule reads a bytes key.

Five adversarial rounds drove the shapes the test pins: a "="-padded
token in a quoted header taken for a name="value" parameter that
swallowed the next field's quote, map[Authorization:[Bearer x]],
unknown schemes, f-strings, a Digest list inside a quoted shell word, a
shell-concatenated credential, {"Authorization":["Basic x"]}, blanks
around "=", a partly env-masked token, http.client's
send: b'...\r\nAuthorization: Basic x\r\n' dump. Every pattern stays
linear (the separator regex that went quadratic on a blank run is
unambiguous now): 100 KB shapes redact in under 40 ms.

Known residue, outside the forms the rule claims: a credential-named
key after "=>" or in Markdown bold is not a key for the keyed rules;
dotted or call forms (headers.set('Authorization', 'Basic x')), other
languages' concatenation operators and double-encoded JSON are not
read; a prose "authorization:" loses the rest of its clause, the safe
side.

Signed-off-by: Antonin Stefanutti <antonin@stefanutti.fr>
@fullsend-ai-review

fullsend-ai-review Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

🤖 Finished Review · ❌ Failure (provider "openai": no OpenAI credential: set FULLSEND_OPENAI_AUDIENCE, FULLSEND_OPENAI_IDENTITY_PROVIDER_ID and FULLSEND_OPENAI_SERVICE_ACCOUNT_ID (or inference.openai in config.yaml) for Workload Ide…) · Started 4:34 PM UTC · Completed 4:34 PM UTC

Commit: 4bc64c8 · View workflow run →

Effort: high

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/enhancement New feature or request risk/moderate PR risk: moderate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant