Skip to content

Phase 37: forgelm approvals listing subcommand - #25

Closed
cemililik wants to merge 2 commits into
developmentfrom
closure/faz37-approvals-listing
Closed

cemililik wants to merge 2 commits into
developmentfrom
closure/faz37-approvals-listing

Conversation

@cemililik

@cemililik cemililik commented May 2, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Closes the Phase 9 discovery gap (GH-7 from `ghost-features-analysis-20260502`). `forgelm approve` / `reject` shipped in Wave 1 but an operator coming to the workstation cold had no way to ask "which runs are awaiting my review?".

`forgelm approvals` fills the gap with two query modes:

```shell
$ forgelm approvals --pending --output-dir checkpoints/
Pending approvals (2):

RUN_ID AGE REQUESTED_AT STAGING


fg-abc123def456 3h 2026-04-30T11:33:10+00:00 present
fg-def456abc789 1d 2026-04-29T14:12:55+00:00 present

$ forgelm approvals --show fg-abc123def456 --output-dir checkpoints/
Run: fg-abc123def456
Status: pending

Audit chain (oldest first):
[2026-04-30T11:33:10+00:00] human_approval.required — require_human_approval=true

Staging contents (4 entries):

  • adapter_config.json
  • adapter_model.safetensors
  • tokenizer.json
  • tokenizer_config.json
    ```

`--output-format json` returns a structured envelope so CI can filter the queue programmatically.

Files

File Change
`forgelm/cli/subcommands/_approvals.py` (new, 350 lines) Dispatcher + helpers; reuses the audit-log parsing pattern from `_approve.py`
`forgelm/cli/_parser.py` `_add_approvals_subcommand` registrar with mutually-exclusive `--pending` / `--show RUN_ID` group
`forgelm/cli/_dispatch.py` `approvals` branch
`forgelm/cli/init.py` Facade re-exports for tests / monkeypatch
`tests/test_approvals_listing.py` (new, 320 lines) 15 tests, torch-free
`docs/usermanuals/{en,tr}/compliance/human-oversight.md` "Inspecting pending runs" section rewritten to match shipped output (table format + `--output-dir` flag)
`CHANGELOG.md` `[Unreleased]` Phase 37 entry

Test plan

  • `ruff format . && ruff check .` clean
  • `pytest tests/ -q` → 1028 passed, 13 skipped (1013 → 1028, +15 new)
  • Coverage 68.42% (≥40% threshold; +0.47 from this PR)
  • `forgelm approvals --help` succeeds (subcommand registration smoke)
  • `forgelm.cli._run_approvals_cmd` reachable via the facade for monkeypatch
  • Malformed audit lines do not hide legitimate pending runs (regression test)

Risk

Low. Pure additive feature: no changes to `approve` / `reject` behaviour, no schema migrations, no new heavy dependencies. The audit-log read path is shared with the existing `_approve` module so any regression there would surface in both subcommands at once.

Closes

🤖 Generated with Claude Code

Summary by Sourcery

Add a new forgelm approvals CLI subcommand to list and inspect pending human approval runs, wire it into the CLI dispatcher, and document the feature and its behavior.

New Features:

  • Introduce forgelm approvals --pending --output-dir DIR to list runs awaiting human approval, with tabular text or JSON envelope output for CI consumption.
  • Introduce forgelm approvals --show RUN_ID --output-dir DIR to display the full approval audit chain and staging directory contents for a single run.

Enhancements:

  • Expose approvals-related helpers via the forgelm.cli facade for easier monkeypatching and testing.
  • Refine human-oversight documentation in English and Turkish to describe the approvals discovery workflow with current CLI output examples.
  • Add a dedicated approvals dispatcher and audit-log parsing helpers that reuse the existing approval gate conventions.

Documentation:

  • Update human oversight user manuals (EN/TR) to document the forgelm approvals subcommand, its modes, JSON output, and error behavior.
  • Add CHANGELOG entry for Phase 37 describing the new approvals listing subcommand and its capabilities.

Tests:

  • Add a new approvals listing test suite covering pending and show modes, malformed audit logs, dispatcher edge cases, CLI registration, and facade reachability.

Closes the Phase 9 discovery gap. forgelm approve / reject ship with
Wave 1 but an operator who walks up to a workstation cold has no way
to ask "which runs are awaiting review?". forgelm approvals fills
the gap.

Two query modes (mutually exclusive, argparse-enforced):

  forgelm approvals --pending --output-dir DIR
    Lists every run whose audit log carries a
    human_approval.required event without a matching terminal
    decision. Tabular text or {"success": true, "pending": [...],
    "count": N} JSON envelope.

  forgelm approvals --show RUN_ID --output-dir DIR
    Prints the full approval-gate audit chain (request -> terminal
    decision) plus the on-disk staging directory listing. Status
    classifies as pending / granted / rejected / unknown.

Implementation:

- forgelm/cli/subcommands/_approvals.py (new) — dispatcher + helpers.
  Reuses the audit-log parsing pattern from _approve.py (iter events,
  skip malformed, emit one summary warning at end).
- forgelm/cli/_parser.py — _add_approvals_subcommand registrar with
  a mutually-exclusive --pending / --show RUN_ID group; --output-dir
  required in both modes (no useful default).
- forgelm/cli/_dispatch.py — approvals branch.
- forgelm/cli/__init__.py — facade re-exports for tests / monkeypatch.

Operator docs:

- docs/usermanuals/en/compliance/human-oversight.md "Inspecting
  pending runs" section rewritten to match the shipped output (table
  format with RUN_ID / AGE / REQUESTED_AT / STAGING columns, the
  --output-dir flag the dispatcher actually requires).
- docs/usermanuals/tr/compliance/human-oversight.md mirror updated
  in the same shape.

Tests (tests/test_approvals_listing.py — 15 cases, all torch-free):

- empty audit log -> empty list, friendly notice
- three runs, one granted -> two pending in JSON envelope
- rejected run also excluded from pending
- pending summary carries metrics + staging_path + reason
- text output renders the table headers + run id + staging column
- --show pending run emits the chain + staging_contents
- --show granted run status == "granted", chain length == 2
- --show unknown run_id exits 1 with actionable error
- --show with no audit log exits 1
- text show renders the timeline + approver
- subcommand registered (forgelm approvals --help)
- malformed audit lines (non-json + non-dict-root) do not hide a
  legitimate pending run
- dispatcher defensive: neither --pending nor --show -> exit 1
- monkeypatch facade resolution: forgelm.cli._collect_pending_runs
  is callable + reachable
- caplog assertion: unknown run_id error names the missing id

Coverage 67.95% -> 68.42% (+0.47 from the new module + tests).
Total tests: 1013 -> 1028.

Closes: closure-plan-202604300906.md §9.5 Phase 37 (GH-7)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented May 2, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Implements a new forgelm approvals CLI subcommand that lists or inspects human-approval-gated runs by scanning the JSONL audit log, integrates it into the existing CLI parser/dispatcher/facade, adds tests around audit parsing and error handling, and updates documentation and changelog accordingly.

Sequence diagram for the forgelm approvals --pending flow

sequenceDiagram
  actor Operator
  participant CLI as forgelm_cli_entrypoint
  participant Parser as parse_args
  participant Dispatcher as _dispatch_subcommand
  participant Facade as _cli_facade
  participant Approvals as _run_approvals_cmd
  participant ListPending as _run_approvals_list_pending
  participant Collector as _collect_pending_runs
  participant Iter as _iter_audit_events
  participant FS as filesystem

  Operator->>CLI: run forgelm approvals --pending --output-dir DIR
  CLI->>Parser: parse_args()
  Parser-->>CLI: args(command=approvals, pending=True)
  CLI->>Dispatcher: _dispatch_subcommand("approvals", args)
  Dispatcher->>Facade: _run_approvals_cmd(args, output_format)
  Facade->>Approvals: _run_approvals_cmd(args, output_format)
  Approvals->>ListPending: _run_approvals_list_pending(args, output_format)
  ListPending->>FS: check DIR/audit_log.jsonl exists
  alt audit_log.jsonl missing
    ListPending-->>Operator: friendly empty summary (text or JSON)
  else audit_log.jsonl present
    ListPending->>Collector: _collect_pending_runs(audit_log_path)
    Collector->>Iter: _iter_audit_events(audit_log_path)
    Iter->>FS: read JSONL lines
    FS-->>Iter: line contents
    Iter-->>Collector: (line_number, event_dict)
    Collector-->>ListPending: pending_events
    ListPending-->>Operator: pending approvals table or JSON envelope
  end
Loading

Sequence diagram for the forgelm approvals --show RUN_ID flow

sequenceDiagram
  actor Operator
  participant CLI as forgelm_cli_entrypoint
  participant Parser as parse_args
  participant Dispatcher as _dispatch_subcommand
  participant Facade as _cli_facade
  participant Approvals as _run_approvals_cmd
  participant ShowRun as _run_approvals_show
  participant ChainCollector as _collect_run_audit_chain
  participant Iter as _iter_audit_events
  participant Classifier as _classify_chain
  participant StagingDir as _staging_dir_for_event
  participant StagingList as _staging_contents
  participant FS as filesystem

  Operator->>CLI: run forgelm approvals --show RUN_ID --output-dir DIR
  CLI->>Parser: parse_args()
  Parser-->>CLI: args(command=approvals, show=RUN_ID)
  CLI->>Dispatcher: _dispatch_subcommand("approvals", args)
  Dispatcher->>Facade: _run_approvals_cmd(args, output_format)
  Facade->>Approvals: _run_approvals_cmd(args, output_format)
  Approvals->>ShowRun: _run_approvals_show(args, output_format)
  ShowRun->>FS: check DIR/audit_log.jsonl exists
  alt audit_log.jsonl missing
    ShowRun-->>Operator: structured error (text or JSON), exit EXIT_CONFIG_ERROR
  else audit_log.jsonl present
    ShowRun->>ChainCollector: _collect_run_audit_chain(audit_log_path, RUN_ID)
    ChainCollector->>Iter: _iter_audit_events(audit_log_path)
    Iter->>FS: read JSONL lines
    FS-->>Iter: line contents
    Iter-->>ChainCollector: (line_number, event_dict)
    ChainCollector-->>ShowRun: chain
    alt chain empty
      ShowRun-->>Operator: structured error for unknown run_id, exit EXIT_CONFIG_ERROR
    else chain nonempty
      ShowRun->>Classifier: _classify_chain(chain)
      Classifier-->>ShowRun: status (pending/granted/rejected)
      ShowRun->>StagingDir: _staging_dir_for_event(DIR, first_required_event)
      StagingDir->>FS: resolve staging path
      FS-->>StagingDir: path or None
      StagingDir-->>ShowRun: staging_path
      ShowRun->>StagingList: _staging_contents(staging_path)
      StagingList->>FS: list directory entries
      FS-->>StagingList: filenames
      StagingList-->>ShowRun: staging_listing
      ShowRun-->>Operator: human-readable timeline + staging contents or JSON object
    end
  end
Loading

Flow diagram for pending approvals audit_log.jsonl processing

flowchart TD
  start([Start _collect_pending_runs])
  read_events[_iter_audit_events reads audit_log.jsonl]
  classify{event.event}
  required[Store first human_approval.required per run_id in required_by_run]
  decision[Add run_id to decided_run_ids]
  other[Ignore non approval-gate events]
  pending_build[Build pending list where run_id not in decided_run_ids]
  sort[Sort pending by timestamp descending]
  return_pending([Return pending events])

  start --> read_events
  read_events --> classify
  classify -->|human_approval.required| required
  classify -->|human_approval.granted or human_approval.rejected| decision
  classify -->|other| other
  required --> read_events
  decision --> read_events
  other --> read_events
  read_events --> pending_build
  pending_build --> sort
  sort --> return_pending
Loading

File-Level Changes

Change Details Files
Introduce forgelm approvals subcommand with --pending and --show RUN_ID modes, wired into the CLI parser, dispatcher, and facade.
  • Add _add_approvals_subcommand registrar that defines mutually-exclusive --pending / --show RUN_ID flags and a required --output-dir plus common flags including --output-format
  • Register the new approvals subparser in parse_args() so forgelm approvals --help and subcommand parsing work
  • Extend _dispatch_subcommand to call _cli_facade._run_approvals_cmd for the approvals command and exit with success
forgelm/cli/_parser.py
forgelm/cli/_dispatch.py
forgelm/cli/__init__.py
Implement approvals listing/show logic backed by the audit log, with text/JSON output and robust handling of malformed lines.
  • Add _output_error_and_exit helper to emit structured JSON or logged errors with appropriate exit codes, mirroring _approve behavior
  • Implement _iter_audit_events to stream audit_log.jsonl, skipping malformed/non-dict lines while logging a single warning summary
  • Implement _collect_pending_runs and _summarise_pending to compute pending approvals from human_approval.required minus terminal decision events and prepare summaries including staging, metrics, and age
  • Implement _collect_run_audit_chain, _classify_chain, and staging helpers to build a per-run approval timeline and staging directory listing for --show
  • Implement _run_approvals_list_pending, _run_approvals_show, and _run_approvals_cmd as the top-level command handlers, including JSON/text emitters and defensive argument validation
forgelm/cli/subcommands/_approvals.py
Add comprehensive tests for the approvals subcommand covering pending/show behavior, registration, malformed logs, and monkeypatch surface.
  • Create helpers to seed synthetic JSONL audit logs and staging directory layouts for runs in different decision states
  • Test --pending behavior for empty logs, mixed pending/decided/rejected runs, JSON envelope structure, and tabular text rendering
  • Test --show behavior for pending/granted runs, unknown run IDs, missing audit logs, and human-readable timeline output
  • Add tests for robustness against malformed audit lines ensuring they do not hide valid pending runs
  • Add tests that the dispatcher rejects invalid argument combinations and that key helpers are exposed via the forgelm.cli facade for monkeypatching, plus logging of actionable errors
tests/test_approvals_listing.py
Update compliance docs and changelog to describe the new approvals discovery flow and its outputs.
  • Rewrite the "Inspecting pending runs" section in the English and Turkish human-oversight manuals to show the new forgelm approvals --pending/--show --output-dir usage and sample text/JSON outputs
  • Document --output-format json envelope shape and behavior for CI integration in both docs
  • Add a Phase 37 changelog entry summarizing the new forgelm approvals subcommand, its modes, shared audit-log parsing, and new tests/docs
docs/usermanuals/en/compliance/human-oversight.md
docs/usermanuals/tr/compliance/human-oversight.md
CHANGELOG.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented May 2, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: afee0385-8f12-4d54-879b-58bd844f94c7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch closure/faz37-approvals-listing

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 and usage tips.

@qodo-code-review

Copy link
Copy Markdown

Review Summary by Qodo

Add forgelm approvals listing subcommand for Phase 37 discovery gap

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add forgelm approvals subcommand for discovering pending approval requests
  - --pending mode lists all runs awaiting human review with tabular/JSON output
  - --show RUN_ID mode displays full audit chain and staging directory contents
• Implement audit log parsing with malformed-line resilience (reuses _approve.py pattern)
• Support both text and JSON output formats for CI integration
• Add 15 comprehensive tests covering empty logs, multiple runs, rejected exclusion, and error cases
• Update operator documentation in English and Turkish with actual shipped output examples
Diagram
flowchart LR
  Parser["Parser registrar<br/>_add_approvals_subcommand"]
  Dispatch["Dispatcher<br/>_dispatch_subcommand"]
  Approvals["Approvals module<br/>_approvals.py"]
  AuditLog["Audit log<br/>audit_log.jsonl"]
  Output["Output<br/>text/JSON"]
  
  Parser -- "register --pending<br/>--show RUN_ID" --> Dispatch
  Dispatch -- "route approvals<br/>command" --> Approvals
  Approvals -- "parse events<br/>collect runs" --> AuditLog
  Approvals -- "emit pending list<br/>or show details" --> Output
Loading

Grey Divider

File Changes

1. forgelm/cli/subcommands/_approvals.py ✨ Enhancement +424/-0

New approvals listing subcommand implementation

• New 424-line module implementing the approvals listing dispatcher
• Two query modes: --pending lists all awaiting approval, --show RUN_ID displays full audit
 chain
• Reuses audit log parsing pattern from _approve.py with malformed-line skipping
• Helpers for staging directory inspection, age formatting, and status classification
• Text and JSON output formatters for both modes

forgelm/cli/subcommands/_approvals.py


2. forgelm/cli/_parser.py ⚙️ Configuration changes +44/-0

Register approvals subcommand with argument parser

• Add _add_approvals_subcommand registrar function with mutually-exclusive --pending / `--show
 RUN_ID` group
• Require --output-dir flag in both modes (no useful default)
• Support --output-format flag for JSON envelope output
• Register subcommand in parse_args() dispatcher

forgelm/cli/_parser.py


3. forgelm/cli/_dispatch.py ✨ Enhancement +3/-0

Add approvals command routing to dispatcher

• Add approvals branch to _dispatch_subcommand function
• Route to _run_approvals_cmd with output format parameter
• Exit with EXIT_SUCCESS after command completion

forgelm/cli/_dispatch.py


View more (5)
4. forgelm/cli/__init__.py Miscellaneous +8/-0

Export approvals module functions via facade

• Re-export _add_approvals_subcommand from _parser for test monkeypatch
• Re-export _collect_pending_runs, _collect_run_audit_chain, _run_approvals_cmd from
 subcommands._approvals for test access

forgelm/cli/init.py


5. tests/test_approvals_listing.py 🧪 Tests +372/-0

Add comprehensive test suite for approvals listing

• New 372-line test module with 15 torch-free test cases
• Test fixtures: _write_event, _seed_run, _build_args helpers for synthetic audit logs
• TestApprovalsPending class: empty log, three-runs-one-decided, rejected exclusion,
 metrics/staging in summary, table rendering
• TestApprovalsShow class: pending/granted/unknown run status, missing audit log, text timeline
 rendering
• TestApprovalsMalformedLines class: robustness against corrupt audit log entries
• TestApprovalsDispatcherDefensive and TestApprovalsMonkeyPatchSurface for defensive checks and
 facade resolution

tests/test_approvals_listing.py


6. docs/usermanuals/en/compliance/human-oversight.md 📝 Documentation +25/-6

Update English docs with shipped approvals output

• Rewrite "Inspecting pending runs" section with actual shipped output examples
• Show --pending mode with tabular output (RUN_ID, AGE, REQUESTED_AT, STAGING columns)
• Show --show RUN_ID mode with audit chain timeline and staging contents listing
• Document --output-format json for CI programmatic filtering
• Explain status classification for granted/rejected/unknown runs

docs/usermanuals/en/compliance/human-oversight.md


7. docs/usermanuals/tr/compliance/human-oversight.md 📝 Documentation +25/-6

Update Turkish docs with shipped approvals output

• Mirror English documentation updates in Turkish
• Translate "Inspecting pending runs" section with same output examples
• Maintain consistent structure and examples across language versions

docs/usermanuals/tr/compliance/human-oversight.md


8. CHANGELOG.md 📝 Documentation +29/-0

Document Phase 37 approvals listing feature in changelog

• Add Phase 37 entry under "Added — Wave 2a" section
• Document both --pending and --show modes with examples
• Note closure of GH-007 discovery gap from ghost-features-analysis
• List new module, 15 test cases, and documentation updates
• Highlight audit-log parsing pattern reuse and malformed-line handling

CHANGELOG.md


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented May 2, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Unbounded staging_path listing ☑ 🐞 Bug ⛨ Security
Description
forgelm approvals trusts staging_path from audit_log.jsonl and passes it to os.listdir,
allowing a tampered audit log to make the CLI enumerate directories outside --output-dir and leak
filenames in text/JSON output. This is inconsistent with approve/reject, which explicitly refuse
staging paths that resolve outside output_dir.
Code

forgelm/cli/subcommands/_approvals.py[R154-203]

+def _staging_dir_for_event(output_dir: str, event: Dict[str, Any]) -> Optional[str]:
+    """Best-effort: return the on-disk staging path the event refers to.
+
+    The trainer records ``staging_path`` on every ``human_approval.required``
+    event (Wave 1 round-2 audit-event vocabulary).  Older audit logs may
+    pre-date that addition; for those we synthesise the canonical path
+    ``<output_dir>/final_model.staging`` and let the caller decide whether
+    to fall back when ``os.path.isdir`` is False.
+    """
+    declared = event.get("staging_path")
+    if isinstance(declared, str) and declared:
+        return declared
+    fallback = os.path.join(output_dir, "final_model.staging")
+    return fallback if os.path.isdir(fallback) else None
+
+
+def _age_seconds(event_timestamp: Optional[str]) -> Optional[float]:
+    """Compute ``now - timestamp`` in seconds; ``None`` when unparseable.
+
+    The audit log emits ISO-8601 with a UTC suffix (``+00:00``).  We accept
+    a missing or malformed timestamp gracefully because hand-edited test
+    fixtures sometimes drop it, and a missing age is far less actionable
+    than a stack trace.
+    """
+    if not isinstance(event_timestamp, str) or not event_timestamp:
+        return None
+    try:
+        # Python 3.10 ``datetime.fromisoformat`` does not accept the ``Z``
+        # suffix.  Normalise to ``+00:00`` for older interpreters.
+        normalised = event_timestamp.replace("Z", "+00:00")
+        when = datetime.fromisoformat(normalised)
+    except ValueError:
+        return None
+    if when.tzinfo is None:
+        # Treat naive timestamps as UTC; mirrors how AuditLogger emits them
+        # but is defensive against external producers.
+        when = when.replace(tzinfo=timezone.utc)
+    delta = datetime.now(timezone.utc) - when
+    return delta.total_seconds()
+
+
+def _staging_contents(staging_path: Optional[str]) -> List[str]:
+    """List files in ``staging_path`` (sorted) or ``[]`` when missing/unreadable."""
+    if not staging_path or not os.path.isdir(staging_path):
+        return []
+    try:
+        return sorted(os.listdir(staging_path))
+    except OSError as exc:
+        logger.warning("Cannot list staging directory %s: %s", staging_path, exc)
+        return []
Evidence
_staging_dir_for_event returns the audit-log-provided staging_path verbatim, and
_staging_contents lists it with os.listdir if it exists. The existing approve/reject code treats
audit-log staging_path as potentially tampered and blocks any path whose realpath escapes
output_dir, demonstrating this is an established security boundary in the codebase that the new
subcommand should follow too.

forgelm/cli/subcommands/_approvals.py[154-203]
forgelm/cli/subcommands/_approve.py[32-52]
forgelm/cli/subcommands/_approve.py[297-306]

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

## Issue description
`forgelm approvals` reads `staging_path` from `audit_log.jsonl` and lists that directory without verifying it stays inside the operator-supplied `--output-dir`. A tampered/untrusted audit log can therefore cause arbitrary directory enumeration.
## Issue Context
`forgelm approve` / `forgelm reject` already implement a defense-in-depth boundary check (`_staging_path_inside_output_dir`) and refuse out-of-bounds paths.
## Fix Focus Areas
- forgelm/cli/subcommands/_approvals.py[154-203]
- forgelm/cli/subcommands/_approve.py[32-52]
- forgelm/cli/subcommands/_approve.py[297-306]
## Suggested fix
- Reuse (import) or duplicate `_staging_path_inside_output_dir` logic in `_approvals.py`.
- In `_staging_dir_for_event`, if `declared` exists but fails the inside-`output_dir` check, treat it as invalid (return `None` and optionally log a warning).
- Ensure both `--pending` and `--show` never call `os.listdir` on out-of-bounds paths; for `--show`, consider failing with `EXIT_CONFIG_ERROR` because the run’s staging path is untrustworthy.

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



Remediation recommended

2. --output-dir marked optional 📘 Rule violation ⚙ Maintainability
Description
The CHANGELOG documents forgelm approvals --pending [--output-dir DIR] as if --output-dir were
optional, but the CLI parser makes --output-dir required. This documentation drift can mislead
operators/CI users and violates the requirement that documentation claims match real behavior.
Code

CHANGELOG.md[R18-22]

+- **`forgelm approvals --pending [--output-dir DIR]`** lists every run whose
+  audit log carries a `human_approval.required` event without a matching
+  terminal decision.  Tabular text output or a JSON envelope
+  (`{"success": true, "pending": [...], "count": N}`) when
+  `--output-format json` is set, so CI can filter the queue programmatically.
Evidence
The CHANGELOG explicitly shows --output-dir in brackets for the --pending usage, which
conventionally indicates an optional flag, while the actual argparse definition for the approvals
subcommand sets --output-dir to required=True for both modes.

CLAUDE.md
CHANGELOG.md[18-22]
forgelm/cli/_parser.py[535-542]

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

## Issue description
`CHANGELOG.md` documents `forgelm approvals --pending [--output-dir DIR]` as if `--output-dir` were optional, but the parser requires it (`required=True`). This is documentation drift.
## Issue Context
Per the subcommand registrar docstring/help text, `--output-dir` is intentionally required for both `--pending` and `--show` modes.
## Fix Focus Areas
- CHANGELOG.md[16-23]
- forgelm/cli/_parser.py[535-542]

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


3. Pending state miscomputed ☑ 🐞 Bug ≡ Correctness
Description
_collect_pending_runs treats a run as decided if *any* granted/rejected event exists for that
run_id, even if a later human_approval.required appears, which can hide a legitimate pending
request in reused/corrupted audit logs. It also retains the first required event (setdefault)
rather than the most recent request, so the pending summary can be stale when multiple required
events exist.
Code

forgelm/cli/subcommands/_approvals.py[R101-135]

+def _collect_pending_runs(audit_log_path: str) -> List[Dict[str, Any]]:
+    """Return the list of pending approval requests, newest-first.
+
+    A "pending" run is one whose audit log carries a
+    ``human_approval.required`` event whose ``run_id`` does **not** appear
+    in any later ``human_approval.granted`` / ``human_approval.rejected``
+    event.  The order is by the *first* ``human_approval.required`` event
+    timestamp (oldest first when sorted ascending; we reverse to surface
+    the most-recent request at the top of the operator's listing).
+    """
+    required_by_run: Dict[str, Dict[str, Any]] = {}
+    decided_run_ids: set[str] = set()
+
+    for _line_no, event in _iter_audit_events(audit_log_path):
+        run_id = event.get("run_id")
+        if not isinstance(run_id, str):
+            # Required field on every approval-gate event; if missing the
+            # event is malformed and we cannot reason about it.  Skip.
+            continue
+        event_name = event.get("event")
+        if event_name == _EVT_HUMAN_APPROVAL_REQUIRED:
+            # Keep the first (oldest) requirement event for the run.  A
+            # second `required` for the same run_id would be unusual but
+            # not malformed — we keep the first because that is the
+            # original request the operator is reviewing.
+            required_by_run.setdefault(run_id, event)
+        elif event_name in _TERMINAL_DECISION_EVENTS:
+            decided_run_ids.add(run_id)
+
+    pending = [event for run_id, event in required_by_run.items() if run_id not in decided_run_ids]
+    # Newest-pending first so an operator opening the list sees the most
+    # recent request at the top.  ``timestamp`` may be missing on
+    # synthetic / hand-edited entries — sort missing values to the bottom.
+    pending.sort(key=lambda e: e.get("timestamp") or "", reverse=True)
+    return pending
Evidence
The code records decided_run_ids for any terminal event and then filters out all required events
for those run_ids, without checking that the decision occurred after the relevant requirement. In
contrast, the approve/reject helpers explicitly operate on the *most recent*
human_approval.required and *most recent* terminal decision for a run_id, implying the run’s
current state should be determined by the latest relevant events rather than the existence of any
historical decision.

forgelm/cli/subcommands/_approvals.py[101-135]
forgelm/cli/subcommands/_approve.py[81-117]
forgelm/cli/subcommands/_approve.py[123-161]

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

## Issue description
`_collect_pending_runs` can incorrectly hide pending approvals when a `run_id` has multiple `required`/decision cycles because it uses a global `decided_run_ids` set (any decision ever) and keeps the first `required` event.
## Issue Context
Even if `run_id` reuse is rare, the audit log can be reused across runs (operator reruns into same output dir) or manually edited/corrupted. The pending list should reflect the *current* state per run_id.
## Fix Focus Areas
- forgelm/cli/subcommands/_approvals.py[101-135]
## Suggested fix
- Stream the audit log in append order and maintain per-run state:
- Keep `latest_required_event[run_id] = event` when you see `human_approval.required`.
- Keep `latest_terminal_event_name[run_id] = event['event']` (or the full event) when you see a terminal decision.
- Determine pending at end as: run_id is pending iff it has a `latest_required_event` and either has no terminal event *after* that required, or more simply: the last seen approval-related event for that run_id is `human_approval.required`.
- Use the selected (latest) required event for the summary so timestamps/metrics/staging_path match the pending request the operator is being asked to review.

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



Advisory comments

4. Help misses approvals 🐞 Bug ⚙ Maintainability
Description
The top-level forgelm --help epilog hardcodes a subcommand list that does not include the newly
registered approvals subcommand, reducing discoverability and making help output inconsistent. The
subcommand is registered, but the help banner doesn’t mention it.
Code

forgelm/cli/_parser.py[R606-610]

   _add_verify_audit_subcommand(subparsers)
   _add_approve_subcommand(subparsers)
   _add_reject_subcommand(subparsers)
+    _add_approvals_subcommand(subparsers)
Evidence
parse_args() constructs an epilog string listing subcommands, which currently includes
approve/reject but not approvals, while the PR adds _add_approvals_subcommand(subparsers)
registration. This means forgelm --help won’t advertise the new command even though it exists.

forgelm/cli/_parser.py[579-595]
forgelm/cli/_parser.py[606-610]

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

## Issue description
`forgelm --help` uses a hardcoded epilog list of subcommands that wasn’t updated when `approvals` was added.
## Issue Context
The PR registers the subcommand, so this is purely a discoverability/help consistency issue.
## Fix Focus Areas
- forgelm/cli/_parser.py[579-595]
## Suggested fix
- Update the epilog string to include an `approvals` line (e.g. `forgelm approvals --pending/--show`) near the approve/reject entries.

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


Grey Divider

Qodo Logo

@codacy-production

codacy-production Bot commented May 2, 2026 •

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 1 high

Alerts:
⚠ 1 issue (≤ 0 issues of at least minor severity)

Results:
1 new issue

Category Results
ErrorProne 1 high

View in Codacy

🟢 Metrics 111 complexity · 11 duplication

Metric Results
Complexity 111
Duplication 11

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces the forgelm approvals subcommand, which allows operators to discover pending approval requests and inspect specific runs using the --pending and --show flags. The implementation includes a new CLI module, updated documentation in English and Turkish, and a comprehensive test suite. Review feedback identifies a logic error in how pending runs are tracked if run IDs are reused, a potential path traversal vulnerability when processing staging paths from the audit log, and opportunities to reduce code duplication between the new module and the existing _approve.py logic.

Comment thread forgelm/cli/subcommands/_approvals.py Outdated
Comment on lines +111 to +130
required_by_run: Dict[str, Dict[str, Any]] = {}
decided_run_ids: set[str] = set()

for _line_no, event in _iter_audit_events(audit_log_path):
run_id = event.get("run_id")
if not isinstance(run_id, str):
# Required field on every approval-gate event; if missing the
# event is malformed and we cannot reason about it. Skip.
continue
event_name = event.get("event")
if event_name == _EVT_HUMAN_APPROVAL_REQUIRED:
# Keep the first (oldest) requirement event for the run. A
# second `required` for the same run_id would be unusual but
# not malformed — we keep the first because that is the
# original request the operator is reviewing.
required_by_run.setdefault(run_id, event)
elif event_name in _TERMINAL_DECISION_EVENTS:
decided_run_ids.add(run_id)

pending = [event for run_id, event in required_by_run.items() if run_id not in decided_run_ids]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The current logic for identifying pending runs is flawed if a run_id is reused after a terminal decision (e.g., if a training run is restarted with the same ID after a rejection). Because it uses a static set of decided_run_ids collected from the entire log, any previous decision for a run_id will permanently exclude it from the pending list, even if a new approval request has been issued later in the log.

A more robust approach is to use a state-machine pattern while iterating through the log: a human_approval.required event adds the run to the pending set, and a terminal decision removes it.

References
  1. Ensure logic handles state transitions correctly over time in append-only logs.

Comment on lines +163 to +165
declared = event.get("staging_path")
if isinstance(declared, str) and declared:
return declared

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-high high

The staging_path retrieved from the audit log is used directly without validation. This presents a path traversal risk: a tampered or malicious audit log could specify an absolute path (e.g., /etc) or a path using .. to escape the output_dir.

Since this tool is designed for compliance and forensic review, it should maintain the defense-in-depth posture established in _approve.py by validating that the staging path remains within the expected output directory boundary.

Comment thread forgelm/cli/subcommands/_approvals.py Outdated
Comment on lines +17 to +21
The dispatcher reuses the helpers shipped with Phase 9 in
:mod:`forgelm.cli.subcommands._approve` (`_find_human_approval_required_event`,
`_find_human_approval_decision_event`) so the JSONL parser, the malformed-line
accounting, and the ``output_dir/audit_log.jsonl`` convention live in exactly
one place.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The module docstring claims to reuse helpers from _approve.py, but the implementation actually duplicates much of the logic (e.g., audit log parsing, event name constants, and error handling). This increases the maintenance burden and the risk of the two subcommands drifting apart in behavior.

Consider importing shared constants and helpers from _approve.py or moving them to a common utility module within the cli package to ensure consistency across the approval family of commands.

@sourcery-ai sourcery-ai 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.

Hey - I've found 6 issues, and left some high level feedback:

  • _staging_dir_for_event's implementation doesn't match its docstring: for older logs without staging_pathyou currently returnNonewhen<output_dir>/final_model.stagingis missing, whereas the docstring says you always synthesise the canonical path and let the caller decide how to handle a non-existent directory; consider always returning the fallback path and lettingstaging_existsbe derived solely fromos.path.isdir`.
  • The approval-related constants/helpers (event names, terminal set, audit-log filename, error-envelope helper, JSONL parsing) are redefined here instead of being shared with _approve; extracting these into a small shared module would prevent them drifting if the audit schema or filenames change in the future.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- _staging_dir_for_event`'s implementation doesn't match its docstring: for older logs without `staging_path` you currently return `None` when `<output_dir>/final_model.staging` is missing, whereas the docstring says you always synthesise the canonical path and let the caller decide how to handle a non-existent directory; consider always returning the fallback path and letting `staging_exists` be derived solely from `os.path.isdir`.
- The approval-related constants/helpers (event names, terminal set, audit-log filename, error-envelope helper, JSONL parsing) are redefined here instead of being shared with `_approve`; extracting these into a small shared module would prevent them drifting if the audit schema or filenames change in the future.

## Individual Comments

### Comment 1
<location path="tests/test_approvals_listing.py" line_range="135-144" />
<code_context>
+    def test_rejected_run_also_excluded_from_pending(self, tmp_path: Path, capsys) -> None:
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test for `--show` on a rejected run to exercise the `rejected` status classification and chain rendering.

You already test that rejected runs are excluded from `--pending`, but there’s no test that `forgelm approvals --show` reports `status == "rejected"` and includes the final `human_approval.rejected` event in the chain. Please add a JSON-path test like `test_show_granted_run_status_granted`, but for `decision="rejected"`, to cover `_classify_chain`’s rejected branch and verify the rejected-run UX.

Suggested implementation:

```python
    def test_rejected_run_also_excluded_from_pending(self, tmp_path: Path, capsys) -> None:
        # Reject (like granted) is a terminal decision; rejected runs must
        # not appear in --pending even though their staging dir lingers.
        output_dir = tmp_path / "run"
        _seed_run(output_dir, "fg-rejected0000", decision="rejected")
        _seed_run(output_dir, "fg-pending00000")

        from forgelm.cli import _run_approvals_cmd

        with pytest.raises(SystemExit) as ei:
            _run_approvals_cmd(_build_args(output_dir, pending=True), output_format="json")
        assert ei.value.code == 0

        payload = json.loads(capsys.readouterr().out)
        assert payload["success"] is True
        # Only the still-pending run should be reported as pending
        assert payload["count"] == 1
        run_ids = {summary["run_id"] for summary in payload["pending"]}
        assert run_ids == {"fg-pending00000"}

    def test_show_rejected_run_status_rejected(self, tmp_path: Path, capsys) -> None:
        # `forgelm approvals --show` for a rejected run should:
        #   * classify the run status as "rejected"
        #   * include the final human_approval.rejected event in the chain
        output_dir = tmp_path / "run"
        run_id = "fg-rejected0000"
        _seed_run(output_dir, run_id, decision="rejected")

        from forgelm.cli import _run_approvals_cmd

        with pytest.raises(SystemExit) as ei:
            _run_approvals_cmd(
                _build_args(output_dir, show=True, run_ids=[run_id]),
                output_format="json",
            )
        assert ei.value.code == 0

        payload = json.loads(capsys.readouterr().out)

        # Basic success + status classification
        assert payload["success"] is True
        # Mirror the JSON-path used in `test_show_granted_run_status_granted`
        # (typically something like payload["run"]["status"] or payload["status"])
        # Here we assume the top-level "status" key is used, as in the granted test.
        assert payload["status"] == "rejected"

        # Verify the rendered chain includes the final human_approval.rejected event.
        # This mirrors the chain JSON-path used in the granted-status test.
        chain = payload["chain"]
        assert isinstance(chain, list)
        assert chain, "expected at least one event in the rendered chain"
        last_event = chain[-1]
        # Assuming the event type is exposed as "type" and matches "human_approval.rejected"
        assert last_event["type"] == "human_approval.rejected"

```

To align this with your existing tests and JSON schema, you should:

1. Adjust the JSON paths to match what `test_show_granted_run_status_granted` already asserts:
   - If that test uses `payload["run"]["status"]` (or `payload["show"]["status"]`, etc.), update `assert payload["status"] == "rejected"` to use the same path.
   - If the chain is nested (e.g. `payload["run"]["chain"]` or `payload["events"]`), update `chain = payload["chain"]` accordingly.
2. Align the event-type assertion with your existing event schema:
   - If events use a different key (e.g. `"event_type"` or `"name"`), change `last_event["type"]` to that key.
   - If the value for a rejected approval is different (e.g. `"human_approval.rejected.v1"`), update the expected string to match.
3. If `test_rejected_run_also_excluded_from_pending` already had additional assertions after the snippet you shared, re-apply or merge them into the updated function body as needed.
</issue_to_address>

### Comment 2
<location path="tests/test_approvals_listing.py" line_range="152-161" />
<code_context>
+        assert payload["count"] == 1
+        assert payload["pending"][0]["run_id"] == "fg-pending00000"
+
+    def test_pending_summary_carries_metrics_and_staging_path(self, tmp_path: Path, capsys) -> None:
+        output_dir = tmp_path / "run"
+        staging_dir = _seed_run(output_dir, "fg-zzzz00000000")
+
+        from forgelm.cli import _run_approvals_cmd
+
+        with pytest.raises(SystemExit):
+            _run_approvals_cmd(_build_args(output_dir, pending=True), output_format="json")
+        payload = json.loads(capsys.readouterr().out)
+        summary = payload["pending"][0]
+        assert summary["staging_path"] == str(staging_dir)
+        assert summary["staging_exists"] is True
+        assert summary["metrics"] == {"eval_loss": 0.42}
+        assert summary["reason"] == "require_human_approval=true"
+
+    def test_text_output_renders_table(self, tmp_path: Path, capsys) -> None:
</code_context>
<issue_to_address>
**suggestion (testing):** Consider a test that covers the fallback `staging_exists=False` / `"MISSING"` path when the staging directory is absent.

Currently the tests only cover the case where the staging directory exists and `staging_path` is present. Since `_summarise_pending` and `_emit_pending_text` also handle missing staging (setting `staging_exists` to `False` and rendering `"MISSING"`), please add a test that seeds a `human_approval.required` event without `final_model.staging` (or with `create_staging=False`) and asserts that `staging_exists is False` and `"MISSING"` appears in the text output.

Suggested implementation:

```python
    def test_pending_summary_missing_staging_sets_flag_and_text_fallback(
        self, tmp_path: Path, capsys
    ) -> None:
        output_dir = tmp_path / "run"
        _seed_run(output_dir, "fg-missing0000000", create_staging=False)

        from forgelm.cli import _run_approvals_cmd

        # JSON output: staging_exists should be False when staging is missing.
        with pytest.raises(SystemExit):
            _run_approvals_cmd(_build_args(output_dir, pending=True), output_format="json")
        payload = json.loads(capsys.readouterr().out)
        summary = payload["pending"][0]
        assert summary["staging_exists"] is False

        # Text output: the table should render the "MISSING" fallback.
        with pytest.raises(SystemExit):
            _run_approvals_cmd(_build_args(output_dir, pending=True), output_format="text")
        out = capsys.readouterr().out
        assert "MISSING" in out

    def test_text_output_renders_table(self, tmp_path: Path, capsys) -> None:
        output_dir = tmp_path / "run"
        _seed_run(output_dir, "fg-xyz000000000")

        from forgelm.cli import _run_approvals_cmd

        with pytest.raises(SystemExit):
            _run_approvals_cmd(_build_args(output_dir, pending=True), output_format="text")
        out = capsys.readouterr().out
        # Table headers + run id present.
        assert "RUN_ID" in out

```

This test assumes that `_seed_run` accepts a `create_staging` keyword argument which, when set to `False`, will omit the `final_model.staging` directory and associated metadata while still emitting a `human_approval.required` event. If the helper uses a different parameter name or mechanism to skip staging, update the `_seed_run` call accordingly (e.g. `with_staging=False` or omitting a `staging_dir` argument).
</issue_to_address>

### Comment 3
<location path="tests/test_approvals_listing.py" line_range="104-113" />
<code_context>
+
+
+class TestApprovalsPending:
+    def test_empty_audit_log_returns_empty_list(self, tmp_path: Path, capsys) -> None:
+        output_dir = tmp_path / "run"
+        output_dir.mkdir()
+        # No audit_log.jsonl on disk → friendly empty path, exit 0.
+        from forgelm.cli import _run_approvals_cmd
+
+        with pytest.raises(SystemExit) as ei:
+            _run_approvals_cmd(_build_args(output_dir, pending=True), output_format="text")
+        assert ei.value.code == 0
+        captured = capsys.readouterr().out
+        assert "nothing to list" in captured.lower() or "no pending" in captured.lower()
+
+    def test_three_runs_one_decided_two_pending(self, tmp_path: Path, capsys) -> None:
</code_context>
<issue_to_address>
**suggestion (testing):** Add a JSON-output variant for the missing `audit_log.jsonl` case to verify the `pending` envelope shape.

This covers the text output when `audit_log.jsonl` is missing, but `_run_approvals_list_pending` has distinct logic for `output_format="json"` (empty `pending` array with `count: 0`). Please add a similar test using `_run_approvals_cmd` with `output_format="json"` in the same setup and assert the JSON payload (e.g., `{"success": True, "pending": [], "count": 0}`) to lock in that contract.

Suggested implementation:

```python
class TestApprovalsPending:
    def test_empty_audit_log_returns_empty_list(self, tmp_path: Path, capsys) -> None:
        output_dir = tmp_path / "run"
        output_dir.mkdir()
        # No audit_log.jsonl on disk → friendly empty path, exit 0.
        from forgelm.cli import _run_approvals_cmd

        with pytest.raises(SystemExit) as ei:
            _run_approvals_cmd(_build_args(output_dir, pending=True), output_format="text")
        assert ei.value.code == 0
        captured = capsys.readouterr().out
        assert "nothing to list" in captured.lower() or "no pending" in captured.lower()

    def test_empty_audit_log_returns_empty_json_envelope(self, tmp_path: Path, capsys) -> None:
        output_dir = tmp_path / "run"
        output_dir.mkdir()
        # No audit_log.jsonl on disk → JSON envelope with empty pending list, count 0, exit 0.
        from forgelm.cli import _run_approvals_cmd
        import json

        with pytest.raises(SystemExit) as ei:
            _run_approvals_cmd(_build_args(output_dir, pending=True), output_format="json")
        assert ei.value.code == 0
        captured = capsys.readouterr().out
        payload = json.loads(captured)
        assert payload.get("success") is True
        assert payload.get("pending") == []
        assert payload.get("count") == 0

    def test_three_runs_one_decided_two_pending(self, tmp_path: Path, capsys) -> None:
        # Three runs, one already granted → listing returns the other two.
        output_dir = tmp_path / "run"
        _seed_run(output_dir, "fg-aaaa00000000")
        _seed_run(output_dir, "fg-bbbb00000000", decision="granted")
        _seed_run(output_dir, "fg-cccc00000000")

        from forgelm.cli import _run_approvals_cmd

        with pytest.raises(SystemExit) as ei:
            _run_approvals_cmd(_build_args(output_dir, pending=True), output_format="json")

```

If `json` is already imported at the top of the file, you can remove the inline `import json` inside the new test and rely on the module-level import instead to avoid duplication.
</issue_to_address>

### Comment 4
<location path="tests/test_approvals_listing.py" line_range="290-299" />
<code_context>
+class TestApprovalsMalformedLines:
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding a malformed-lines test for the `--show` path to exercise `_collect_run_audit_chain`'s robustness.

You already cover malformed JSONL for `--pending` via `_iter_audit_events`, but there’s no equivalent coverage for `--show`. Please add a test where a run has one valid `human_approval.required` event, followed by a malformed line and a non-dict JSON value, then invoke `--show` (JSON output) for that run and assert the `chain` and `status` remain correct. That will exercise this robustness on the `--show` path as well.

Suggested implementation:

```python
# ---------------------------------------------------------------------------
# Robustness against malformed audit log lines
# ---------------------------------------------------------------------------


class TestApprovalsMalformedLines:
    def test_skipped_lines_do_not_hide_pending_runs(self, tmp_path: Path, capsys) -> None:
        # Simulate a corrupt audit log: a real `required` event followed by
        # a malformed line and a JSON list (non-dict root).  The pending
        # listing must still surface the legitimate run.
        #
        # This exercises robustness of the `--pending` path (via
        # `_iter_audit_events`), ensuring that bad lines do not hide a
        # legitimately pending run.
        run_id = "run_pending_malformed"
        output_dir = tmp_path / run_id
        output_dir.mkdir()

        audit_path = output_dir / "audit_log.jsonl"

        # 1) Valid human_approval.required event
        _write_event(
            audit_path,
            "human_approval.required",
            run_id=run_id,
            data={"prompt": "Please approve this run"},
        )

        # 2) Malformed JSON line that should be skipped
        audit_path.write_text(
            audit_path.read_text()
            + "{ this is not valid json\n",
            encoding="utf-8",
        )

        # 3) A syntactically valid JSON value with a non-dict root that
        #    should also be ignored by the iterator.
        audit_path.write_text(
            audit_path.read_text()
            + json.dumps(["not-a-dict-root"])  # type: ignore[list-item]
            + "\n",
            encoding="utf-8",
        )

        # Run the approvals listing in "pending" mode.
        #
        # NOTE: The exact helper/CLI invocation may differ in your codebase
        # (e.g. click runner, subprocess, or a direct function call). See
        # <additional_changes> for what may need to be adjusted.
        result = _run_approvals_cli(
            [
                "approvals",
                "--pending",
                "--output-dir",
                str(tmp_path),
                "--format",
                "json",
            ]
        )

        captured = capsys.readouterr()
        stdout = captured.out or result.stdout

        pending = json.loads(stdout)
        run_ids = {item["run_id"] for item in pending}
        assert run_id in run_ids

    def test_malformed_lines_do_not_break_show_chain_json(self, tmp_path: Path, capsys) -> None:
        # Similar corruption scenario as above, but exercise the `--show`
        # path (which uses `_collect_run_audit_chain`) instead of
        # `--pending`. We ensure that:
        #
        #   * The valid `human_approval.required` event remains in the chain
        #   * Malformed and non-dict JSONL lines do not break the command
        #   * The reported status for the run remains correct
        run_id = "run_show_malformed"
        output_dir = tmp_path / run_id
        output_dir.mkdir()

        audit_path = output_dir / "audit_log.jsonl"

        # 1) Valid required event that should be present in the chain
        _write_event(
            audit_path,
            "human_approval.required",
            run_id=run_id,
            data={"prompt": "Approve me via --show"},
        )

        # 2) Malformed JSON line
        audit_path.write_text(
            audit_path.read_text()
            + "{ malformed json line\n",
            encoding="utf-8",
        )

        # 3) Valid JSON but with non-dict root (e.g. a simple string)
        audit_path.write_text(
            audit_path.read_text()
            + json.dumps("not-a-dict-root")
            + "\n",
            encoding="utf-8",
        )

        # Invoke the approvals listing in "show" mode with JSON output for
        # this specific run. The CLI is expected to use
        # `_collect_run_audit_chain` internally and gracefully skip the bad
        # lines while still providing a correct chain and status.
        result = _run_approvals_cli(
            [
                "approvals",
                "--show",
                run_id,
                "--output-dir",
                str(tmp_path),
                "--format",
                "json",
            ]
        )

        captured = capsys.readouterr()
        stdout = captured.out or result.stdout

        show_output = json.loads(stdout)

        # The exact schema may differ, but typically we expect something like:
        #   {
        #       "run_id": "...",
        #       "status": "pending" | "...",
        #       "chain": [ ... events ... ],
        #       ...
        #   }
        #
        # We assert that:
        #   * status is still a non-terminal status (e.g. "pending") since
        #     only `human_approval.required` exists in the log.
        #   * the chain contains exactly one event corresponding to the
        #     required-approval event we wrote.
        assert show_output["run_id"] == run_id

        # Adjust the expected status string here if your implementation uses
        # a different vocabulary (for example "waiting_for_approval").
        assert show_output["status"] in ("pending", "waiting_for_approval")

        chain = show_output["chain"]
        assert isinstance(chain, list)
        assert len(chain) == 1

        event = chain[0]
        assert event["event_type"] == "human_approval.required"
        assert event["run_id"] == run_id
        assert event["data"]["prompt"] == "Approve me via --show"

```

for what may need to be adjusted.
        result = _run_approvals_cli(
            [
                "approvals",
                "--pending",
                "--output-dir",
                str(tmp_path),
                "--format",
                "json",
            ]
        )

        captured = capsys.readouterr()
        stdout = captured.out or result.stdout

        pending = json.loads(stdout)
        run_ids = {item["run_id"] for item in pending}
        assert run_id in run_ids

    def test_malformed_lines_do_not_break_show_chain_json(self, tmp_path: Path, capsys) -> None:
        # Similar corruption scenario as above, but exercise the `--show`
        # path (which uses `_collect_run_audit_chain`) instead of
        # `--pending`. We ensure that:
        #
        #   * The valid `human_approval.required` event remains in the chain
        #   * Malformed and non-dict JSONL lines do not break the command
        #   * The reported status for the run remains correct
        run_id = "run_show_malformed"
        output_dir = tmp_path / run_id
        output_dir.mkdir()

        audit_path = output_dir / "audit_log.jsonl"

        # 1) Valid required event that should be present in the chain
        _write_event(
            audit_path,
            "human_approval.required",
            run_id=run_id,
            data={"prompt": "Approve me via --show"},
        )

        # 2) Malformed JSON line
        audit_path.write_text(
            audit_path.read_text()
            + "{ malformed json line\n",
            encoding="utf-8",
        )

        # 3) Valid JSON but with non-dict root (e.g. a simple string)
        audit_path.write_text(
            audit_path.read_text()
            + json.dumps("not-a-dict-root")
            + "\n",
            encoding="utf-8",
        )

        # Invoke the approvals listing in "show" mode with JSON output for
        # this specific run. The CLI is expected to use
        # `_collect_run_audit_chain` internally and gracefully skip the bad
        # lines while still providing a correct chain and status.
        result = _run_approvals_cli(
            [
                "approvals",
                "--show",
                run_id,
                "--output-dir",
                str(tmp_path),
                "--format",
                "json",
            ]
        )

        captured = capsys.readouterr()
        stdout = captured.out or result.stdout

        show_output = json.loads(stdout)

        # The exact schema may differ, but typically we expect something like:
        #   {
        #       "run_id": "...",
        #       "status": "pending" | "...",
        #       "chain": [ ... events ... ],
        #       ...
        #   }
        #
        # We assert that:
        #   * status is still a non-terminal status (e.g. "pending") since
        #     only `human_approval.required` exists in the log.
        #   * the chain contains exactly one event corresponding to the
        #     required-approval event we wrote.
        assert show_output["run_id"] == run_id

        # Adjust the expected status string here if your implementation uses
        # a different vocabulary (for example "waiting_for_approval").
        assert show_output["status"] in ("pending", "waiting_for_approval")

        chain = show_output["chain"]
        assert isinstance(chain, list)
        assert len(chain) == 1

        event = chain[0]
        assert event["event_type"] == "human_approval.required"
        assert event["run_id"] == run_id
        assert event["data"]["prompt"] == "Approve me via --show"
>>>>>>> REPLACE
</file_operation>
</file_operations>

<additional_changes>
1. The above tests assume:
   - A helper `_run_approvals_cli(argv: list[str])` exists and returns an object with a `stdout` attribute, or that `capsys` can capture its printed output. If your file currently invokes the CLI differently (e.g. using a pytest `cli_runner`, `subprocess.run`, or a direct function), adapt the calls to match that pattern.
   - The JSON schema for `--pending --format json` is a list of entries containing a `"run_id"` field.
   - The JSON schema for `--show --format json` is a single-object response with `"run_id"`, `"status"`, and `"chain"` keys, where `"chain"` is a list of events that each include `"event_type"`, `"run_id"`, and `"data"`.

2. If your CLI uses a different option name than `"--format json"` (e.g. `"--json"` or `"--output-format json"`), update the argument list accordingly in both tests.

3. If the status terminology for a run that is awaiting human approval is not `"pending"` or `"waiting_for_approval"`, update the `assert show_output["status"] in (...)` line to match the actual status value used in your implementation.

4. Ensure `json` and any helper imports used here (`_run_approvals_cli`, `_write_event`) are present at the top of `tests/test_approvals_listing.py`. If `_write_event` is defined in a different module, import it from there instead of relying on it being in the same file.
</issue_to_address>

### Comment 5
<location path="tests/test_approvals_listing.py" line_range="358-367" />
<code_context>
+
+
+@pytest.mark.usefixtures("caplog")
+class TestApprovalsErrorMessages:
+    def test_unknown_run_logs_actionable_error(self, tmp_path: Path, caplog) -> None:
+        import logging
+
+        output_dir = tmp_path / "run"
+        _seed_run(output_dir, "fg-real00000000")
+
+        from forgelm.cli import _run_approvals_cmd
+
+        with caplog.at_level(logging.ERROR, logger="forgelm.cli"):
+            with patch("forgelm.cli._collect_pending_runs"):  # ensure not called by --show
+                with pytest.raises(SystemExit) as ei:
+                    _run_approvals_cmd(_build_args(output_dir, show="fg-fake00000000"), output_format="text")
+        assert ei.value.code == 1
+        assert "fg-fake00000000" in caplog.text
</code_context>
<issue_to_address>
**suggestion (testing):** You might also want a JSON-output error test for `--show` on an unknown run to lock in the structured error envelope.

Since `_output_error_and_exit` also formats errors for JSON as `{ "success": False, "error": ... }`, please add a similar test that calls `_run_approvals_cmd` with `output_format="json"` and a non-existent run ID, then asserts `success is False` and that the `error` message includes the run ID. This will lock in the behavior relied on by CI/automation for JSON mode.
</issue_to_address>

### Comment 6
<location path="docs/usermanuals/tr/compliance/human-oversight.md" line_range="159" />
<code_context>
+  - tokenizer_config.json
 ```

+Granted / rejected bir koşu üzerinde `--show` çalıştırıldığında tam zaman çizelgesi (talep → karar) ve son onaylayan + yorum yazdırılır. Bilinmeyen bir `run_id` üzerinde `--show` net bir hata mesajıyla 1 kodu ile çıkar.
+
 ## Sık hatalar
</code_context>
<issue_to_address>
**nitpick (typo):** Küçük Türkçe dilbilgisi düzeltmesi: "1 kodu ile" yerine "1 koduyla" daha doğal.

Anlaşılır olsa da Türkçe’de bu tip durumlarda “1 koduyla çıkar” daha doğal. Cümleyi buna göre güncelleyebilirsiniz.

```suggestion
Granted / rejected bir koşu üzerinde `--show` çalıştırıldığında tam zaman çizelgesi (talep → karar) ve son onaylayan + yorum yazdırılır. Bilinmeyen bir `run_id` üzerinde `--show` net bir hata mesajıyla 1 koduyla çıkar.
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +135 to +144
def test_rejected_run_also_excluded_from_pending(self, tmp_path: Path, capsys) -> None:
# Reject (like granted) is a terminal decision; rejected runs must
# not appear in --pending even though their staging dir lingers.
output_dir = tmp_path / "run"
_seed_run(output_dir, "fg-rejected0000", decision="rejected")
_seed_run(output_dir, "fg-pending00000")

from forgelm.cli import _run_approvals_cmd

with pytest.raises(SystemExit) as ei:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Add a test for --show on a rejected run to exercise the rejected status classification and chain rendering.

You already test that rejected runs are excluded from --pending, but there’s no test that forgelm approvals --show reports status == "rejected" and includes the final human_approval.rejected event in the chain. Please add a JSON-path test like test_show_granted_run_status_granted, but for decision="rejected", to cover _classify_chain’s rejected branch and verify the rejected-run UX.

Suggested implementation:

    def test_rejected_run_also_excluded_from_pending(self, tmp_path: Path, capsys) -> None:
        # Reject (like granted) is a terminal decision; rejected runs must
        # not appear in --pending even though their staging dir lingers.
        output_dir = tmp_path / "run"
        _seed_run(output_dir, "fg-rejected0000", decision="rejected")
        _seed_run(output_dir, "fg-pending00000")

        from forgelm.cli import _run_approvals_cmd

        with pytest.raises(SystemExit) as ei:
            _run_approvals_cmd(_build_args(output_dir, pending=True), output_format="json")
        assert ei.value.code == 0

        payload = json.loads(capsys.readouterr().out)
        assert payload["success"] is True
        # Only the still-pending run should be reported as pending
        assert payload["count"] == 1
        run_ids = {summary["run_id"] for summary in payload["pending"]}
        assert run_ids == {"fg-pending00000"}

    def test_show_rejected_run_status_rejected(self, tmp_path: Path, capsys) -> None:
        # `forgelm approvals --show` for a rejected run should:
        #   * classify the run status as "rejected"
        #   * include the final human_approval.rejected event in the chain
        output_dir = tmp_path / "run"
        run_id = "fg-rejected0000"
        _seed_run(output_dir, run_id, decision="rejected")

        from forgelm.cli import _run_approvals_cmd

        with pytest.raises(SystemExit) as ei:
            _run_approvals_cmd(
                _build_args(output_dir, show=True, run_ids=[run_id]),
                output_format="json",
            )
        assert ei.value.code == 0

        payload = json.loads(capsys.readouterr().out)

        # Basic success + status classification
        assert payload["success"] is True
        # Mirror the JSON-path used in `test_show_granted_run_status_granted`
        # (typically something like payload["run"]["status"] or payload["status"])
        # Here we assume the top-level "status" key is used, as in the granted test.
        assert payload["status"] == "rejected"

        # Verify the rendered chain includes the final human_approval.rejected event.
        # This mirrors the chain JSON-path used in the granted-status test.
        chain = payload["chain"]
        assert isinstance(chain, list)
        assert chain, "expected at least one event in the rendered chain"
        last_event = chain[-1]
        # Assuming the event type is exposed as "type" and matches "human_approval.rejected"
        assert last_event["type"] == "human_approval.rejected"

To align this with your existing tests and JSON schema, you should:

  1. Adjust the JSON paths to match what test_show_granted_run_status_granted already asserts:
    • If that test uses payload["run"]["status"] (or payload["show"]["status"], etc.), update assert payload["status"] == "rejected" to use the same path.
    • If the chain is nested (e.g. payload["run"]["chain"] or payload["events"]), update chain = payload["chain"] accordingly.
  2. Align the event-type assertion with your existing event schema:
    • If events use a different key (e.g. "event_type" or "name"), change last_event["type"] to that key.
    • If the value for a rejected approval is different (e.g. "human_approval.rejected.v1"), update the expected string to match.
  3. If test_rejected_run_also_excluded_from_pending already had additional assertions after the snippet you shared, re-apply or merge them into the updated function body as needed.

Comment on lines +152 to +161
def test_pending_summary_carries_metrics_and_staging_path(self, tmp_path: Path, capsys) -> None:
output_dir = tmp_path / "run"
staging_dir = _seed_run(output_dir, "fg-zzzz00000000")

from forgelm.cli import _run_approvals_cmd

with pytest.raises(SystemExit):
_run_approvals_cmd(_build_args(output_dir, pending=True), output_format="json")
payload = json.loads(capsys.readouterr().out)
summary = payload["pending"][0]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Consider a test that covers the fallback staging_exists=False / "MISSING" path when the staging directory is absent.

Currently the tests only cover the case where the staging directory exists and staging_path is present. Since _summarise_pending and _emit_pending_text also handle missing staging (setting staging_exists to False and rendering "MISSING"), please add a test that seeds a human_approval.required event without final_model.staging (or with create_staging=False) and asserts that staging_exists is False and "MISSING" appears in the text output.

Suggested implementation:

    def test_pending_summary_missing_staging_sets_flag_and_text_fallback(
        self, tmp_path: Path, capsys
    ) -> None:
        output_dir = tmp_path / "run"
        _seed_run(output_dir, "fg-missing0000000", create_staging=False)

        from forgelm.cli import _run_approvals_cmd

        # JSON output: staging_exists should be False when staging is missing.
        with pytest.raises(SystemExit):
            _run_approvals_cmd(_build_args(output_dir, pending=True), output_format="json")
        payload = json.loads(capsys.readouterr().out)
        summary = payload["pending"][0]
        assert summary["staging_exists"] is False

        # Text output: the table should render the "MISSING" fallback.
        with pytest.raises(SystemExit):
            _run_approvals_cmd(_build_args(output_dir, pending=True), output_format="text")
        out = capsys.readouterr().out
        assert "MISSING" in out

    def test_text_output_renders_table(self, tmp_path: Path, capsys) -> None:
        output_dir = tmp_path / "run"
        _seed_run(output_dir, "fg-xyz000000000")

        from forgelm.cli import _run_approvals_cmd

        with pytest.raises(SystemExit):
            _run_approvals_cmd(_build_args(output_dir, pending=True), output_format="text")
        out = capsys.readouterr().out
        # Table headers + run id present.
        assert "RUN_ID" in out

This test assumes that _seed_run accepts a create_staging keyword argument which, when set to False, will omit the final_model.staging directory and associated metadata while still emitting a human_approval.required event. If the helper uses a different parameter name or mechanism to skip staging, update the _seed_run call accordingly (e.g. with_staging=False or omitting a staging_dir argument).

Comment on lines +104 to +113
def test_empty_audit_log_returns_empty_list(self, tmp_path: Path, capsys) -> None:
output_dir = tmp_path / "run"
output_dir.mkdir()
# No audit_log.jsonl on disk → friendly empty path, exit 0.
from forgelm.cli import _run_approvals_cmd

with pytest.raises(SystemExit) as ei:
_run_approvals_cmd(_build_args(output_dir, pending=True), output_format="text")
assert ei.value.code == 0
captured = capsys.readouterr().out

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Add a JSON-output variant for the missing audit_log.jsonl case to verify the pending envelope shape.

This covers the text output when audit_log.jsonl is missing, but _run_approvals_list_pending has distinct logic for output_format="json" (empty pending array with count: 0). Please add a similar test using _run_approvals_cmd with output_format="json" in the same setup and assert the JSON payload (e.g., {"success": True, "pending": [], "count": 0}) to lock in that contract.

Suggested implementation:

class TestApprovalsPending:
    def test_empty_audit_log_returns_empty_list(self, tmp_path: Path, capsys) -> None:
        output_dir = tmp_path / "run"
        output_dir.mkdir()
        # No audit_log.jsonl on disk → friendly empty path, exit 0.
        from forgelm.cli import _run_approvals_cmd

        with pytest.raises(SystemExit) as ei:
            _run_approvals_cmd(_build_args(output_dir, pending=True), output_format="text")
        assert ei.value.code == 0
        captured = capsys.readouterr().out
        assert "nothing to list" in captured.lower() or "no pending" in captured.lower()

    def test_empty_audit_log_returns_empty_json_envelope(self, tmp_path: Path, capsys) -> None:
        output_dir = tmp_path / "run"
        output_dir.mkdir()
        # No audit_log.jsonl on disk → JSON envelope with empty pending list, count 0, exit 0.
        from forgelm.cli import _run_approvals_cmd
        import json

        with pytest.raises(SystemExit) as ei:
            _run_approvals_cmd(_build_args(output_dir, pending=True), output_format="json")
        assert ei.value.code == 0
        captured = capsys.readouterr().out
        payload = json.loads(captured)
        assert payload.get("success") is True
        assert payload.get("pending") == []
        assert payload.get("count") == 0

    def test_three_runs_one_decided_two_pending(self, tmp_path: Path, capsys) -> None:
        # Three runs, one already granted → listing returns the other two.
        output_dir = tmp_path / "run"
        _seed_run(output_dir, "fg-aaaa00000000")
        _seed_run(output_dir, "fg-bbbb00000000", decision="granted")
        _seed_run(output_dir, "fg-cccc00000000")

        from forgelm.cli import _run_approvals_cmd

        with pytest.raises(SystemExit) as ei:
            _run_approvals_cmd(_build_args(output_dir, pending=True), output_format="json")

If json is already imported at the top of the file, you can remove the inline import json inside the new test and rely on the module-level import instead to avoid duplication.

Comment on lines +290 to +299
class TestApprovalsMalformedLines:
def test_skipped_lines_do_not_hide_pending_runs(self, tmp_path: Path, capsys) -> None:
# Simulate a corrupt audit log: a real `required` event followed by
# a malformed line and a JSON list (non-dict root). The pending
# listing must still surface the legitimate run.
output_dir = tmp_path / "run"
output_dir.mkdir()
audit_path = output_dir / "audit_log.jsonl"
_write_event(
audit_path,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Consider adding a malformed-lines test for the --show path to exercise _collect_run_audit_chain's robustness.

You already cover malformed JSONL for --pending via _iter_audit_events, but there’s no equivalent coverage for --show. Please add a test where a run has one valid human_approval.required event, followed by a malformed line and a non-dict JSON value, then invoke --show (JSON output) for that run and assert the chain and status remain correct. That will exercise this robustness on the --show path as well.

Suggested implementation:

# ---------------------------------------------------------------------------
# Robustness against malformed audit log lines
# ---------------------------------------------------------------------------


class TestApprovalsMalformedLines:
    def test_skipped_lines_do_not_hide_pending_runs(self, tmp_path: Path, capsys) -> None:
        # Simulate a corrupt audit log: a real `required` event followed by
        # a malformed line and a JSON list (non-dict root).  The pending
        # listing must still surface the legitimate run.
        #
        # This exercises robustness of the `--pending` path (via
        # `_iter_audit_events`), ensuring that bad lines do not hide a
        # legitimately pending run.
        run_id = "run_pending_malformed"
        output_dir = tmp_path / run_id
        output_dir.mkdir()

        audit_path = output_dir / "audit_log.jsonl"

        # 1) Valid human_approval.required event
        _write_event(
            audit_path,
            "human_approval.required",
            run_id=run_id,
            data={"prompt": "Please approve this run"},
        )

        # 2) Malformed JSON line that should be skipped
        audit_path.write_text(
            audit_path.read_text()
            + "{ this is not valid json\n",
            encoding="utf-8",
        )

        # 3) A syntactically valid JSON value with a non-dict root that
        #    should also be ignored by the iterator.
        audit_path.write_text(
            audit_path.read_text()
            + json.dumps(["not-a-dict-root"])  # type: ignore[list-item]
            + "\n",
            encoding="utf-8",
        )

        # Run the approvals listing in "pending" mode.
        #
        # NOTE: The exact helper/CLI invocation may differ in your codebase
        # (e.g. click runner, subprocess, or a direct function call). See
        # <additional_changes> for what may need to be adjusted.
        result = _run_approvals_cli(
            [
                "approvals",
                "--pending",
                "--output-dir",
                str(tmp_path),
                "--format",
                "json",
            ]
        )

        captured = capsys.readouterr()
        stdout = captured.out or result.stdout

        pending = json.loads(stdout)
        run_ids = {item["run_id"] for item in pending}
        assert run_id in run_ids

    def test_malformed_lines_do_not_break_show_chain_json(self, tmp_path: Path, capsys) -> None:
        # Similar corruption scenario as above, but exercise the `--show`
        # path (which uses `_collect_run_audit_chain`) instead of
        # `--pending`. We ensure that:
        #
        #   * The valid `human_approval.required` event remains in the chain
        #   * Malformed and non-dict JSONL lines do not break the command
        #   * The reported status for the run remains correct
        run_id = "run_show_malformed"
        output_dir = tmp_path / run_id
        output_dir.mkdir()

        audit_path = output_dir / "audit_log.jsonl"

        # 1) Valid required event that should be present in the chain
        _write_event(
            audit_path,
            "human_approval.required",
            run_id=run_id,
            data={"prompt": "Approve me via --show"},
        )

        # 2) Malformed JSON line
        audit_path.write_text(
            audit_path.read_text()
            + "{ malformed json line\n",
            encoding="utf-8",
        )

        # 3) Valid JSON but with non-dict root (e.g. a simple string)
        audit_path.write_text(
            audit_path.read_text()
            + json.dumps("not-a-dict-root")
            + "\n",
            encoding="utf-8",
        )

        # Invoke the approvals listing in "show" mode with JSON output for
        # this specific run. The CLI is expected to use
        # `_collect_run_audit_chain` internally and gracefully skip the bad
        # lines while still providing a correct chain and status.
        result = _run_approvals_cli(
            [
                "approvals",
                "--show",
                run_id,
                "--output-dir",
                str(tmp_path),
                "--format",
                "json",
            ]
        )

        captured = capsys.readouterr()
        stdout = captured.out or result.stdout

        show_output = json.loads(stdout)

        # The exact schema may differ, but typically we expect something like:
        #   {
        #       "run_id": "...",
        #       "status": "pending" | "...",
        #       "chain": [ ... events ... ],
        #       ...
        #   }
        #
        # We assert that:
        #   * status is still a non-terminal status (e.g. "pending") since
        #     only `human_approval.required` exists in the log.
        #   * the chain contains exactly one event corresponding to the
        #     required-approval event we wrote.
        assert show_output["run_id"] == run_id

        # Adjust the expected status string here if your implementation uses
        # a different vocabulary (for example "waiting_for_approval").
        assert show_output["status"] in ("pending", "waiting_for_approval")

        chain = show_output["chain"]
        assert isinstance(chain, list)
        assert len(chain) == 1

        event = chain[0]
        assert event["event_type"] == "human_approval.required"
        assert event["run_id"] == run_id
        assert event["data"]["prompt"] == "Approve me via --show"

for what may need to be adjusted.
result = _run_approvals_cli(
[
"approvals",
"--pending",
"--output-dir",
str(tmp_path),
"--format",
"json",
]
)

    captured = capsys.readouterr()
    stdout = captured.out or result.stdout

    pending = json.loads(stdout)
    run_ids = {item["run_id"] for item in pending}
    assert run_id in run_ids

def test_malformed_lines_do_not_break_show_chain_json(self, tmp_path: Path, capsys) -> None:
    # Similar corruption scenario as above, but exercise the `--show`
    # path (which uses `_collect_run_audit_chain`) instead of
    # `--pending`. We ensure that:
    #
    #   * The valid `human_approval.required` event remains in the chain
    #   * Malformed and non-dict JSONL lines do not break the command
    #   * The reported status for the run remains correct
    run_id = "run_show_malformed"
    output_dir = tmp_path / run_id
    output_dir.mkdir()

    audit_path = output_dir / "audit_log.jsonl"

    # 1) Valid required event that should be present in the chain
    _write_event(
        audit_path,
        "human_approval.required",
        run_id=run_id,
        data={"prompt": "Approve me via --show"},
    )

    # 2) Malformed JSON line
    audit_path.write_text(
        audit_path.read_text()
        + "{ malformed json line\n",
        encoding="utf-8",
    )

    # 3) Valid JSON but with non-dict root (e.g. a simple string)
    audit_path.write_text(
        audit_path.read_text()
        + json.dumps("not-a-dict-root")
        + "\n",
        encoding="utf-8",
    )

    # Invoke the approvals listing in "show" mode with JSON output for
    # this specific run. The CLI is expected to use
    # `_collect_run_audit_chain` internally and gracefully skip the bad
    # lines while still providing a correct chain and status.
    result = _run_approvals_cli(
        [
            "approvals",
            "--show",
            run_id,
            "--output-dir",
            str(tmp_path),
            "--format",
            "json",
        ]
    )

    captured = capsys.readouterr()
    stdout = captured.out or result.stdout

    show_output = json.loads(stdout)

    # The exact schema may differ, but typically we expect something like:
    #   {
    #       "run_id": "...",
    #       "status": "pending" | "...",
    #       "chain": [ ... events ... ],
    #       ...
    #   }
    #
    # We assert that:
    #   * status is still a non-terminal status (e.g. "pending") since
    #     only `human_approval.required` exists in the log.
    #   * the chain contains exactly one event corresponding to the
    #     required-approval event we wrote.
    assert show_output["run_id"] == run_id

    # Adjust the expected status string here if your implementation uses
    # a different vocabulary (for example "waiting_for_approval").
    assert show_output["status"] in ("pending", "waiting_for_approval")

    chain = show_output["chain"]
    assert isinstance(chain, list)
    assert len(chain) == 1

    event = chain[0]
    assert event["event_type"] == "human_approval.required"
    assert event["run_id"] == run_id
    assert event["data"]["prompt"] == "Approve me via --show"

REPLACE
</file_operation>
</file_operations>

<additional_changes>

  1. The above tests assume:

    • A helper _run_approvals_cli(argv: list[str]) exists and returns an object with a stdout attribute, or that capsys can capture its printed output. If your file currently invokes the CLI differently (e.g. using a pytest cli_runner, subprocess.run, or a direct function), adapt the calls to match that pattern.
    • The JSON schema for --pending --format json is a list of entries containing a "run_id" field.
    • The JSON schema for --show --format json is a single-object response with "run_id", "status", and "chain" keys, where "chain" is a list of events that each include "event_type", "run_id", and "data".
  2. If your CLI uses a different option name than "--format json" (e.g. "--json" or "--output-format json"), update the argument list accordingly in both tests.

  3. If the status terminology for a run that is awaiting human approval is not "pending" or "waiting_for_approval", update the assert show_output["status"] in (...) line to match the actual status value used in your implementation.

  4. Ensure json and any helper imports used here (_run_approvals_cli, _write_event) are present at the top of tests/test_approvals_listing.py. If _write_event is defined in a different module, import it from there instead of relying on it being in the same file.

Comment on lines +358 to +367
class TestApprovalsErrorMessages:
def test_unknown_run_logs_actionable_error(self, tmp_path: Path, caplog) -> None:
import logging

output_dir = tmp_path / "run"
_seed_run(output_dir, "fg-real00000000")

from forgelm.cli import _run_approvals_cmd

with caplog.at_level(logging.ERROR, logger="forgelm.cli"):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): You might also want a JSON-output error test for --show on an unknown run to lock in the structured error envelope.

Since _output_error_and_exit also formats errors for JSON as { "success": False, "error": ... }, please add a similar test that calls _run_approvals_cmd with output_format="json" and a non-existent run ID, then asserts success is False and that the error message includes the run ID. This will lock in the behavior relied on by CI/automation for JSON mode.

- tokenizer_config.json
```

Granted / rejected bir koşu üzerinde `--show` çalıştırıldığında tam zaman çizelgesi (talep → karar) ve son onaylayan + yorum yazdırılır. Bilinmeyen bir `run_id` üzerinde `--show` net bir hata mesajıyla 1 kodu ile çıkar.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nitpick (typo): Küçük Türkçe dilbilgisi düzeltmesi: "1 kodu ile" yerine "1 koduyla" daha doğal.

Anlaşılır olsa da Türkçe’de bu tip durumlarda “1 koduyla çıkar” daha doğal. Cümleyi buna göre güncelleyebilirsiniz.

Suggested change
Granted / rejected bir koşu üzerinde `--show` çalıştırıldığında tam zaman çizelgesi (talep → karar) ve son onaylayan + yorum yazdırılır. Bilinmeyen bir `run_id` üzerinde `--show` net bir hata mesajıyla 1 kodu ile çıkar.
Granted / rejected bir koşu üzerinde `--show` çalıştırıldığında tam zaman çizelgesi (talep → karar) ve son onaylayan + yorum yazdırılır. Bilinmeyen bir `run_id` üzerinde `--show` net bir hata mesajıyla 1 koduyla çıkar.

Comment thread forgelm/cli/subcommands/_approvals.py
Closes the two HIGH findings + four bot suggestions.

HIGH fixes:

- F-25-01 (HIGH security): _staging_dir_for_event now reuses
  _staging_path_inside_output_dir from _approve.py to refuse
  staging_path values that escape output_dir.  A tampered audit log
  pointing staging_path at /etc no longer leaks a directory listing
  through _staging_contents.  Two new regression tests
  (TestApprovalsPathTraversalGuard, --show + --pending modes).
- F-25-02 (HIGH refactor): consolidated the audit-log JSONL parser
  into a new shared module forgelm/cli/subcommands/_audit_log_reader.py.
  _approve.py's _find_human_approval_required_event +
  _find_human_approval_decision_event and _approvals.py's
  _iter_audit_events all now delegate to iter_audit_events /
  find_latest_event_for_run there.  Future malformed-line policy
  fixes land in one place.  XPR-01 cross-PR observation closed
  (Phase 21 forgelm purge will use the same module).

MEDIUM fixes:

- F-25-03 (MEDIUM correctness): latest-wins semantics for re-staged
  runs.  _collect_pending_runs walked the log with setdefault (first
  required wins) — inconsistent with _approve.py's latest-wins
  decision-event pattern.  Now both use latest-wins (track the
  latest required line + latest terminal-decision line per run_id;
  a run is pending iff its latest required > latest decision).
  New regression: TestApprovalsLatestWinsRunIdReuse exercises the
  re-staged-after-rejection scenario.

Bot-suggested coverage (4 tests + TR typo):

- test_show_rejected_run_status_rejected: covers the rejected --show
  path classifier branch.
- test_pending_table_renders_missing_when_staging_absent: covers the
  MISSING staging cell rendering when create_staging=False.
- test_pending_empty_audit_log_json_envelope: pins the JSON envelope
  shape for the no-audit-log-yet case.
- test_show_unknown_run_id_json_envelope: pins the structured error
  envelope from _output_error_and_exit on unknown run_id under JSON
  output.
- test_show_robust_to_malformed_lines: --show inherits the same
  malformed-line tolerance as --pending (auto via shared
  _audit_log_reader).
- TR typo: "1 kodu ile" → "1 koduyla" (sourcery nit).

DEFERRED (out of this PR's scope):

- F-25-04 (MEDIUM doc): JSON envelope schema page —
  docs/reference/cli_json_schemas.md is XPR-03 cross-PR concern;
  same page should also document Phase 34 doctor envelope.  Better
  as one consolidated docs PR after this batch.
- F-25-05 (MEDIUM test): hardcoded timestamp parametrisation —
  noise-to-signal too low for this PR's blast radius; absorbed into
  Wave 2b factory growth (XPR-03 recommendation).
- F-25-06 (MEDIUM contract): defensive "neither flag set" branch —
  kept as belt-and-suspenders; argparse exit code 2 vs our 1
  divergence is intentional (operator misuse vs internal contract
  violation).
- LOW / NIT findings (07-14): not regressions; kept on the backlog.

Tests: 1028 → 1036 (+8). ruff format/check clean.
Coverage 68.42% → 68.68% (+0.26).

Closes (review IDs): F-25-01, F-25-02, F-25-03 + 4 bot tests + TR typo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented May 2, 2026

Copy link
Copy Markdown

@cemililik

Copy link
Copy Markdown
Collaborator Author

Wave 2a Round-1 multi-agent review — fixes pushed

All CRITICAL + HIGH findings from the multi-agent review (and the relevant inline bot comments) have been addressed in the latest commit on this branch. See the commit message for the per-finding mapping (review IDs F-25-*, gemini bot IDs, qodo bot IDs, sourcery bot IDs).

Per-PR test counts:

ruff format/check clean across all five PRs. Cross-PR XPR-01 concern (audit-log parser proliferation) closed in PR #25 via the new forgelm/cli/subcommands/_audit_log_reader.py shared module that _approve.py and _approvals.py now both delegate to (Phase 21 forgelm purge will use the same module).

Items deferred to follow-up PRs (with rationale) are listed under DEFERRED in each commit message.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@cemililik: Thanks for the detailed Wave 2a Round-1 summary! The changes look well-organized:

  • XPR-01 resolved — the new forgelm/cli/subcommands/_audit_log_reader.py shared module cleanly eliminates the audit-log parser proliferation concern, and the noted forward-compatibility with Phase 21 forgelm purge is a nice design win.
  • +8 tests on PR #25 (1028 → 1036) covering security and bot-flagged scenarios is a solid improvement.
  • ruff clean across all five PRs is good to see.

I'll kick off a fresh review now to verify the fixes against the previous findings.

[review]

@cemililik

Copy link
Copy Markdown
Collaborator Author

Superseded by #28 (consolidated Wave 2a integration). All commits from this branch are merged into closure/wave2a-integration and the Wave-2a Round-1 review fixes ride on top there. Closing without merge to keep history single-stream.

@cemililik cemililik closed this May 4, 2026
@cemililik
cemililik deleted the closure/faz37-approvals-listing branch May 10, 2026 11:35
cemililik added a commit that referenced this pull request Jun 15, 2026
… secret (PR#63-review)

#24: export_compliance_artifacts rollback swallowed os.remove/os.replace failures with except OSError: pass; collect and logger.error them before re-raising the original promotion error. #25: strict-HMAC guard rejected only None, letting an empty-string secret through; gate on 'not hmac_secret'. CodeRabbit #24,#25.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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