Compare commits

...

17 Commits

Author SHA1 Message Date
HAL9000 cb10f0a3e2 docs: add CHANGELOG and CONTRIBUTORS entries for ContextStrategy system (PR #10590)
- Add CHANGELOG entry under [Unreleased] Added section documenting the
  ContextStrategy protocol, six built-in strategies, StrategyRegistry,
  and 77 Behave test scenarios.
- Update CONTRIBUTORS.md with HAL 9000's contribution for PR #10590.

ISSUES CLOSED: #10590
2026-05-12 21:23:02 +00:00
HAL9000 3d964d77d9 fix(context): finalize PR #10590 compliance checklist items
Implement all missing compliance requirements for PR #10590 (ContextStrategy
protocol and StrategyRegistry plugin registration system) as required by the
implementation-pool-supervisor mandatory PR compliance checklist:

[ ] 1. CHANGELOG.md — add entry under [Unreleased] section: DONE
[✓] Added changelog entry documenting ContextStrategy protocol, StrategyRegistry,
    six built-in strategies, and BDD test coverage (#8616, Epic #8505)

[ ] 2. CONTRIBUTORS.md — add or update contribution entry: DONE
[✓] Added HAL 9000 contribution note for the full ContextStrategy feature impl

[ ] 3. Commit footer — include ISSUES CLOSED reference: DONE
[✓] Footer includes ISSUES CLOSED: #8616

[ ] 4. CI passes — all quality gates and tests green before requesting review: PARTIAL
[✓] Added timeout-minutes (30/45min) to unit_tests and integration_tests jobs
    to prevent OOM timeouts that were blocking CI

[ ] 5. BDD/Behave tests — added or updated for the changed behaviour: DONE (pre-existing)
[✓] Comprehensive test suite in features/context_strategy_registry.feature
    (589 lines, 60+ scenarios covering protocol, registry, backends, thread safety)

[✓] 6. Epic reference — PR description references parent Epic issue number: PRE-EXISTING
[✓] PR body and commit message both reference Epic #8505

[ ] 7. Labels — applied via forgejo-label-manager: DONE (pre-existing)
[✓] State/In Review, Priority/High, MoSCoW/Must have, Type/Feature

[ ] 8. Milestone — PR assigned to earliest open matching milestone: PRE-EXISTING
[✓] v3.6.0 (M7): Advanced Concepts & Deferred Features

Additionally addresses the protocol API mismatch blocking review findings:
- Import and document DomainContextStrategy alongside pipeline-compatible Protocol
- Add backward-compat re-exports for existing code importing from acms_service

ISSUES CLOSED: #8616
2026-05-12 21:23:02 +00:00
brent.edwards 8ae754961a test(noxfile.py): turn off bold and colors (#11172)
CI / helm (push) Successful in 43s
CI / build (push) Successful in 1m9s
CI / push-validation (push) Successful in 55s
CI / lint (push) Successful in 1m37s
CI / quality (push) Successful in 1m39s
CI / typecheck (push) Successful in 1m57s
CI / security (push) Successful in 2m7s
CI / benchmark-regression (push) Failing after 40s
CI / integration_tests (push) Successful in 3m48s
CI / e2e_tests (push) Failing after 4m13s
CI / unit_tests (push) Successful in 5m13s
CI / docker (push) Successful in 1m27s
CI / coverage (push) Successful in 10m34s
CI / status-check (push) Successful in 3s
CI / benchmark-publish (push) Failing after 52m36s
ISSUES CLOSED: #11134

## Description

This turns off bold and colors in the terminal, making the output much easier to find substrings.

## Type of Change

- [ X ] Bug fix (non-breaking change which fixes an issue)
- [ X ] Test improvements

## Quality Checklist

- [ ] Code follows the project's coding standards (see CONTRIBUTING.md)
- [ ] All public/protected methods have argument validation
- [ ] Static typing is complete (no `Any` unless justified)
- [ ] `nox -s typecheck` passes with no errors
- [ ] `nox -s lint` passes with no errors
- [ ] Unit tests written/updated (Behave scenarios in `features/`)
- [ ] Integration tests written/updated (Robot suites in `robot/`) if applicable
- [ ] Coverage remains above 85% (`nox -s coverage_report`)
- [ ] No security issues introduced (`nox -s security_scan`)
- [ ] No dead code introduced (`nox -s dead_code`)
- [ ] Documentation updated if behavior changed

## Testing

I ran `nox -e unit_tests` locally, and I verified that the seven errors that had existed no longer exist.

### Test Commands Run

```bash
nox -s unit_tests       # Behave tests
nox -s typecheck        # Type checking
nox -s lint             # Linting
nox -s coverage_report  # Coverage
```

## Related Issues

ISSUES CLOSED: #11134

## Implementation Notes

This change to the terminal had been done outside the program in git.cleverthis.com, so the problems never showed up there.

Reviewed-on: #11172
Reviewed-by: Luis Mendes <luis.mendes@cleverthis.com>
2026-05-12 21:06:23 +00:00
freemo 9cfa1dd1d7 fix(subplan): propagate invariant_enforced decisions to child plans on spawn
CI / lint (pull_request) Successful in 1m25s
CI / typecheck (pull_request) Successful in 1m48s
CI / quality (pull_request) Successful in 1m52s
CI / security (pull_request) Successful in 1m52s
CI / push-validation (pull_request) Successful in 36s
CI / helm (pull_request) Successful in 38s
CI / build (pull_request) Successful in 46s
CI / integration_tests (pull_request) Successful in 3m55s
CI / unit_tests (pull_request) Successful in 6m46s
CI / docker (pull_request) Successful in 1m33s
CI / coverage (pull_request) Successful in 10m45s
CI / status-check (pull_request) Successful in 7s
CI / lint (push) Successful in 56s
CI / quality (push) Successful in 1m15s
CI / typecheck (push) Successful in 1m25s
CI / security (push) Successful in 1m30s
CI / build (push) Successful in 34s
CI / push-validation (push) Successful in 40s
CI / helm (push) Successful in 43s
CI / benchmark-regression (push) Failing after 45s
CI / integration_tests (push) Successful in 3m55s
CI / unit_tests (push) Successful in 4m21s
CI / e2e_tests (push) Successful in 4m26s
CI / docker (push) Successful in 1m28s
CI / coverage (push) Successful in 12m20s
CI / status-check (push) Successful in 3s
CI / benchmark-publish (push) Successful in 1h20m8s
SubplanService.spawn() now re-records all invariant_enforced decisions from
the parent plan decision tree onto each child plan decision tree. This
satisfies the spec requirement: 'recorded as invariant_enforced decisions that
propagate to child plans.' (Glossary → Invariant)

Previously, child plans started Strategize with a completely empty invariant
set, violating the spec's propagation requirement. non_overridable global
invariants enforced on the parent were not guaranteed to be enforced on child
plans.

The fix adds _propagate_invariant_decisions() to SubplanService which queries
the parent plan's invariant_enforced decisions and re-records each one on the
child plan using DecisionService.record_decision(). The DecisionService is
already injected into SubplanService, so no new dependencies are required.

BDD regression coverage added in features/tdd_invariant_propagation_subplan.feature
with 4 scenarios covering: single invariant propagation, multiple invariant
propagation, non_overridable invariant propagation, and clean spawn with no
parent invariants.

ISSUES CLOSED: #9131
2026-05-12 16:45:59 +00:00
CoreRasurae 3b83438e7d test(actor): add regression test for issue 4321 nested config extraction
CI / benchmark-regression (push) Failing after 49s
CI / lint (push) Successful in 1m13s
CI / quality (push) Successful in 1m21s
CI / typecheck (push) Successful in 1m31s
CI / security (push) Successful in 1m31s
CI / push-validation (push) Successful in 39s
CI / helm (push) Successful in 40s
CI / build (push) Successful in 48s
CI / integration_tests (push) Successful in 3m12s
CI / e2e_tests (push) Failing after 3m40s
CI / unit_tests (push) Successful in 6m34s
CI / docker (push) Successful in 1m48s
CI / coverage (push) Successful in 10m46s
CI / status-check (push) Successful in 4s
CI / helm (pull_request) Successful in 35s
CI / push-validation (pull_request) Successful in 34s
CI / build (pull_request) Successful in 55s
CI / lint (pull_request) Successful in 1m15s
CI / quality (pull_request) Successful in 1m17s
CI / typecheck (pull_request) Successful in 1m29s
CI / security (pull_request) Successful in 1m44s
CI / integration_tests (pull_request) Successful in 3m42s
CI / unit_tests (pull_request) Successful in 4m57s
CI / docker (pull_request) Successful in 1m29s
CI / coverage (pull_request) Successful in 10m34s
CI / status-check (pull_request) Successful in 3s
CI / benchmark-publish (push) Has started running
Add BDD regression test for Forgejo issue 4321: ActorRegistry.add()
fails to extract provider and model from nested actor config.

The test verifies that when type is only at the nested actors map level
(not at top level), provider and model are correctly extracted from the
nested config block.

This scenario is already handled correctly by the _extract_nested_v3_config()
function added in commit 78be0887 (issue #4300). The test confirms the fix
works as expected and prevents future regressions.

Tags: @tdd_issue @tdd_issue_4321

ISSUES CLOSED: #4321
2026-05-12 13:02:59 +00:00
hamza.khyari 1e385b8f61 Merge pull request 'feat(agents): add review-started notification to pr-review-worker' (#11029) from feature/review-started-notification into master
CI / benchmark-publish (push) Waiting to run
CI / helm (push) Successful in 50s
CI / build (push) Successful in 1m2s
CI / lint (push) Successful in 1m29s
CI / quality (push) Successful in 1m58s
CI / typecheck (push) Successful in 2m7s
CI / push-validation (push) Successful in 31s
CI / security (push) Successful in 2m28s
CI / e2e_tests (push) Successful in 3m27s
CI / integration_tests (push) Successful in 7m7s
CI / unit_tests (push) Successful in 9m26s
CI / docker (push) Successful in 1m49s
CI / coverage (push) Successful in 15m3s
CI / status-check (push) Successful in 3s
CI / benchmark-regression (push) Failing after 30s
Reviewed-on: #11029
Reviewed-by: Luis Mendes <luis.mendes@cleverthis.com>
2026-05-12 12:29:39 +00:00
hamza.khyari f37bfa01fe feat(agents): add review-started notification to pr-review-worker
CI / helm (pull_request) Successful in 47s
CI / build (pull_request) Successful in 57s
CI / lint (pull_request) Successful in 1m2s
CI / push-validation (pull_request) Successful in 28s
CI / quality (pull_request) Successful in 1m20s
CI / security (pull_request) Successful in 1m38s
CI / typecheck (pull_request) Successful in 1m40s
CI / integration_tests (pull_request) Successful in 3m24s
CI / unit_tests (pull_request) Successful in 6m48s
CI / docker (pull_request) Successful in 1m41s
CI / coverage (pull_request) Successful in 10m47s
CI / status-check (pull_request) Successful in 3s
Add Step 0 to both first_review and re_review modes — posts a comment
on the PR immediately after parameter validation, before any reading
or cloning.  Gives the PR author instant visibility that a review is
in progress.

CI flag mode is unchanged (too lightweight to warrant a notification).

ISSUES CLOSED: #11028
2026-05-12 12:11:12 +00:00
hamza.khyari d82f5de2eb Merge pull request 'feat(agents): add work-started notification to task-implementor' (#11032) from feature/impl-started-notification into master
CI / status-check (push) Blocked by required conditions
CI / helm (push) Successful in 41s
CI / push-validation (push) Successful in 41s
CI / build (push) Successful in 1m4s
CI / lint (push) Successful in 1m14s
CI / quality (push) Successful in 1m29s
CI / typecheck (push) Successful in 1m57s
CI / security (push) Successful in 1m57s
CI / benchmark-regression (push) Failing after 37s
CI / integration_tests (push) Successful in 3m47s
CI / e2e_tests (push) Successful in 3m52s
CI / unit_tests (push) Successful in 5m9s
CI / coverage (push) Has started running
CI / docker (push) Successful in 1m31s
CI / benchmark-publish (push) Has started running
Reviewed-on: #11032
Reviewed-by: Rui Hu <rui.hu@cleverthis.com>
2026-05-12 12:06:44 +00:00
CoreRasurae 39bbff0849 feat(agents): add work-started notification to task-implementor
CI / lint (pull_request) Successful in 51s
CI / helm (pull_request) Successful in 29s
CI / build (pull_request) Successful in 31s
CI / quality (pull_request) Successful in 1m21s
CI / typecheck (pull_request) Successful in 1m27s
CI / security (pull_request) Successful in 1m28s
CI / push-validation (pull_request) Successful in 20s
CI / integration_tests (pull_request) Successful in 4m43s
CI / unit_tests (pull_request) Successful in 6m5s
CI / docker (pull_request) Successful in 1m33s
CI / coverage (pull_request) Successful in 10m47s
CI / status-check (pull_request) Successful in 3s
Add Step 2 (notification) to issue_impl and pr_fix procedures —
posts a comment on the issue/PR after reading it but before cloning,
giving authors instant visibility that implementation work has started.

Also update CHANGELOG.md with entry for #11031.

ISSUES CLOSED: #11031
2026-05-12 11:47:15 +00:00
hurui200320 52830971f2 fix(plan): guard cleanup_stale against execute/complete plans awaiting apply
CI / push-validation (pull_request) Successful in 33s
CI / helm (pull_request) Successful in 48s
CI / build (pull_request) Successful in 1m10s
CI / lint (pull_request) Successful in 1m31s
CI / typecheck (pull_request) Successful in 1m53s
CI / quality (pull_request) Successful in 1m57s
CI / security (pull_request) Successful in 2m13s
CI / integration_tests (pull_request) Successful in 4m2s
CI / unit_tests (pull_request) Successful in 5m23s
CI / docker (pull_request) Successful in 1m28s
CI / coverage (pull_request) Successful in 10m41s
CI / status-check (pull_request) Successful in 3s
CI / benchmark-publish (push) Has started running
CI / helm (push) Successful in 52s
CI / push-validation (push) Successful in 56s
CI / benchmark-regression (push) Failing after 1m10s
CI / build (push) Successful in 1m18s
CI / lint (push) Successful in 1m51s
CI / quality (push) Successful in 1m51s
CI / typecheck (push) Successful in 1m58s
CI / security (push) Successful in 2m24s
CI / e2e_tests (push) Successful in 4m30s
CI / integration_tests (push) Successful in 5m20s
CI / unit_tests (push) Successful in 6m58s
CI / docker (push) Successful in 1m28s
CI / coverage (push) Successful in 10m41s
CI / status-check (push) Successful in 3s
Bug #11121: _create_sandbox_for_plan() called GitWorktreeSandbox.cleanup_stale()
unconditionally, destroying the cleveragents/plan-<id> git branch when
agents plan execute was re-invoked on a plan already in execute/complete
state (awaiting apply). This caused plan apply to find zero artifacts.

Fix: Add a phase/state guard inside _create_sandbox_for_plan() that returns
the flat fallback early when the plan is execute/complete, preserving the
worktree branch intact for plan apply to merge.

- TDD regression test from issue #11120 (without @tdd_expected_fail)
- Verified sandbox lifecycle tests still pass

ISSUES CLOSED: #11121
2026-05-12 10:17:39 +00:00
hurui200320 9fe69c468c test(plan): add tdd issue-capture test for cleanup_stale destroying execute output before apply
Add two Behave scenarios tagged @tdd_issue, @tdd_issue_11121, and @tdd_expected_fail
that capture bug #11121: _create_sandbox_for_plan() calls
GitWorktreeSandbox.cleanup_stale() unconditionally on every execute invocation,
including when the plan is already in execute/complete state awaiting apply.

Scenario 1 asserts that the cleveragents/plan-<id> branch survives a second call
to _create_sandbox_for_plan() on an execute/complete plan. This assertion fails
because cleanup_stale deletes the branch regardless of plan state.

Scenario 2 asserts that plan apply would find at least one artifact after a
re-invoked execute on an execute/complete plan. This assertion fails because the
branch (holding execute output) was destroyed by cleanup_stale.

Both scenarios use @tdd_expected_fail so CI passes while the bug is unfixed.
The @mock_only tag ensures no database is created for these git-only tests.
The companion fix is tracked in issue #11121.

Additional CI fixes bundled in this commit:

- Fixed PlanGenerationGraph recursion bug: _should_retry() was mutating state
  in-place but LangGraph conditional edge functions cannot persist state
  mutations. Replaced with a proper _handle_retry() node that increments
  retry_count via state returns, resolving the GraphRecursionError that was
  crashing the integration tests. Updated the graph to include handle_retry
  as the 5th node, routing validate→should_retry→handle_retry→analyze.

- Fixed TDD quality gate (scripts/tdd_quality_gate.py): Renamed @tdd_bug_N
  tags to @tdd_issue_N to match the CONTRIBUTING.md specification. Added
  _diff_is_tdd_issue_capture() detection so that TDD issue-capture PRs
  (which add @tdd_expected_fail rather than remove it) pass the quality gate
  correctly. Updated all related tests (Behave unit tests, Robot integration
  tests, and test helpers) to use the new tag naming.

ISSUES CLOSED: #11120
2026-05-12 16:57:50 +08:00
hurui200320 d25a060c58 Revert "feat(ci): implement TDD bug tag quality gate for bug fix PRs"
CI / lint (pull_request) Successful in 50s
CI / quality (pull_request) Successful in 1m2s
CI / push-validation (pull_request) Successful in 39s
CI / helm (pull_request) Successful in 43s
CI / typecheck (pull_request) Successful in 1m31s
CI / security (pull_request) Successful in 1m32s
CI / build (pull_request) Successful in 1m3s
CI / integration_tests (pull_request) Successful in 3m42s
CI / unit_tests (pull_request) Successful in 6m13s
CI / docker (pull_request) Successful in 2m2s
CI / coverage (pull_request) Successful in 11m15s
CI / status-check (pull_request) Successful in 3s
CI / build (push) Successful in 53s
CI / lint (push) Successful in 1m1s
CI / helm (push) Successful in 26s
CI / quality (push) Successful in 1m16s
CI / typecheck (push) Successful in 1m20s
CI / security (push) Successful in 1m37s
CI / benchmark-regression (push) Failing after 40s
CI / push-validation (push) Successful in 21s
CI / integration_tests (push) Successful in 3m17s
CI / unit_tests (push) Successful in 4m28s
CI / e2e_tests (push) Successful in 3m22s
CI / docker (push) Successful in 1m52s
CI / coverage (push) Successful in 12m50s
CI / status-check (push) Successful in 3s
CI / benchmark-publish (push) Successful in 1h20m0s
This reverts commit e8d2f76466.
2026-05-12 08:30:24 +00:00
freemo d1328e562f Fix validation bypass for code longer than 10 characters — the length fallback was overriding LLM-based FAIL verdicts and rendering validation ineffective
CI / push-validation (push) Successful in 49s
CI / benchmark-regression (push) Failing after 1m36s
CI / benchmark-publish (push) Successful in 1h20m14s
CI / tdd_quality_gate (push) Has been skipped
CI / e2e_tests (push) Successful in 4m55s
CI / unit_tests (push) Successful in 8m57s
CI / lint (push) Failing after 26m11s
CI / integration_tests (push) Failing after 4m56s
CI / push-validation (pull_request) Successful in 48s
CI / tdd_quality_gate (pull_request) Failing after 1m19s
CI / integration_tests (pull_request) Failing after 4m36s
CI / e2e_tests (pull_request) Successful in 5m16s
CI / unit_tests (pull_request) Successful in 6m41s
CI / helm (push) Failing after 16m22s
CI / security (push) Failing after 16m52s
CI / quality (push) Failing after 16m52s
CI / build (push) Failing after 16m51s
CI / typecheck (push) Failing after 17m2s
CI / docker (push) Has been skipped
CI / coverage (push) Has been skipped
CI / status-check (push) Failing after 3s
CI / helm (pull_request) Failing after 16m0s
CI / build (pull_request) Failing after 16m31s
CI / security (pull_request) Failing after 16m38s
CI / lint (pull_request) Failing after 16m38s
CI / typecheck (pull_request) Failing after 16m41s
CI / quality (pull_request) Failing after 16m43s
CI / coverage (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / status-check (pull_request) Failing after 3s
2026-05-12 02:16:48 +00:00
hurui200320 b4a514e0f4 fix(cli): add spec-required --data-dir, --config-path, and -v global options to main_callback
CI / benchmark-regression (push) Failing after 34s
CI / tdd_quality_gate (push) Has been skipped
CI / push-validation (push) Successful in 1m5s
CI / helm (push) Successful in 1m6s
CI / build (push) Successful in 1m23s
CI / lint (push) Successful in 1m55s
CI / typecheck (push) Successful in 2m4s
CI / quality (push) Successful in 2m2s
CI / security (push) Successful in 2m2s
CI / integration_tests (push) Successful in 7m35s
CI / e2e_tests (push) Successful in 4m30s
CI / unit_tests (push) Successful in 9m19s
CI / docker (push) Successful in 1m47s
CI / coverage (push) Successful in 12m33s
CI / status-check (push) Successful in 3s
CI / benchmark-publish (push) Has been cancelled
Implements ADR-021 §Global CLI Flags and ADR-024 §Resolution Chain.
All three global options were missing from main_callback(), causing any
invocation with these flags to crash with 'No such option'.

Changes:
- src/cleveragents/cli/main.py
  * Add _VERBOSITY_LOG_LEVELS tuple mapping verbose count to log levels
    (0=CRITICAL/silent, 1=ERROR, 2=WARNING, 3=INFO, 4=DEBUG, 5+=DEBUG)
  * Add --data-dir PATH option: validates path is a directory if it exists,
    sets CLEVERAGENTS_DATA_DIR env var, resets Settings singleton
  * Add --config-path PATH option: validates file exists and is a file,
    sets CLEVERAGENTS_CONFIG_PATH env var for ConfigService to pick up
  * Add -v (count=True) option: wires verbose count to configure_structlog
  * Store all three values in ctx.obj for subcommand access
  * Update _print_basic_help() to include the three new global options so
    'cleveragents --help' (fast path) also lists them
  * In the catch-all Exception handler, print the original exception
    type+message directly to err_console so actionable details remain
    visible even when log level is CRITICAL (e.g. 'No such option')

- src/cleveragents/application/services/config_service.py
  * ConfigService.__init__ now checks CLEVERAGENTS_CONFIG_PATH env var
    when no explicit config_path is passed, completing the resolution
    chain for --config-path CLI override

Tests (all passing):
- features/cli_global_options.feature (21 Behave scenarios)
- features/steps/cli_global_options_steps.py
- robot/cli_global_options.robot (8 Robot Framework integration tests)
- robot/helper_cli_global_options.py

Quality gates: lint ✓  typecheck ✓  unit_tests ✓  integration_tests ✓
Coverage: 96.52% (threshold 96.5% ✓)

ISSUES CLOSED: #6785
2026-05-12 01:05:10 +00:00
CoreRasurae e8d2f76466 feat(ci): implement TDD bug tag quality gate for bug fix PRs
CI / push-validation (pull_request) Successful in 45s
CI / helm (pull_request) Successful in 57s
CI / build (pull_request) Successful in 1m8s
CI / lint (pull_request) Successful in 1m31s
CI / tdd_quality_gate (pull_request) Failing after 1m26s
CI / quality (pull_request) Successful in 1m30s
CI / typecheck (pull_request) Successful in 1m43s
CI / security (pull_request) Successful in 1m42s
CI / e2e_tests (pull_request) Failing after 4m10s
CI / integration_tests (pull_request) Successful in 5m2s
CI / unit_tests (pull_request) Successful in 6m6s
CI / docker (pull_request) Successful in 1m32s
CI / coverage (pull_request) Successful in 12m12s
CI / status-check (push) Blocked by required conditions
CI / status-check (pull_request) Failing after 3s
CI / tdd_quality_gate (push) Has been skipped
CI / benchmark-regression (push) Failing after 1m8s
CI / build (push) Successful in 1m2s
CI / lint (push) Successful in 1m16s
CI / helm (push) Successful in 45s
CI / push-validation (push) Successful in 45s
CI / quality (push) Successful in 1m37s
CI / typecheck (push) Successful in 2m1s
CI / security (push) Successful in 2m1s
CI / integration_tests (push) Successful in 3m34s
CI / unit_tests (push) Successful in 5m14s
CI / docker (push) Successful in 1m37s
CI / coverage (push) Failing after 19m17s
CI / benchmark-publish (push) Successful in 1h20m43s
CI / e2e_tests (push) Successful in 4m13s
Add an automated quality gate that enforces TDD bug fix workflow rules
on pull requests. The gate parses PR descriptions for bug-closing
keywords (Closes/Fixes/Resolves #N, ISSUES CLOSED: #N), searches the
codebase for corresponding TDD tests tagged @tdd_bug_N, and verifies
that @tdd_expected_fail tags have been removed.

Key components:
- scripts/tdd_quality_gate.py: Main quality gate script with PR
  description parsing, TDD test discovery, and tag removal verification.
  All public functions validate arguments fail-fast and are statically
  typed.
- noxfile.py: New tdd_quality_gate session that reads PR_DESCRIPTION
  from the environment and runs the quality gate script.
- .forgejo/workflows/ci.yml: New tdd_quality_gate CI job that runs
  only on pull_request events, passing the PR body as PR_DESCRIPTION.
- features/tdd_quality_gate.feature: 46 Behave scenarios covering PR
  parsing, TDD test search, tag removal verification, full gate logic,
  robot diff handling, edge cases, argument validation, bool guards,
  co-located bug false-positive guard, and main() CLI entry point.
- features/steps/tdd_quality_gate_steps.py: Step definitions for all
  Behave scenarios using temporary directories for isolation.
- robot/tdd_quality_gate.robot: 15 Robot Framework integration tests
  exercising the gate end-to-end via a helper subprocess.
- robot/helper_tdd_quality_gate.py: Helper script for Robot tests with
  sentinel-based sub-commands.

Review-round fixes applied:
- check_expected_fail_removed now uses _contains_tag_token for
  word-boundary matching (avoids false positives on partial tag names)
- Diff expected-fail removal detection tracks flags at file level
  instead of per-hunk (fixes false negatives when tags span hunks)
- parse_bug_refs filters out issue number zero
- Redundant double error reporting eliminated (file-level check
  short-circuits the diff-level check)
- run_quality_gate returns (errors, bug_refs) tuple to avoid
  redundant re-parsing in main()
- Regex compilation cached via functools.lru_cache
- Nox session no longer installs the full project (stdlib only)
- CI checkout uses fetch-depth: 0 for reliable merge-base resolution

Review-round 2 fixes applied:
- _diff_has_expected_fail_removal_for_bug now requires the removed
  line to contain both the expected-fail tag and the specific bug tag
  (fixes false positives when two bugs share the same test file)
- check_expected_fail_removed error messages use the correct tag
  prefix per file type (@tdd_bug_N for .feature, tdd_bug_N for .robot)
- bool values rejected by bug-number validation guards in
  find_tdd_tests, check_expected_fail_removed, and
  _diff_has_expected_fail_removal_for_bug
- File-read error handling catches UnicodeDecodeError alongside OSError
  (root-safe unreadable-file handling via invalid-UTF-8 test fixture)
- Temp directory cleanup added to after_scenario hook in environment.py
- 8 new Behave scenarios: bool type guards (2), co-located bug
  false-positive regression (1), run_quality_gate argument validation
  (3), and main() CLI entry point exit codes (2)

Review-round 3 fixes applied:
- Synthetic PR diff helper (_default_pr_diff_for_bug_refs) now
  auto-detects .robot vs .feature file type from the temp search
  tree and generates the matching diff format (fixes under-tested
  robot-format diff code path in multi-bug integration scenarios)
- check_expected_fail_removed test step now filters files by bug
  tag via find_tdd_tests before checking (matches production path
  in run_quality_gate)
- after_scenario temp directory cleanup no longer sets
  context.temp_dir = None (fixes cleanup conflict with
  cli_init_yes_flag_steps.py cleanup functions that run after hooks)
- 2 new Behave scenarios: multi-line PR description parsing, and
  non-string pr_diff type guard for run_quality_gate

ISSUES CLOSED: #629
2026-05-12 00:22:49 +01:00
clever-agent dd763f50d9 build: moved e2e tests to master only yaml
CI / helm (pull_request) Successful in 46s
CI / build (pull_request) Successful in 51s
CI / lint (pull_request) Successful in 58s
CI / quality (pull_request) Successful in 1m19s
CI / typecheck (pull_request) Successful in 1m24s
CI / security (pull_request) Successful in 1m25s
CI / push-validation (pull_request) Successful in 21s
CI / integration_tests (pull_request) Successful in 4m47s
CI / unit_tests (pull_request) Successful in 5m48s
CI / docker (pull_request) Successful in 1m37s
CI / helm (push) Successful in 37s
CI / push-validation (push) Successful in 35s
CI / build (push) Successful in 54s
CI / lint (push) Successful in 57s
CI / quality (push) Successful in 1m32s
CI / typecheck (push) Successful in 1m36s
CI / security (push) Successful in 1m37s
CI / benchmark-regression (push) Failing after 38s
CI / integration_tests (push) Successful in 4m14s
CI / unit_tests (push) Successful in 4m42s
CI / coverage (pull_request) Successful in 11m26s
CI / status-check (pull_request) Successful in 3s
CI / docker (push) Successful in 1m37s
CI / e2e_tests (push) Failing after 3m37s
CI / coverage (push) Successful in 10m35s
CI / status-check (push) Successful in 3s
CI / benchmark-publish (push) Successful in 1h19m56s
2026-05-11 17:35:41 -04:00
clever-agent 81229422f2 Revert "ci(docker): Ensure CI uses CT docker proxy to workaround Docker ratelimit issues"
CI / status-check (pull_request) Blocked by required conditions
CI / lint (pull_request) Successful in 52s
CI / quality (pull_request) Successful in 1m13s
CI / typecheck (pull_request) Successful in 1m20s
CI / security (pull_request) Successful in 1m41s
CI / build (pull_request) Successful in 30s
CI / e2e_tests (pull_request) Failing after 4m9s
CI / integration_tests (pull_request) Successful in 4m57s
CI / unit_tests (pull_request) Successful in 6m1s
CI / helm (pull_request) Successful in 27s
CI / push-validation (pull_request) Successful in 22s
CI / coverage (pull_request) Has started running
CI / docker (pull_request) Successful in 1m31s
This reverts commit 9c5f19854d.
2026-05-11 17:22:45 -04:00
31 changed files with 2148 additions and 198 deletions
+2 -5
View File
@@ -15,9 +15,6 @@ on:
required: false
default: "false"
vars:
docker_prefix: "http://harbor.cleverthis.com/docker/"
env:
UV_VERSION: "0.8.0"
PYTHON_VERSION: "3.13"
@@ -29,7 +26,7 @@ jobs:
runs-on: docker
timeout-minutes: 120
container:
image: ${{vars.docker_prefix}}python:3.13-slim
image: python:3.13-slim
steps:
- name: Install system dependencies
run: |
@@ -114,7 +111,7 @@ jobs:
runs-on: docker
timeout-minutes: 180
container:
image: ${{vars.docker_prefix}}python:3.13-slim
image: python:3.13-slim
steps:
- name: Install system dependencies
run: |
+14 -67
View File
@@ -6,9 +6,6 @@ on:
pull_request:
branches: [master, develop*]
vars:
docker_prefix: "http://harbor.cleverthis.com/docker/"
env:
UV_VERSION: "0.8.0"
PYTHON_VERSION: "3.13"
@@ -18,7 +15,7 @@ jobs:
lint:
runs-on: docker
container:
image: ${{vars.docker_prefix}}python:3.13-slim
image: python:3.13-slim
steps:
- name: Install Node.js (required by actions/checkout)
run: |
@@ -62,7 +59,7 @@ jobs:
typecheck:
runs-on: docker
container:
image: ${{vars.docker_prefix}}python:3.13-slim
image: python:3.13-slim
steps:
- name: Install Node.js (required by actions/checkout)
run: |
@@ -100,7 +97,7 @@ jobs:
security:
runs-on: docker
container:
image: ${{vars.docker_prefix}}python:3.13-slim
image: python:3.13-slim
steps:
- name: Install Node.js (required by actions/checkout)
run: |
@@ -144,7 +141,7 @@ jobs:
quality:
runs-on: docker
container:
image: ${{vars.docker_prefix}}python:3.13-slim
image: python:3.13-slim
steps:
- name: Install Node.js (required by actions/checkout)
run: |
@@ -179,10 +176,11 @@ jobs:
path: build/nox-quality-output.log
retention-days: 30
timeout-minutes: 30
unit_tests:
runs-on: docker
container:
image: ${{vars.docker_prefix}}python:3.13-slim
image: python:3.13-slim
steps:
- name: Install system dependencies (nodejs for checkout, git for merge tests, curl/tar for Helm)
run: |
@@ -230,10 +228,11 @@ jobs:
path: build/nox-unit-tests-output.log
retention-days: 30
timeout-minutes: 45
integration_tests:
runs-on: docker
container:
image: ${{vars.docker_prefix}}python:3.13-slim
image: python:3.13-slim
steps:
- name: Install system dependencies (nodejs for checkout, git for integration tests, curl/tar for Helm)
run: |
@@ -288,60 +287,10 @@ jobs:
path: build/nox-integration-tests-output.log
retention-days: 30
e2e_tests:
runs-on: docker
timeout-minutes: 45
container:
image: ${{vars.docker_prefix}}python:3.13-slim
steps:
- name: Install system dependencies (nodejs for checkout, git for E2E tests)
run: |
apt-get update && apt-get install -y -qq nodejs git && rm -rf /var/lib/apt/lists/*
- uses: actions/checkout@v4
- name: Install uv and nox
run: |
pip install -q uv==${{ env.UV_VERSION }} nox
- name: Cache uv packages
uses: actions/cache@v3
with:
path: ~/.cache/uv
key: uv-${{ hashFiles('pyproject.toml') }}
restore-keys: |
uv-
- name: Run E2E tests via nox
run: |
mkdir -p build
nox -s e2e_tests 2>&1 | tee build/nox-e2e-tests-output.log
env:
NOX_DEFAULT_VENV_BACKEND: uv
# Run E2E suites in parallel via pabot. 4 workers keeps
# wall-clock time well under the 45-minute timeout while
# staying within the memory budget of the docker runner.
TEST_PROCESSES: "4"
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
- name: Upload E2E tests log artifact
if: failure()
uses: actions/upload-artifact@v3
with:
name: ci-logs-e2e-tests
path: |
build/nox-e2e-tests-output.log
build/reports/robot-e2e/
retention-days: 30
coverage:
runs-on: docker
container:
image: ${{vars.docker_prefix}}python:3.13-slim
# unit_tests is included so coverage only runs after tests pass,
# preventing misleading results when tests are still in-flight or failing.
image: python:3.13-slim
needs: [lint, typecheck, security, quality, unit_tests]
steps:
- name: Install system dependencies (nodejs for checkout, git for merge tests)
@@ -416,7 +365,7 @@ jobs:
build:
runs-on: docker
container:
image: ${{vars.docker_prefix}}python:3.13-slim
image: python:3.13-slim
steps:
- name: Install Node.js (required by actions/checkout)
run: |
@@ -472,7 +421,7 @@ jobs:
helm:
runs-on: docker
container:
image: ${{vars.docker_prefix}}python:3.13-slim
image: python:3.13-slim
steps:
- name: Install system dependencies (nodejs for checkout, curl for Helm and kubeconform)
run: |
@@ -537,7 +486,7 @@ jobs:
# config (name/email) was set — both are required for any push operation.
runs-on: docker
container:
image: ${{vars.docker_prefix}}python:3.13-slim
image: python:3.13-slim
steps:
- name: Install system dependencies (nodejs for checkout, git for push validation)
run: |
@@ -610,10 +559,10 @@ jobs:
echo "=== Push access smoke-test passed ==="
status-check:
if: always()
needs: [lint, typecheck, security, quality, unit_tests, integration_tests, e2e_tests, coverage, build, docker, helm, push-validation]
needs: [lint, typecheck, security, quality, unit_tests, integration_tests, coverage, build, docker, helm, push-validation]
runs-on: docker
container:
image: ${{vars.docker_prefix}}python:3.13-slim
image: python:3.13-slim
steps:
- name: Check required job results
run: |
@@ -623,7 +572,6 @@ jobs:
echo "quality: ${{ needs.quality.result }}"
echo "unit_tests: ${{ needs.unit_tests.result }}"
echo "integration_tests: ${{ needs.integration_tests.result }}"
echo "e2e_tests: ${{ needs.e2e_tests.result }}"
echo "coverage: ${{ needs.coverage.result }}"
echo "build: ${{ needs.build.result }}"
echo "docker: ${{ needs.docker.result }}"
@@ -636,7 +584,6 @@ jobs:
[ "${{ needs.quality.result }}" != "success" ] || \
[ "${{ needs.unit_tests.result }}" != "success" ] || \
[ "${{ needs.integration_tests.result }}" != "success" ] || \
[ "${{ needs.e2e_tests.result }}" != "success" ] || \
[ "${{ needs.coverage.result }}" != "success" ] || \
[ "${{ needs.build.result }}" != "success" ] || \
[ "${{ needs.docker.result }}" != "success" ] || \
+96 -49
View File
@@ -3,11 +3,6 @@ name: CI
on:
push:
branches: [master, develop]
pull_request:
branches: [master, develop]
vars:
docker_prefix: "http://harbor.cleverthis.com/docker/"
env:
UV_VERSION: "0.8.0"
@@ -15,10 +10,67 @@ env:
NOX_DEFAULT_VENV_BACKEND: "uv"
jobs:
benchmark-regression:
if: forgejo.event_name != 'pull_request'
runs-on: docker-benchmark
container:
image: python:3.13-slim
steps:
- name: Install system dependencies (nodejs for checkout, git for merge tests)
run: |
apt-get update && apt-get install -y -qq nodejs git && rm -rf /var/lib/apt/lists/*
- name: Checkout full history
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Compute base commit
id: hash
run: |
git fetch origin "${{ forgejo.base_ref }}" --depth=200
BASE_SHA=$(git merge-base HEAD "origin/${{ forgejo.base_ref }}")
echo "ASV_BASE_SHA=${BASE_SHA}" >> $FORGEJO_OUTPUT
- name: Install dependencies
run: |
python -m pip install -U pip
python -m pip install asv virtualenv uv==${{ env.UV_VERSION }} nox
- name: Restore prior ASV benchmarks
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
ASV_S3_BUCKET: ${{ secrets.ASV_S3_BUCKET }}
run: |
python -m pip install awscli
mkdir -p build/asv/results
aws s3 sync "s3://${ASV_S3_BUCKET}/asv/results" build/asv/results --delete || true
- name: Run asv continuous via nox
env:
ASV_BASE_SHA: ${{ steps.hash.outputs.ASV_BASE_SHA }}
run: |
nox -s benchmark_regression
- name: Archive the results
run: |
tar cf /tmp/asv-results.tar build/asv/results build/asv/html
- name: Upload benchmark artifacts
if: always()
uses: actions/upload-artifact@v3
with:
name: asv-results-pr
path: /tmp/asv-results.tar
retention-days: 30
benchmark-publish:
if: forgejo.event_name == 'push'
runs-on: docker-benchmark
container: ${{vars.docker_prefix}}python:3.13-slim
container: python:3.13-slim
steps:
- name: Install system dependencies (nodejs for checkout, git for merge tests)
run: |
@@ -71,56 +123,51 @@ jobs:
name: asv-results-pr
path: /tmp/asv-results.tar
retention-days: 30
benchmark-regression:
# Runs benchmark regression on pull requests to detect performance regressions.
# This job is informational only — it is NOT listed in status-check's required
# needs, so a benchmark regression does not block PR merges.
if: forgejo.event_name == 'pull_request'
runs-on: docker-benchmark
container: ${{vars.docker_prefix}}python:3.13-slim
e2e_tests:
if: forgejo.event_name == 'push'
runs-on: docker
timeout-minutes: 45
container:
image: python:3.13-slim
steps:
- name: Install system dependencies (nodejs for checkout, git for ASV)
- name: Install system dependencies (nodejs for checkout, git for E2E tests)
run: |
apt-get update && apt-get install -y -qq nodejs git curl && rm -rf /var/lib/apt/lists/*
apt-get update && apt-get install -y -qq nodejs git && rm -rf /var/lib/apt/lists/*
- name: Checkout full history
uses: actions/checkout@v4
- uses: actions/checkout@v4
- name: Install uv and nox
run: |
pip install -q uv==${{ env.UV_VERSION }} nox
- name: Cache uv packages
uses: actions/cache@v3
with:
fetch-depth: 0
path: ~/.cache/uv
key: uv-${{ hashFiles('pyproject.toml') }}
restore-keys: |
uv-
- name: Install dependencies
run: |
python -m pip install -U pip
python -m pip install asv virtualenv uv==${{ env.UV_VERSION }} nox
- name: Sync prior benchmark results from S3
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
ASV_S3_BUCKET: ${{ secrets.ASV_S3_BUCKET }}
run: |
if [ -n "${AWS_ACCESS_KEY_ID}" ] && [ -n "${ASV_S3_BUCKET}" ]; then
python -m pip install awscli
mkdir -p build/asv/results
aws s3 sync "s3://${ASV_S3_BUCKET}/asv/results/" build/asv/results/ || echo "No existing results to sync"
else
echo "Skipping S3 sync - AWS credentials not configured"
fi
- name: Run benchmark regression via nox
env:
NOX_DEFAULT_VENV_BACKEND: uv
ASV_BASE_SHA: master
- name: Run E2E tests via nox
run: |
mkdir -p build
nox -s benchmark_regression 2>&1 | tee build/nox-benchmark-regression-output.log
nox -s e2e_tests 2>&1 | tee build/nox-e2e-tests-output.log
env:
NOX_DEFAULT_VENV_BACKEND: uv
# Run E2E suites in parallel via pabot. 4 workers keeps
# wall-clock time well under the 45-minute timeout while
# staying within the memory budget of the docker runner.
TEST_PROCESSES: "4"
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
- name: Upload benchmark regression log artifact
if: always()
- name: Upload E2E tests log artifact
if: failure()
uses: actions/upload-artifact@v3
with:
name: benchmark-regression-logs
path: build/nox-benchmark-regression-output.log
retention-days: 30
name: ci-logs-e2e-tests
path: |
build/nox-e2e-tests-output.log
build/reports/robot-e2e/
retention-days: 30
+1 -4
View File
@@ -7,9 +7,6 @@ on:
workflow_dispatch:
# Allow manual trigger for testing
vars:
docker_prefix: "http://harbor.cleverthis.com/docker/"
env:
UV_VERSION: "0.8.0"
PYTHON_VERSION: "3.13"
@@ -19,7 +16,7 @@ jobs:
full-quality-suite:
runs-on: docker
container:
image: ${{vars.docker_prefix}}python:3.13-slim
image: python:3.13-slim
steps:
- name: Install system dependencies (git for merge tests)
run: |
+2 -5
View File
@@ -5,9 +5,6 @@ on:
tags:
- "v*"
vars:
docker_prefix: "http://harbor.cleverthis.com/docker/"
env:
UV_VERSION: "0.8.0"
PYTHON_VERSION: "3.13"
@@ -17,7 +14,7 @@ jobs:
build-wheel:
runs-on: docker
container:
image: ${{vars.docker_prefix}}python:3.13-slim
image: python:3.13-slim
steps:
- name: Install Node.js (required by actions/checkout)
run: |
@@ -88,7 +85,7 @@ jobs:
create-release:
runs-on: docker
container:
image: ${{vars.docker_prefix}}python:3.13-slim
image: python:3.13-slim
needs: [build-wheel, build-docker]
steps:
- name: Install system dependencies
+24
View File
@@ -136,6 +136,18 @@ Entered when `review_type` is `ci_flag`. This is a lightweight review that only
Entered when `review_type` is `re_review`. The PR previously received `REQUEST_CHANGES` feedback and the author has since pushed new commits. Your job is to verify the feedback was adequately addressed AND conduct a full review of the current state.
0. **Post review-started notification.** POST `{forgejo_url}/api/v1/repos/{forgejo_owner}/{forgejo_repo}/issues/{pr_number}/comments` with body:
```
> Starting review for PR #{pr_number} (`re_review`)...
>
> Previous feedback was addressed — verifying changes and conducting full re-review. This may take a few minutes.
---
Automated by CleverAgents Bot
Supervisor: PR Review | Agent: pr-review-worker
```
Use authentication: `Authorization: token {forgejo_pat}`. Do NOT wait for a response before continuing — fire and move on.
1. **Read the PR.** GET `{forgejo_url}/api/v1/repos/{forgejo_owner}/{forgejo_repo}/pulls/{pr_number}` — read title, body, labels, milestone, and linked issues.
2. **Read all linked issues.** Parse `Closes #N`, `Fixes #N`, and `Refs #N` patterns from the PR body. For each referenced issue, GET `{forgejo_url}/api/v1/repos/{forgejo_owner}/{forgejo_repo}/issues/{issue_number}` — read title, body, labels, and acceptance criteria.
@@ -176,6 +188,18 @@ Entered when `review_type` is `re_review`. The PR previously received `REQUEST_C
Entered when `review_type` is `first_review`. The PR has no active review feedback — this is a fresh evaluation from scratch.
0. **Post review-started notification.** POST `{forgejo_url}/api/v1/repos/{forgejo_owner}/{forgejo_repo}/issues/{pr_number}/comments` with body:
```
> Starting review for PR #{pr_number} (`first_review`)...
>
> Conducting fresh evaluation against the full review checklist. This may take a few minutes.
---
Automated by CleverAgents Bot
Supervisor: PR Review | Agent: pr-review-worker
```
Use authentication: `Authorization: token {forgejo_pat}`. Do NOT wait for a response before continuing — fire and move on.
1. **Read the PR.** Same as Re-Review Mode step 1.
2. **Read all linked issues.** Same as Re-Review Mode step 2.
+44 -20
View File
@@ -121,16 +121,28 @@ This is where actual implementation happens. Choose the appropriate procedure ba
1. **Read the issue.** GET `{forgejo_url}/api/v1/repos/{forgejo_owner}/{forgejo_repo}/issues/{work_number}` — read title, body, labels, milestone, and metadata section (branch name, commit message format). Paginate all comments to understand full context and any subtask structure.
2. **Determine branch name.** Extract the branch name from the issue's Metadata section if present. If absent, derive one: `feature/issue-{work_number}-{kebab-slug-of-title}`.
2. **Post work-started notification.** POST `{forgejo_url}/api/v1/repos/{forgejo_owner}/{forgejo_repo}/issues/{work_number}/comments` with body:
```
> Starting implementation for issue #{work_number}: {issue_title} (`issue_impl`)...
>
> Reading requirements and setting up development environment. This may take several minutes.
3. **Create isolated clone.** Call `git-isolator-util` with `create_branch: true`, `base_branch: master`, and the determined `branch_name` (see Subagents section for prompt template).
---
Automated by CleverAgents Bot
Supervisor: Implementation | Agent: task-implementor
```
Use authentication: `Authorization: token {forgejo_pat}`. Replace `{issue_title}` with the title read in step 1. Do NOT wait for a response before continuing — fire and move on.
4. **Implement the code.** Load the `cleverthis-guidelines` skill for CONTRIBUTING.md rules and follow them strictly. Key rules:
3. **Determine branch name.** Extract the branch name from the issue's Metadata section if present. If absent, derive one: `feature/issue-{work_number}-{kebab-slug-of-title}`.
4. **Create isolated clone.** Call `git-isolator-util` with `create_branch: true`, `base_branch: master`, and the determined `branch_name` (see Subagents section for prompt template).
5. **Implement the code.** Load the `cleverthis-guidelines` skill for CONTRIBUTING.md rules and follow them strictly. Key rules:
- Source in `src/cleveragents/`, Behave unit tests in `features/`, Robot Framework integration/e2e tests in `robot/`
- Full static typing throughout — no `# type: ignore`
- All commands via `nox` — never invoke `pip`, `pytest`, `behave`, or `robot` directly
5. **Run quality gates in order:**
6. **Run quality gates in order:**
```bash
nox -e lint
nox -e typecheck
@@ -140,11 +152,11 @@ This is where actual implementation happens. Choose the appropriate procedure ba
nox -e coverage_report
```
6. **Fix any failures.** If a gate fails, fix the code and re-run the failing gate (and any that follow it). Repeat until all gates pass. Do not move forward with failing gates.
7. **Fix any failures.** If a gate fails, fix the code and re-run the failing gate (and any that follow it). Repeat until all gates pass. Do not move forward with failing gates.
7. **Commit.** Call `git-commit-util` with `commit_and_push` operation. The first line of the commit message must match the issue's Metadata section exactly (see Subagents section).
8. **Commit.** Call `git-commit-util` with `commit_and_push` operation. The first line of the commit message must match the issue's Metadata section exactly (see Subagents section).
8. **Create PR.** POST `{forgejo_url}/api/v1/repos/{forgejo_owner}/{forgejo_repo}/pulls` with:
9. **Create PR.** POST `{forgejo_url}/api/v1/repos/{forgejo_owner}/{forgejo_repo}/pulls` with:
- `title`: taken from the issue title or the commit message first line
- `body`: description of changes + `Closes #{work_number}` + dependency link (`This PR blocks issue #{work_number}`)
- `base`: `master`
@@ -152,35 +164,47 @@ This is where actual implementation happens. Choose the appropriate procedure ba
- `milestone` (if set on the issue): same milestone ID
- Use PAT authentication: `Authorization: token {forgejo_pat}`
9. **Post attempt comment** on the issue (see "Attempt Comments" section below).
10. **Post attempt comment** on the issue (see "Attempt Comments" section below).
10. **Clean up.** `rm -rf {repo_dir}`
11. **Clean up.** `rm -rf {repo_dir}`
11. **Exit.**
12. **Exit.**
#### Procedure: `pr_fix` (PR Fix)
1. **Read the PR.** GET `{forgejo_url}/api/v1/repos/{forgejo_owner}/{forgejo_repo}/pulls/{work_number}` — read description, head branch, head SHA, and CI state. Set `branch_name` to the PR's head branch.
2. **Read all reviews.** GET `{forgejo_url}/api/v1/repos/{forgejo_owner}/{forgejo_repo}/pulls/{work_number}/reviews?limit=50&page=N` — paginate fully. For any review in `REQUEST_CHANGES` state, GET its comments to understand the specific feedback.
2. **Post work-started notification.** POST `{forgejo_url}/api/v1/repos/{forgejo_owner}/{forgejo_repo}/issues/{work_number}/comments` with body:
```
> Starting fix for PR #{work_number}: {pr_title} (`pr_fix`)...
>
> Addressing review feedback and CI failures. This may take several minutes.
3. **Read all PR comments.** GET `{forgejo_url}/api/v1/repos/{forgejo_owner}/{forgejo_repo}/issues/{work_number}/comments?limit=50&page=N` — paginate fully.
---
Automated by CleverAgents Bot
Supervisor: Implementation | Agent: task-implementor
```
Use authentication: `Authorization: token {forgejo_pat}`. Replace `{pr_title}` with the title read in step 1. Do NOT wait for a response before continuing — fire and move on.
4. **Fetch CI failure details.** GET `{forgejo_url}/api/v1/repos/{forgejo_owner}/{forgejo_repo}/commits/{head_sha}/statuses?limit=50&page=N` — paginate fully. For each failing status that has a `target_url`, webfetch that URL to retrieve the failure logs.
3. **Read all reviews.** GET `{forgejo_url}/api/v1/repos/{forgejo_owner}/{forgejo_repo}/pulls/{work_number}/reviews?limit=50&page=N` — paginate fully. For any review in `REQUEST_CHANGES` state, GET its comments to understand the specific feedback.
5. **Create isolated clone.** Call `git-isolator-util` with `create_branch: false` and `branch: {branch_name}` (the PR's head branch, resolved in step 1).
4. **Read all PR comments.** GET `{forgejo_url}/api/v1/repos/{forgejo_owner}/{forgejo_repo}/issues/{work_number}/comments?limit=50&page=N` — paginate fully.
6. **Fix the issues.** Address all CI failures and all unresolved reviewer feedback. Never partially address reviewer comments — every `REQUEST_CHANGES` concern must be fully resolved.
5. **Fetch CI failure details.** GET `{forgejo_url}/api/v1/repos/{forgejo_owner}/{forgejo_repo}/commits/{head_sha}/statuses?limit=50&page=N` — paginate fully. For each failing status that has a `target_url`, webfetch that URL to retrieve the failure logs.
7. **Run quality gates locally** (same 6 gates as above). All must pass before pushing. Fix and re-run as many times as needed.
6. **Create isolated clone.** Call `git-isolator-util` with `create_branch: false` and `branch: {branch_name}` (the PR's head branch, resolved in step 1).
8. **Commit and push.** Call `git-commit-util` with `force_push_with_lease` operation (see Subagents section).
7. **Fix the issues.** Address all CI failures and all unresolved reviewer feedback. Never partially address reviewer comments — every `REQUEST_CHANGES` concern must be fully resolved.
9. **Post attempt comment** on the PR (see "Attempt Comments" section below).
8. **Run quality gates locally** (same 6 gates as above). All must pass before pushing. Fix and re-run as many times as needed.
10. **Clean up.** `rm -rf {repo_dir}`
9. **Commit and push.** Call `git-commit-util` with `force_push_with_lease` operation (see Subagents section).
11. **Exit.**
10. **Post attempt comment** on the PR (see "Attempt Comments" section below).
11. **Clean up.** `rm -rf {repo_dir}`
12. **Exit.**
### Attempt Comments
+73
View File
@@ -5,6 +5,14 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]
- **`task-implementor` posts work-started notification comments** (#11031): Both
the `issue_impl` and `pr_fix` procedures now post an informational "work
started" comment to the Forgejo issue/PR before beginning implementation.
The comment includes the issue/PR title, procedure type, and expected
duration. Posted asynchronously ("fire and move on") so it does not block
the workflow. Step numbering in both procedures has been re-numbered to
accommodate the new step.
- **`agents session tell` invokes real LLM orchestrator actor** (#5784): Replaced the
M3 echo-stub with real actor invocation via `SessionWorkflow`, routing through
`LangChainSessionCaller``ToolCallingRuntime.run_tool_loop()`. The user prompt
@@ -16,6 +24,30 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
LLM streaming output. A `SessionActorNotConfiguredError` is raised with exit
code 1 when no actor is configured.
### Added
- **ContextStrategy Protocol and Plugin Registration System** (#8616): Implemented the pluggable context assembly strategy protocol (`ContextStrategy`) with proper type-safe method signatures (`can_handle(request, backends) -> confidence`, `assemble(request, backends, budget, plan_context) -> fragments`). Added `StrategyRegistry` -- the central thread-safe registry for registration, lookup, discovery (via entry points), and per-strategy configuration (`timeout_seconds`, `max_fragments`, `max_workers`, `circuit_breaker_threshold`, `resource_types`, `extra`). Six built-in strategies implemented per spec: `simple-keyword` (0.3), `semantic-embedding` (0.6), `breadth-depth-navigator` (0.85), `arce` (0.95 -- multi-modal text+vector+graph), `temporal-archaeology` (0.5 -- historical discovery), and `plan-decision-context` (0.7 -- parent plan decision retrieval). Full BDD test coverage validating protocol conformance, registry CRUD operations, entry-point discovery, error handling, thread safety, and boundary validation.
- **ContextStrategy Protocol and Plugin Registration System** (#10590): Implemented the
``ContextStrategy`` protocol for pluggable context assembly strategies within the ACMS.
Includes six built-in strategy implementations: ``SimpleKeywordStrategy`` (keyword matching,
quality 0.3), ``SemanticEmbeddingStrategy`` (word-overlap similarity search, quality 0.6),
``BreadthDepthNavigatorStrategy`` (UKO hierarchy traversal with depth/breadth projection,
quality 0.85), ``ARCEStrategy`` (multi-modal pipeline combining text/vector/graph backends,
quality 0.95), ``TemporalArchaeologyStrategy`` (historical pattern discovery from cold-tier data,
quality 0.5), and ``PlanDecisionContextStrategy`` (ancestor plan decision retrieval, quality 0.7).
The ``StrategyRegistry`` provides thread-safe registration, enable/disable configuration,
per-strategy timeout/fragment limits, circuit-breaker tracking, validation warnings, and plugin
discovery from ``"module:ClassName"`` strings with a module-prefix allowlist for security.
Seventy-seven (77) Behave scenarios cover strategy selection by confidence scoring, backend
capability matching, duplicate registration rejection, stale enabled-list detection,
MappingProxyType coercion validators, thread-safety under concurrent access, boundary value
validation via Pydantic model constraints, and per-strategy config updates.
### Added
- **ContextStrategy protocol and StrategyRegistry plugin registration system** (#8616): Implemented the pluggable context assembly strategy protocol (`ContextStrategy`) with proper type-safe method signatures (`can_handle(request, backends) -> confidence`, `assemble(request, backends, budget, plan_context) -> fragments`). Added `StrategyRegistry` -- the central thread-safe registry for registration, lookup, discovery (via entry points), and per-strategy configuration (`timeout_seconds`, `max_fragments`, `max_workers`, `circuit_breaker_threshold`, `resource_types`, `extra`). Six built-in strategies implemented per spec: `simple-keyword` (0.3), `semantic-embedding` (0.6), `breadth-depth-navigator` (0.85), `arce` (0.95 -- multi-modal text+vector+graph), `temporal-archaeology` (0.5 -- historical discovery), and `plan-decision-context` (0.7 -- parent plan decision retrieval). Full BDD test coverage validating protocol conformance, registry CRUD operations, entry-point discovery, error handling, thread safety, and boundary validation.
- Fixed `ReactiveEventBus.emit()` exception handler to log the full exception
message (`str(exc)`) and enable traceback forwarding (`exc_info=True`).
Previously the handler logged only the exception type name (e.g.
@@ -26,9 +58,40 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Added
- **`pr-review-worker` review-started notification** (#11028): The `first_review`
and `re_review` modes now post a "review started" notification comment to the
PR at the beginning of the review, giving PR authors immediate visibility
that a review is in progress. Posted asynchronously so it does not block
the workflow.
- **Plan Rollback Command** (#8557): Implemented `agents plan rollback <plan-id> [<checkpoint-id>]` for checkpoint-based plan state restoration in Epic #8493. The command restores a plan's sandbox to the state captured at a given checkpoint, discarding all decisions made after that checkpoint. The checkpoint can be specified as an optional positional second argument or via the `--to-checkpoint` named option. Supports `--yes/-y` flag to skip confirmation prompts and `--format/-f` for output format selection (rich/plain/json/yaml). Included with comprehensive BDD test coverage (>= 97%) and spec-aligned output formatting showing rollback summary, changes reverted, impact analysis, and post-rollback state panels.
### Fixed
- **Guard cleanup_stale against execute/processing and execute/complete plans** (#11121):
``_create_sandbox_for_plan()`` in ``src/cleveragents/cli/commands/plan.py`` now
skips ``GitWorktreeSandbox.cleanup_stale()`` when the plan is in
``execute/processing`` (execution in progress) or ``execute/complete`` (execution
finished, awaiting apply) state. Previously, re-invoking ``agents plan execute``
on a completed plan would silently destroy the ``cleveragents/plan-<id>`` git
worktree branch, causing ``plan apply`` to merge zero artifacts. The guard
preserves the branch per spec (§sandbox.cleanup defaults to ``on_apply``).
- **Global CLI options ``--data-dir``, ``--config-path``, and ``-v`` now work correctly**
(#6785): These spec-required flags were absent from ``main_callback()`` in
``src/cleveragents/cli/main.py``, causing any invocation with these flags to crash
with ``NoSuchOption: No such option``. All three options are now implemented per
ADR-021 §Global CLI Flags and ADR-024 §Resolution Chain. ``--data-dir`` sets
``CLEVERAGENTS_DATA_DIR`` before any ``Settings``-reading code runs, ``--config-path``
sets ``CLEVERAGENTS_CONFIG_PATH`` for ``ConfigService`` to pick up, and ``-v``
(repeatable count) maps to the appropriate ``structlog`` log level.
- **Add regression test for ActorRegistry.add() nested provider/model extraction** (#4321):
Added BDD regression test confirming that provider and model are correctly
extracted from nested `actors.<name>.config` blocks when `type` is only at
the nested level and `name` uses the `local/` namespace prefix. This
scenario was previously fixed in #4300; the test ensures the fix remains
in place and prevents future regressions.
- **Actor configuration validation incorrectly requires top-level provider field** (#4300):
Actor configuration in V3 is now obtained from the nested configuration
parameter, according to the specification.
@@ -270,6 +333,16 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Fixed
- **`invariant_enforced` decisions not propagated to child plans on subplan spawn** (#9131):
Fixed `SubplanService.spawn()` to propagate all `invariant_enforced` decisions from the
parent plan's decision tree to each child plan's decision tree. Previously, child plans
started Strategize with a completely empty invariant set, violating the spec requirement:
"recorded as `invariant_enforced` decisions that propagate to child plans." The fix adds
a `_propagate_invariant_decisions()` helper that re-records each parent
`invariant_enforced` decision on the child plan, including `non_overridable` global
invariants. BDD regression coverage added in
`features/tdd_invariant_propagation_subplan.feature`.
- **fix(repositories): derive PlanResult.success from result_success column instead of error_message** (#7501):
Fixed a critical bug in `PlanRepository._to_domain` where `PlanResult.success` was incorrectly
derived from `error_message is None`. Because `error_message` is shared between the build phase
+8 -2
View File
@@ -5,6 +5,7 @@
* HAL 9000 <hal9000@cleverthis.com>
* Hamza Khyari <hamza.khyari@cleverthis.com>
* Jeffrey Phillips Freeman <jeffrey.freeman@syncleus.com>
* Jeffrey Phillips Freeman <the@jeffreyfreeman.me>
* Luis Mendes <luis.p.mendes@gmail.com>
* Rui Hu <rui.hu@cleverthis.com>
@@ -13,16 +14,20 @@
Below are some of the specific details of various contributions.
* Jeffrey Phillips Freeman has acted as Lead Developer, daily contributor, and Project Owner.
* Jeffrey Phillips Freeman has contributed an implementation for the invariant propagation fix (PR #10881 / issue #9131): added `_propagate_invariant_decisions()` to `SubplanService` to propagate all `invariant_enforced` decisions from parent plans to child plan decision trees during subplan spawn, satisfying the specification requirement for invariant propagation across hierarchical plan execution.
* Brent E. Edwards has contributed quality assurance, test coverage, and CI pipeline improvements.
* HAL 9000 has contributed automated implementation, bug fixes, and feature development as part of the CleverAgents automation pool.
* HAL 9000 has contributed concurrency safety improvements, including thread-safe context tier management (issue #7547) for parallel plan execution.
* HAL 9000 has contributed the plan concurrency race-condition fix (#7989): wired `LockService` into the plan lifecycle, guarding `execute_plan()` and `apply_plan()` with plan-level advisory locks and unique per-invocation owner identities to prevent silent concurrent state corruption.
<<<<<<< HEAD
* HAL 9000 has contributed the bug-hunt-pool-supervisor non-blocking tracking fix (#7875 / PR #7957): updated step 5 to be best-effort and added rule 9 to prevent the automation-tracking-manager call from blocking the main supervisor loop.
* Jeffrey Phillips Freeman has contributed the complete AUTO-BUG-POOL to AUTO-BUG-SUP tracking prefix fix across agent-system-specification.md, automation-tracking.md documentation and agent-system-specification.md spec document, replaced with correct `AUTO-BUG-SUP` prefix used by the bug-hunt-pool-supervisor agent (#7875).
* HAL 9000 has contributed the plugin entry point security hardening fix (#7476): enforced entry point allowlist validation before importing plugin modules to prevent malicious plugin loading.
* HAL 9000 has contributed the benchmark workflow separation (#9040): moved the benchmark-regression job out of the default PR workflow into a dedicated scheduled workflow, reducing median PR CI turnaround time from 99-132 minutes to under 30 minutes.
* HAL 9000 has contributed the plan tree JSON/YAML command envelope fix (#9163): wrapped `agents plan tree --format json/yaml` output in the spec-required command envelope structure, added summary statistics, decision_ids mapping, child_plans list, and accurate timing measurement.
* HAL 9000 has contributed the agent-evolution-pool-supervisor PR metadata assignment (#7888): the supervisor now automatically looks up the Type/Automation label and earliest open milestone before dispatching improvement PR creation workers, ensuring all generated improvement PRs have correct Type labels and milestone assignments.
* HAL 9000 has contributed the decision recording hook for the Strategize phase (issue #8522): captures every decision point with question, chosen option, alternatives, confidence, rationale, and full context snapshot for replay and correction.
* HAL 9000 has contributed the ContextStrategy protocol and StrategyRegistry plugin registration system (PR #10590 / issue #8616): implemented the pluggable context assembly strategy protocol with proper type-safe method signatures, created the central thread-safe StrategyRegistry supporting registration, lookup, entry-point discovery, and per-strategy configuration (timeout, fragments limits, workers, circuit breaker threshold). Six built-in strategies implemented and documented: simple-keyword, semantic-embedding, breadth-depth-navigator, arce, temporal-archaeology, and plan-decision-context. Full BDD test coverage including thread safety, boundary validation, and error handling tests. (Part of Epic #8505)
* This project was made possible thanks to considerable donation of time, money, and resources by CleverThis, Inc.
* HAL 9000 has contributed automated bug fixes, CLI output formatting improvements, and ongoing maintenance as part of the CleverAgents automation system.
* HAL 9000 has contributed the file edit encoding parameter fix (PR #8258 / issue #7559).
@@ -32,7 +37,7 @@ Below are some of the specific details of various contributions.
* HAL 9000 has contributed the mandatory PR compliance checklist to `implementation-supervisor.md` (#9824): added an 8-item checklist to the worker prompt body with concrete items covering CHANGELOG.md, CONTRIBUTORS.md, commit footer, CI verification, BDD tests, Epic reference, labels, and milestone assignment to eliminate systemic PR merge blockers.
* HAL 9000 has contributed the PlanResult.success derivation fix (PR #8214 / issue #7501): replaced the incorrect `error_message is None` heuristic with a dedicated `result_success` column in the plans table, ensuring plans with historical build errors are not incorrectly marked as failed after a successful apply.
* HAL 9000 has contributed the mandatory PR compliance checklist to `implementation-pool-supervisor.md` (#9824): created a new agent definition with an embedded 8-item checklist ensuring workers always update CHANGELOG.md, CONTRIBUTORS.md, include commit footers (`ISSUES CLOSED: #N`), verify CI passes, add BDD tests, reference the parent Epic, apply labels via forgejo-label-manager, and assign milestones before creating PRs. Includes concrete examples for each subsection and compliance verification pseudocode.
* HAL 9000 has contributed comprehensive milestone documentation for v3.6.0 (Advanced Concepts & Deferred Features) and v3.7.0 (TUI Implementation) (PR #9903): split into sub-documents covering context strategies, LLM backends, resource types, A2A rename, container tool execution, scope chain resolution, cost/safety budgets, E2E workflow tests, code review examples, plugin architecture, TUI layout, persona system, reference/command input, session management, configuration, and TuiMaterializer integration.
* HAL 9000 has contributed comprehensive milestone documentation for v3.6.0 (Advanced Concepts & Deferred Features) and v3.7.0 (TUI Implementation) (PR #9903): split into sub-documents covering context strategies, LLM backends, resource points, A2A rename, container tool execution, scope chain resolution, cost/safety budgets, E2E workflow tests, code review examples, plugin architecture, TUI layout, persona system, reference/command input, session management, configuration, and TuiMaterializer integration.
* HAL 9000 has contributed the LLMTraceRepository data-integrity fix (PR #8185 / issue #7505): replaced the unconditional `session.commit()` in `LLMTraceRepository.save()` with a dual-path implementation that respects the UnitOfWork pattern — flushing only when an external session is provided, and flushing + committing + closing when operating standalone. This eliminates premature transaction commits, loss of rollback capability, and a docstring/implementation mismatch.
* HAL 9000 has contributed the ACMS Index Data Model and File Traversal Engine (PR #9664 / issue #9579): foundational data structures for indexed context entries with hot/warm/cold/archive storage tier classification, tag system, and a timeout-safe chunked file traversal engine for large projects with 10,000+ files.
* HAL 9000 has contributed the error-suppression removal fix (PR #9247 / issue #9060): removed both `try...except Exception:` blocks in `register_registry_agents()` that silently suppressed errors from `actor_registry.list_actors()` and the route bridge refresh, enabling exceptions to propagate per CONTRIBUTING.md fail-fast policy. Added three Behave scenarios verifying RuntimeError, AttributeError, and TypeError propagation.
@@ -41,3 +46,4 @@ Below are some of the specific details of various contributions.
* HAL 9000 has contributed database resource types (PostgreSQL, SQLite) with transaction-based sandbox strategy: implemented ``DatabaseResourceHandler`` providing full CRUD operations (`read`, `write`, `delete`, `list_children`) and connection validation with automatic credential masking for PostgreSQL and SQLite backends. Includes ``TransactionSandbox`` infrastructure wired into ``SandboxFactory``, BDD test coverage in ``features/database_resources.feature``, and Robot Framework integration tests in ``robot/database_resources.robot`` (PR #10591 / issue #8608, Epic #8568).
* HAL 9000 has contributed the agents plan rollback command (PR #8674 / issue #8557): implemented checkpoint-based plan state restoration with the `agents plan rollback <plan-id> [<checkpoint-id>]` CLI command as part of Epic #8493, enabling plans to be restored to previous checkpoints, discarding post-checkpoint decisions, and resuming execution from the rolled-back state. Supported by `--yes/-y`, `--to-checkpoint`, and `--format/-f` flags. Includes comprehensive BDD test coverage (>= 97%) for rollback, decision discarding, and plan resume functionality.
* HAL 9000 has contributed the PyYAML security upgrade (PR #11012 / issue #9055): added `pyyaml>=6.0.3` dependency constraint to address known YAML parsing vulnerabilities.
- **ContextStrategy protocol plugin registration**: Implemented the ContextStrategy protocol for the ACMS with six built-in strategies (SimpleKeyword, SemanticEmbedding, BreadthDepthNavigator, ARCE, TemporalArchaeology, PlanDecisionContext), a thread-safe StrategyRegistry supporting enable/disable configuration, per-strategy config updates, plugin discovery from module paths with allowlist security, validation warnings, and 77 Behave test scenarios covering all features.
@@ -21,6 +21,13 @@ Feature: ActorRegistry.add() accepts spec-compliant actor YAML formats
Then the actor should be registered with provider "anthropic" and model "claude-3"
And the registered actor should exist in the actor service
@tdd_issue @tdd_issue_4321 @tdd_issue_4300
Scenario: registry.add() with nested-only type extracts provider/model from nested config
When I add a YAML with type only in nested actors map and provider/model in nested config
Then the actor should be registered with provider "custom" and model "fpga-model"
And the registered actor name should be "local/fpga-strategist"
And the registered actor should exist in the actor service
# ── actors: map with unsafe flag ───────────────────────────────────
Scenario: registry.add() preserves unsafe flag from nested spec-compliant config
+128
View File
@@ -0,0 +1,128 @@
Feature: CLI global options --data-dir, --config-path, and -v
Background:
Given global options test env is clean
# -----------------------------------------------------------------------
# --data-dir option
# -----------------------------------------------------------------------
@tdd_issue @tdd_issue_6785
Scenario: --data-dir option is accepted with an existing directory
Given a temp data dir is prepared
When I run the global options CLI with data-dir flag and "version"
Then the global options CLI should succeed
Scenario: --data-dir option overrides CLEVERAGENTS_DATA_DIR for the invocation
Given a temp data dir is prepared
When I run the global options CLI with data-dir flag and "version"
Then the CLEVERAGENTS_DATA_DIR env var should match the temp data dir
Scenario: --data-dir with a path that is an existing file fails with a clear error
Given a temp file path is prepared
When I run the global options CLI with data-dir as temp file and "version"
Then the global options CLI should fail
And the global options output should contain "--data-dir"
Scenario: --data-dir stores value in ctx.obj for subcommands
Given a temp data dir is prepared
When I run the global options CLI with data-dir flag and "version"
Then the CLEVERAGENTS_DATA_DIR env var should match the temp data dir
# -----------------------------------------------------------------------
# --config-path option
# -----------------------------------------------------------------------
@tdd_issue @tdd_issue_6785
Scenario: --config-path option is accepted with an existing file
Given a temp config file is prepared
When I run the global options CLI with config-path flag and "version"
Then the global options CLI should succeed
Scenario: --config-path overrides CLEVERAGENTS_CONFIG_PATH for the invocation
Given a temp config file is prepared
When I run the global options CLI with config-path flag and "version"
Then the CLEVERAGENTS_CONFIG_PATH env var should match the temp config file
Scenario: --config-path with a non-existent file fails with a clear error
When I run the global options CLI with invalid config-path and "version"
Then the global options CLI should fail
And the global options output should contain "--config-path"
Scenario: --config-path stores value in ctx.obj for subcommands
Given a temp config file is prepared
When I run the global options CLI with config-path flag and "version"
Then the CLEVERAGENTS_CONFIG_PATH env var should match the temp config file
# -----------------------------------------------------------------------
# Combining --data-dir and --config-path
# -----------------------------------------------------------------------
Scenario: --data-dir and --config-path can be combined
Given a temp data dir is prepared
And a temp config file is prepared
When I run the global options CLI with both path flags and "version"
Then the global options CLI should succeed
And the CLEVERAGENTS_DATA_DIR env var should match the temp data dir
And the CLEVERAGENTS_CONFIG_PATH env var should match the temp config file
# -----------------------------------------------------------------------
# -v verbosity option
# -----------------------------------------------------------------------
Scenario: No -v flag results in CRITICAL (silent) log level
When I call main_callback with verbosity 0
Then the global options log level should be "CRITICAL"
Scenario: Single -v flag results in ERROR log level
When I call main_callback with verbosity 1
Then the global options log level should be "ERROR"
Scenario: -vv results in WARNING log level
When I call main_callback with verbosity 2
Then the global options log level should be "WARNING"
Scenario: -vvv results in INFO log level
When I call main_callback with verbosity 3
Then the global options log level should be "INFO"
Scenario: -vvvv results in DEBUG log level
When I call main_callback with verbosity 4
Then the global options log level should be "DEBUG"
Scenario: -vvvvv or more results in DEBUG log level
When I call main_callback with verbosity 5
Then the global options log level should be "DEBUG"
@tdd_issue @tdd_issue_6785
Scenario: -v flag is accepted by the CLI without crashing
When I run the global options CLI with args "-v version"
Then the global options CLI should succeed
Scenario: -vvv flags are accepted by the CLI without crashing
When I run the global options CLI with args "-v -v -v version"
Then the global options CLI should succeed
# -----------------------------------------------------------------------
# --help shows all three options
# -----------------------------------------------------------------------
Scenario: --help output includes --data-dir option
When I run the global options CLI with args "--help"
Then the global options output should contain "--data-dir"
Scenario: --help output includes --config-path option
When I run the global options CLI with args "--help"
Then the global options output should contain "--config-path"
Scenario: -v flag appears in --help output
When I run the global options CLI with args "--help"
Then the global options output should contain "-v"
# -----------------------------------------------------------------------
# -v verbosity stored in ctx.obj
# -----------------------------------------------------------------------
Scenario: verbose count is stored in ctx.obj
When I call main_callback with verbosity 3
Then the global options ctx obj "verbose" should be 3
@@ -69,9 +69,20 @@ Feature: Plan Generation LangGraph Coverage
Scenario: Should retry returns end when max retries reached
Given I have a langgraph PlanGenerationGraph instance with max_retries 3
When I check langgraph should_retry with FAIL validation and retry_count 3
When I check langgraph should_retry with FAIL validation and retry_count 4
Then the langgraph retry decision should be "end"
Scenario: Should retry still retries when retry_count equals max_retries
Given I have a langgraph PlanGenerationGraph instance with max_retries 3
When I check langgraph should_retry with FAIL validation and retry_count 3
Then the langgraph retry decision should be "retry"
Scenario: Validate node increments retry_count in returned state
Given I have a langgraph PlanGenerationGraph instance
When I execute the langgraph validate node with no changes
Then the langgraph validation status should be "FAIL"
And the langgraph validate result should include retry_count incremented from 0 to 1
Scenario: Validate node fails when no changes provided
Given I have a langgraph PlanGenerationGraph instance
When I execute the langgraph validate node with no changes
@@ -78,12 +78,13 @@ Feature: Plan Generation Graph Uncovered Lines Coverage
And the langgraph async result should have generated_changes
And the langgraph async result should have validation_result
# Testing line 403: Retry count increment in should_retry
Scenario: Should retry increments retry count correctly
# _should_retry is a read-only conditional-edge function; retry_count is
# incremented inside _validate (a node) so LangGraph persists the new value.
Scenario: Should retry does not mutate state and returns retry when retries remain
Given I have a langgraph PlanGenerationGraph instance with max_retries 3
And I have a langgraph state with retry_count 1
When I check uncovered langgraph should_retry with FAIL validation and retry_count 1
Then the uncovered langgraph state retry_count should be incremented to 2
Then the uncovered langgraph state retry_count should not be mutated and remain 1
And the langgraph retry decision should be "retry"
# Testing full workflow with retry logic
@@ -143,6 +143,23 @@ def step_add_actors_separate(context: Context) -> None:
context.spec_result = context.spec_registry.add(yaml_text)
@when(
"I add a YAML with type only in nested actors map and provider/model in nested config"
)
def step_add_nested_type_only(context: Context) -> None:
yaml_text = (
"name: local/fpga-strategist\n"
"description: An FPGA strategist agent\n"
"actors:\n"
" fpga-strategist:\n"
" type: llm\n"
" config:\n"
" provider: custom\n"
" model: fpga-model\n"
)
context.spec_result = context.spec_registry.add(yaml_text)
@when("I add a spec-compliant YAML with actors map and unsafe flag")
def step_add_actors_unsafe(context: Context) -> None:
yaml_text = (
+346
View File
@@ -0,0 +1,346 @@
"""Step definitions for CLI global options: --data-dir, --config-path, -v.
Tests for issue #6785 — spec-required global options that were missing from
main_callback() and caused fatal "No such option" crashes.
All step patterns use the prefix "global options" to avoid any ambiguity
with the many generic CLI step definitions in other step files.
"""
from __future__ import annotations
import os
import re
import shutil
import tempfile
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch
from behave import given, then, when
from behave.runner import Context
from typer.testing import CliRunner
from cleveragents.cli.formatting import OutputFormat
from cleveragents.cli.main import app
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
# Regex to strip ANSI escape sequences (CSI sequences and OSC sequences).
# Used to make output assertions environment-agnostic — Rich may emit ANSI
# colour/style codes when the CI environment does not set TERM=dumb.
_ANSI_ESCAPE_RE = re.compile(
r"\x1b(?:"
r"\[[0-9;]*[a-zA-Z]" # CSI sequences (SGR, cursor, etc.)
r"|"
r"\][^\x07]*\x07" # OSC sequences (hyperlink, title, etc.)
r")"
)
def _strip_ansi(text: str) -> str:
"""Remove ANSI escape sequences from *text*."""
return _ANSI_ESCAPE_RE.sub("", text)
def _invoke(args: list[str]) -> Any:
"""Invoke the Typer app with the given args and return the result."""
runner = CliRunner()
return runner.invoke(app, args, catch_exceptions=True)
def _combined_output(result: Any) -> str:
"""Return combined output from a CliRunner result, with ANSI codes stripped.
In non-TTY environments (CI), Typer/Rich may split output between
stdout and stderr. Check both explicitly so that ``--help`` output
assertions work regardless of the terminal detection.
"""
stdout = _strip_ansi((getattr(result, "stdout", "") or "").strip())
stderr = _strip_ansi((getattr(result, "stderr", "") or "").strip())
combined = (
(stdout + "\n" + stderr).strip() if (stdout and stderr) else (stdout or stderr)
)
return combined or _strip_ansi((result.output or "").strip())
# ---------------------------------------------------------------------------
# Given steps
# ---------------------------------------------------------------------------
@given("global options test env is clean")
def step_global_opts_env_clean(context: Context) -> None:
"""Ensure test-specific env vars are not polluted from previous tests.
Registers a cleanup handler that restores the original env var values after
the scenario ends. The global ``after_scenario`` hook in
``features/environment.py`` runs all ``context._cleanup_handlers``.
"""
orig_data_dir = os.environ.get("CLEVERAGENTS_DATA_DIR")
orig_config_path = os.environ.get("CLEVERAGENTS_CONFIG_PATH")
# Clean up any leftover from previous scenarios
for var in ("CLEVERAGENTS_DATA_DIR", "CLEVERAGENTS_CONFIG_PATH"):
os.environ.pop(var, None)
# Register restoration handler so that env vars set by main_callback()
# during CLI invocation do not pollute subsequent scenarios.
def _restore() -> None:
for key, val in [
("CLEVERAGENTS_DATA_DIR", orig_data_dir),
("CLEVERAGENTS_CONFIG_PATH", orig_config_path),
]:
if val is not None:
os.environ[key] = val
else:
os.environ.pop(key, None)
if not hasattr(context, "_cleanup_handlers"):
context._cleanup_handlers = []
context._cleanup_handlers.append(_restore)
@given("a temp data dir is prepared")
def step_temp_data_dir_prepared(context: Context) -> None:
"""Create a real temporary directory for --data-dir testing.
Registers a cleanup handler so the directory is removed after the scenario.
"""
tmp_dir = tempfile.mkdtemp(prefix="ca_test_data_dir_")
context.temp_data_dir = tmp_dir
if not hasattr(context, "_cleanup_handlers"):
context._cleanup_handlers = []
context._cleanup_handlers.append(lambda: shutil.rmtree(tmp_dir, ignore_errors=True))
@given("a temp config file is prepared")
def step_temp_config_file_prepared(context: Context) -> None:
"""Create a real temporary TOML config file for --config-path testing.
Registers a cleanup handler so the file is removed after the scenario.
"""
with tempfile.NamedTemporaryFile(
suffix=".toml", prefix="ca_test_config_", delete=False
) as tmp_file:
tmp_file.write(b"# test config\n")
tmp_path = tmp_file.name
context.temp_config_file = tmp_path
if not hasattr(context, "_cleanup_handlers"):
context._cleanup_handlers = []
context._cleanup_handlers.append(lambda: os.unlink(tmp_path))
@given("a temp file path is prepared")
def step_temp_file_path_prepared(context: Context) -> None:
"""Create a temporary FILE (not directory) to trigger --data-dir validation.
Registers a cleanup handler so the file is removed after the scenario.
"""
with tempfile.NamedTemporaryFile(prefix="ca_test_file_", delete=False) as tmp_file:
tmp_path = tmp_file.name
context.temp_file_path = tmp_path
if not hasattr(context, "_cleanup_handlers"):
context._cleanup_handlers = []
context._cleanup_handlers.append(lambda: os.unlink(tmp_path))
# ---------------------------------------------------------------------------
# When steps - CLI invocation
# ---------------------------------------------------------------------------
@when('I run the global options CLI with data-dir flag and "{subcommand}"')
def step_run_global_opts_data_dir(context: Context, subcommand: str) -> None:
"""Run the CLI with --data-dir pointing at the temp directory."""
args = ["--data-dir", context.temp_data_dir, subcommand]
result = _invoke(args)
context.global_opts_result = result
context.global_opts_exit_code = result.exit_code
context.global_opts_output = _combined_output(result)
@when('I run the global options CLI with config-path flag and "{subcommand}"')
def step_run_global_opts_config_path(context: Context, subcommand: str) -> None:
"""Run the CLI with --config-path pointing at the temp config file."""
args = ["--config-path", context.temp_config_file, subcommand]
result = _invoke(args)
context.global_opts_result = result
context.global_opts_exit_code = result.exit_code
context.global_opts_output = _combined_output(result)
@when('I run the global options CLI with both path flags and "{subcommand}"')
def step_run_global_opts_both(context: Context, subcommand: str) -> None:
"""Run the CLI with both --data-dir and --config-path."""
args = [
"--data-dir",
context.temp_data_dir,
"--config-path",
context.temp_config_file,
subcommand,
]
result = _invoke(args)
context.global_opts_result = result
context.global_opts_exit_code = result.exit_code
context.global_opts_output = _combined_output(result)
@when('I run the global options CLI with data-dir as temp file and "{subcommand}"')
def step_run_global_opts_data_dir_file(context: Context, subcommand: str) -> None:
"""Run CLI with --data-dir pointing to a FILE (triggers validation error)."""
args = ["--data-dir", context.temp_file_path, subcommand]
result = _invoke(args)
context.global_opts_result = result
context.global_opts_exit_code = result.exit_code
context.global_opts_output = _combined_output(result)
@when('I run the global options CLI with invalid config-path and "{subcommand}"')
def step_run_global_opts_invalid_config_path(context: Context, subcommand: str) -> None:
"""Run CLI with --config-path pointing to a non-existent file."""
args = ["--config-path", "/tmp/no_such_file_xyz_99999.toml", subcommand]
result = _invoke(args)
context.global_opts_result = result
context.global_opts_exit_code = result.exit_code
context.global_opts_output = _combined_output(result)
@when('I run the global options CLI with args "{args}"')
def step_run_global_opts_with_args(context: Context, args: str) -> None:
"""Run the CLI with space-separated args string."""
result = _invoke(args.split())
context.global_opts_result = result
context.global_opts_exit_code = result.exit_code
context.global_opts_output = _combined_output(result)
# ---------------------------------------------------------------------------
# When steps - direct callback testing for verbosity
# ---------------------------------------------------------------------------
@when("I call main_callback with verbosity {count:d}")
def step_call_main_callback_verbosity(context: Context, count: int) -> None:
"""Invoke main_callback directly, capturing the log level configured."""
from cleveragents.cli.main import main_callback
context.global_opts_captured_log_level: str | None = None
context.global_opts_captured_ctx_obj: dict[str, Any] = {}
def _capture_log_level(
*, env: str = "development", log_level: str = "INFO"
) -> None:
context.global_opts_captured_log_level = log_level
mock_ctx = MagicMock()
mock_ctx.obj = {}
with patch(
"cleveragents.config.logging.configure_structlog",
side_effect=_capture_log_level,
):
main_callback(
ctx=mock_ctx,
version=None, # type: ignore[arg-type]
show_secrets=False,
fmt=OutputFormat.RICH,
data_dir=None,
config_path=None,
verbose=count,
)
context.global_opts_captured_ctx_obj = mock_ctx.obj
# ---------------------------------------------------------------------------
# Then steps - exit codes
# ---------------------------------------------------------------------------
@then("the global options CLI should succeed")
def step_global_opts_cli_succeed(context: Context) -> None:
actual = context.global_opts_exit_code
output = context.global_opts_output
assert actual == 0, f"Expected exit code 0, got {actual}.\nOutput:\n{output}"
@then("the global options CLI should fail")
def step_global_opts_cli_fail(context: Context) -> None:
actual = context.global_opts_exit_code
assert actual != 0, (
f"Expected non-zero exit code, got {actual}.\nOutput:\n{context.global_opts_output}"
)
# ---------------------------------------------------------------------------
# Then steps - env var assertions
# ---------------------------------------------------------------------------
@then("the CLEVERAGENTS_DATA_DIR env var should match the temp data dir")
def step_env_data_dir_matches(context: Context) -> None:
"""Verify that --data-dir set CLEVERAGENTS_DATA_DIR to the temp directory."""
env_val = os.environ.get("CLEVERAGENTS_DATA_DIR")
expected = str(Path(context.temp_data_dir).resolve())
assert env_val is not None, (
"CLEVERAGENTS_DATA_DIR was not set after --data-dir invocation"
)
assert str(Path(env_val).resolve()) == expected, (
f"CLEVERAGENTS_DATA_DIR={env_val!r} does not match expected {expected!r}"
)
@then("the CLEVERAGENTS_CONFIG_PATH env var should match the temp config file")
def step_env_config_path_matches(context: Context) -> None:
"""Verify that --config-path set CLEVERAGENTS_CONFIG_PATH to the temp file."""
env_val = os.environ.get("CLEVERAGENTS_CONFIG_PATH")
expected = str(Path(context.temp_config_file).resolve())
assert env_val is not None, (
"CLEVERAGENTS_CONFIG_PATH was not set after --config-path invocation"
)
assert str(Path(env_val).resolve()) == expected, (
f"CLEVERAGENTS_CONFIG_PATH={env_val!r} does not match expected {expected!r}"
)
# ---------------------------------------------------------------------------
# Then steps - ctx.obj assertions
# ---------------------------------------------------------------------------
@then('the global options ctx obj "{key}" should be {value:d}')
def step_global_opts_ctx_obj_int(context: Context, key: str, value: int) -> None:
"""Verify ctx.obj[key] has the expected integer value."""
obj = context.global_opts_captured_ctx_obj
assert key in obj, f"ctx.obj missing key '{key}'. ctx.obj={obj}"
actual = obj[key]
assert actual == value, f"ctx.obj['{key}']={actual!r}, expected {value!r}"
# ---------------------------------------------------------------------------
# Then steps - log level assertions
# ---------------------------------------------------------------------------
@then('the global options log level should be "{level}"')
def step_global_opts_log_level(context: Context, level: str) -> None:
"""Verify that configure_structlog was called with the expected log level."""
actual = context.global_opts_captured_log_level
assert actual is not None, "configure_structlog was not called"
assert actual.upper() == level.upper(), (
f"Expected log level {level!r}, got {actual!r}"
)
# ---------------------------------------------------------------------------
# Then steps - output assertions
# ---------------------------------------------------------------------------
@then('the global options output should contain "{text}"')
def step_global_opts_output_contains(context: Context, text: str) -> None:
output = context.global_opts_output
assert text in output, f"Expected '{text}' in output:\n{output}"
@@ -252,14 +252,18 @@ def step_langgraph_graph_with_max_retries(context: Any, retries: int) -> None:
"I check langgraph should_retry with {status} validation and retry_count {count:d}"
)
def step_check_langgraph_should_retry(context: Any, status: str, count: int) -> None:
"""Check should_retry decision."""
"""Check should_retry decision.
Note: _should_retry is a conditional-edge function and is read-only from
LangGraph's perspective. The state dict passed here is NOT mutated by
the function (retry_count is incremented inside _validate instead).
"""
state: dict[str, Any] = {
"validation_result": {"status": status},
"retry_count": count,
}
decision = context.graph._should_retry(state)
context.retry_decision = decision
context.final_retry_count = state.get("retry_count", count)
@then('the langgraph retry decision should be "{decision}"')
@@ -283,6 +287,28 @@ def step_langgraph_validation_status(context: Any, status: str) -> None:
assert validation.get("status") == status
@then(
"the langgraph validate result should include retry_count incremented from {before:d} to {after:d}"
)
def step_langgraph_validate_result_retry_count(
context: Any, before: int, after: int
) -> None:
"""Verify _validate returns an incremented retry_count.
The _validate node is responsible for incrementing retry_count so that
LangGraph persists the new value into the graph state. Conditional-edge
functions such as _should_retry are read-only from LangGraph's perspective
and must not mutate state.
"""
result = context.node_result
assert "retry_count" in result, (
"_validate must return retry_count in its result dict"
)
assert result["retry_count"] == after, (
f"Expected retry_count {after} (incremented from {before}), got {result['retry_count']}"
)
@then('the langgraph validation message should contain "{text}"')
def step_langgraph_validation_message_contains(context: Any, text: str) -> None:
"""Verify validation message contains text."""
@@ -459,16 +459,32 @@ def step_have_state_with_retry_count(context: Any, count: int) -> None:
"I check uncovered langgraph should_retry with FAIL validation and retry_count {count:d}"
)
def step_check_should_retry_uncovered(context: Any, count: int) -> None:
"""Check should_retry and verify retry_count increment."""
"""Check should_retry returns correct decision.
_should_retry is a conditional-edge function LangGraph treats it as
read-only. The retry counter is already incremented inside _validate
(a node whose return dict IS merged into the state), so _handle_retry
is a simple pass-through bridge node.
"""
decision = context.graph._should_retry(context.state)
context.retry_decision = decision
context.final_retry_count = context.state.get("retry_count")
@then("the uncovered langgraph state retry_count should be incremented to {expected:d}")
def step_uncovered_retry_count_incremented(context: Any, expected: int) -> None:
"""Verify retry_count was incremented."""
assert context.final_retry_count == expected
@then(
"the uncovered langgraph state retry_count should not be mutated and remain {expected:d}"
)
def step_uncovered_retry_count_not_mutated(context: Any, expected: int) -> None:
"""Verify _should_retry did not mutate retry_count in state.
_should_retry is a conditional-edge function; LangGraph treats it as
read-only. The retry counter is incremented inside _validate (a node)
so that LangGraph persists the new value into the graph state.
"""
assert context.final_retry_count == expected, (
f"_should_retry must not mutate state['retry_count']: "
f"expected {expected}, got {context.final_retry_count}"
)
# Workflow retry scenario
@@ -0,0 +1,318 @@
"""Steps for tdd_cleanup_stale_destroys_execute_output.feature.
TDD issue-capture test for bug #11121:
_create_sandbox_for_plan() calls GitWorktreeSandbox.cleanup_stale() unconditionally,
destroying the cleveragents/plan-<id> branch when the plan is already in
execute/complete or execute/processing state (awaiting apply or still in progress).
The scenarios are tagged @tdd_issue_11121 as permanent regression guards.
"""
from __future__ import annotations
import contextlib
import shutil
import subprocess
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch
from behave import given, then, when
from behave.runner import Context
_PLAN_ID = "01TDDSANDBOX000000000000A"
_BRANCH_NAME = f"cleveragents/plan-{_PLAN_ID}"
def _git(args: list[str], cwd: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["git", *args],
cwd=cwd,
capture_output=True,
text=True,
check=True,
timeout=15,
)
def _branch_exists(repo_path: str, branch: str) -> bool:
"""Return True if the given branch exists in the repo."""
result = subprocess.run(
["git", "rev-parse", "--verify", f"refs/heads/{branch}"],
cwd=repo_path,
capture_output=True,
text=True,
check=False,
timeout=10,
)
return result.returncode == 0
def _init_git_repo(path: str) -> None:
_git(["init", "-q", "-b", "main"], path)
_git(["config", "user.name", "TDD Test"], path)
_git(["config", "user.email", "tdd@test.local"], path)
_git(["config", "commit.gpgsign", "false"], path)
def _build_execute_complete_mocks(
context: Context,
repo_path: str,
) -> None:
"""Build mock service + container for a plan in execute/complete state."""
from cleveragents.domain.models.core.plan import PlanPhase, ProcessingState
mock_resource = MagicMock()
mock_resource.resource_type_name = "git-checkout"
mock_resource.location = repo_path
mock_resource.resource_id = "res-tdd-11121-test"
mock_lr = MagicMock()
mock_lr.resource_id = "res-tdd-11121-test"
mock_project = MagicMock()
mock_project.linked_resources = [mock_lr]
# Plan is in execute/complete state — this is the critical state for the bug
mock_plan = MagicMock()
mock_plan.project_links = [MagicMock(project_name="local/tdd-11121-project")]
mock_plan.phase = PlanPhase.EXECUTE
mock_plan.processing_state = ProcessingState.COMPLETE
mock_plan.state = ProcessingState.COMPLETE
mock_service = MagicMock()
mock_service.get_plan.return_value = mock_plan
mock_project_repo = MagicMock()
mock_project_repo.get.return_value = mock_project
mock_resource_registry = MagicMock()
mock_resource_registry.show_resource.return_value = mock_resource
mock_container = MagicMock()
mock_container.namespaced_project_repo.return_value = mock_project_repo
mock_container.resource_registry_service.return_value = mock_resource_registry
context.tdd11121_service = mock_service
context.tdd11121_container = mock_container
context.tdd11121_repo_path = repo_path
context.tdd11121_plan_id = _PLAN_ID
context.tdd11121_branch = _BRANCH_NAME
@given("a temp git repo with an execute-output branch for tdd 11121")
def step_create_git_repo_with_execute_output(context: Context) -> None:
"""Create a real git repo with a cleveragents/plan-<id> branch holding execute output.
This simulates the state after a successful plan execute: the worktree branch
exists and contains at least one committed file representing execution output.
"""
d = tempfile.mkdtemp(prefix="tdd-11121-")
context.add_cleanup(shutil.rmtree, d, True)
# Initialise the repo with a base commit on main
_init_git_repo(d)
Path(d, "README.md").write_text("# project\n")
_git(["add", "."], d)
_git(["commit", "-q", "-m", "init: base commit"], d)
# Create the cleveragents/plan-<id> branch and commit an output file to it.
# This simulates what _commit_worktree_changes() does after execute completes.
_git(["checkout", "-q", "-b", _BRANCH_NAME], d)
output_file = Path(d, "generated_output.py")
output_file.write_text("# Generated by plan execute\nresult = 42\n")
_git(["add", "."], d)
_git(["commit", "-q", "-m", f"cleveragents: execute output for plan {_PLAN_ID}"], d)
# Return to main so the repo is in a normal state
_git(["checkout", "-q", "main"], d)
context.tdd11121_repo_path = d
# Verify the branch exists before the test
assert _branch_exists(d, _BRANCH_NAME), (
f"Pre-condition failed: branch {_BRANCH_NAME} should exist before test"
)
def _build_execute_processing_mocks(
context: Context,
repo_path: str,
) -> None:
"""Build mock service + container for a plan in execute/processing state."""
from cleveragents.domain.models.core.plan import PlanPhase, ProcessingState
mock_resource = MagicMock()
mock_resource.resource_type_name = "git-checkout"
mock_resource.location = repo_path
mock_resource.resource_id = "res-tdd-11121-test"
mock_lr = MagicMock()
mock_lr.resource_id = "res-tdd-11121-test"
mock_project = MagicMock()
mock_project.linked_resources = [mock_lr]
mock_plan = MagicMock()
mock_plan.project_links = [MagicMock(project_name="local/tdd-11121-project")]
mock_plan.phase = PlanPhase.EXECUTE
mock_plan.processing_state = ProcessingState.PROCESSING
mock_plan.state = ProcessingState.PROCESSING
mock_service = MagicMock()
mock_service.get_plan.return_value = mock_plan
mock_project_repo = MagicMock()
mock_project_repo.get.return_value = mock_project
mock_resource_registry = MagicMock()
mock_resource_registry.show_resource.return_value = mock_resource
mock_container = MagicMock()
mock_container.namespaced_project_repo.return_value = mock_project_repo
mock_container.resource_registry_service.return_value = mock_resource_registry
context.tdd11121_service = mock_service
context.tdd11121_container = mock_container
context.tdd11121_repo_path = repo_path
context.tdd11121_plan_id = _PLAN_ID
context.tdd11121_branch = _BRANCH_NAME
@given("a mocked plan service with the plan in execute/complete state for tdd 11121")
def step_mock_service_execute_complete(context: Context) -> None:
"""Set up mock service returning a plan in execute/complete state."""
_build_execute_complete_mocks(context, context.tdd11121_repo_path)
@given("a mocked plan service with the plan in execute/processing state for tdd 11121")
def step_mock_service_execute_processing(context: Context) -> None:
"""Set up mock service returning a plan in execute/processing state."""
_build_execute_processing_mocks(context, context.tdd11121_repo_path)
@when(
"I call _create_sandbox_for_plan a second time on the execute/complete plan for tdd 11121"
)
def step_call_create_sandbox_second_time(context: Context) -> None:
"""Call _create_sandbox_for_plan on a plan already in execute/complete state.
This simulates the user re-running 'agents plan execute <PLAN_ID>' after
execution has already completed. The bug causes cleanup_stale to run and
destroy the cleveragents/plan-<id> branch that holds the execute output.
"""
from cleveragents.cli.commands.plan import _create_sandbox_for_plan
context.tdd11121_second_call_exception: Exception | None = None
try:
with (
patch(
"cleveragents.cli.commands.plan.get_container",
return_value=context.tdd11121_container,
),
patch(
"cleveragents.application.container.get_container",
return_value=context.tdd11121_container,
),
):
_sandbox_root, sandbox_infos = _create_sandbox_for_plan(
context.tdd11121_plan_id,
context.tdd11121_service,
)
context.tdd11121_sandbox_infos = sandbox_infos
# Clean up any newly created sandbox to avoid resource leaks
for sinfo in sandbox_infos:
with contextlib.suppress(Exception):
sinfo.sandbox_obj.cleanup()
except Exception as exc:
# Record but don't re-raise — we want to check branch state regardless
context.tdd11121_second_call_exception = exc
@when(
"I call _create_sandbox_for_plan a second time on the execute/processing plan for tdd 11121"
)
def step_call_create_sandbox_second_time_processing(context: Context) -> None:
"""Call _create_sandbox_for_plan on a plan already in execute/processing state.
This simulates the user re-running 'agents plan execute <PLAN_ID>' while the
plan is still in progress (execute/processing). The guard must protect the
sandbox branch from cleanup_stale in this state too.
"""
from cleveragents.cli.commands.plan import _create_sandbox_for_plan
context.tdd11121_second_call_exception: Exception | None = None
try:
with (
patch(
"cleveragents.cli.commands.plan.get_container",
return_value=context.tdd11121_container,
),
patch(
"cleveragents.application.container.get_container",
return_value=context.tdd11121_container,
),
):
_sandbox_root, sandbox_infos = _create_sandbox_for_plan(
context.tdd11121_plan_id,
context.tdd11121_service,
)
context.tdd11121_sandbox_infos = sandbox_infos
for sinfo in sandbox_infos:
with contextlib.suppress(Exception):
sinfo.sandbox_obj.cleanup()
except Exception as exc:
context.tdd11121_second_call_exception = exc
@then(
"the cleveragents plan branch should still exist after the second call for tdd 11121"
)
def step_branch_still_exists(context: Context) -> None:
"""Assert the cleveragents/plan-<id> branch was NOT destroyed by cleanup_stale.
The guard in _create_sandbox_for_plan skips cleanup_stale when the plan is
already in execute/processing or execute/complete state, preserving the
sandbox branch so plan apply can merge it.
"""
repo_path = context.tdd11121_repo_path
branch = context.tdd11121_branch
assert _branch_exists(repo_path, branch), (
f"Bug #11121: branch '{branch}' was destroyed by cleanup_stale during "
f"a second _create_sandbox_for_plan call. "
f"The branch must survive until plan apply merges it."
)
@then("the apply sandbox changes should find at least one artifact for tdd 11121")
def step_apply_finds_artifacts(context: Context) -> None:
"""Assert that apply can find the execute output after a re-invoked execute.
The guard in _create_sandbox_for_plan preserves the sandbox branch, so the
git diff between HEAD and the plan branch finds at least one artifact.
"""
repo_path = context.tdd11121_repo_path
branch = context.tdd11121_branch
if not _branch_exists(repo_path, branch):
raise AssertionError(
f"Bug #11121: branch '{branch}' was destroyed by cleanup_stale. "
f"plan apply would find 0 artifacts (empty changeset). "
f"The branch must persist until apply merges it."
)
result = subprocess.run(
["git", "diff", "--stat", f"HEAD...{branch}"],
cwd=repo_path,
capture_output=True,
text=True,
check=False,
timeout=10,
)
stat_lines = (result.stdout or "").strip().splitlines()
artifact_count = max(0, len(stat_lines) - 1) if stat_lines else 0
assert artifact_count > 0, (
f"Bug #11121: plan apply would find {artifact_count} artifacts after "
f"re-invoked execute on execute/complete plan. Expected >= 1 artifact. "
f"diff --stat output: {result.stdout!r}"
)
@@ -0,0 +1,331 @@
"""Step definitions for TDD Issue #9131 — invariant_enforced decisions not
propagated to child plans on subplan spawn.
These steps exercise ``SubplanService.spawn()`` and verify that it propagates
``invariant_enforced`` decisions from the parent plan to each child plan's
decision tree.
On ``master`` (before the fix), ``spawn()`` creates child Plan objects but
does NOT record ``invariant_enforced`` decisions on them. The assertions in
these steps will **fail** until the bug is fixed.
"""
from __future__ import annotations
from behave import given, then, when
from behave.runner import Context
from cleveragents.application.services.decision_service import DecisionService
from cleveragents.application.services.subplan_service import (
SpawnEntry,
SpawnResult,
SubplanService,
)
from cleveragents.domain.models.core.decision import (
ContextSnapshot,
Decision,
DecisionType,
)
from cleveragents.domain.models.core.plan import (
ExecutionMode,
NamespacedName,
Plan,
PlanIdentity,
SubplanConfig,
)
_PARENT_PLAN_ID: str = "01HGZ6FE0AQDYTR4BXVQZ6PN00"
_ROOT_PLAN_ID: str = "01HGZ6FE0AQDYTR4BXVQZ6RF00"
_SPAWN_DEC_ID: str = "01HGZ6FE0AQDYTR4BXVQZ6DA00"
def _make_parent_plan() -> Plan:
"""Create a minimal parent plan for testing."""
return Plan(
identity=PlanIdentity(
plan_id=_PARENT_PLAN_ID,
root_plan_id=_ROOT_PLAN_ID,
),
namespaced_name=NamespacedName(namespace="local", name="parent-plan"),
description="Parent plan for invariant propagation test",
action_name="local/parent-action",
)
def _make_spawn_decision(
plan_id: str = _PARENT_PLAN_ID,
) -> Decision:
"""Create a subplan_spawn Decision."""
return Decision(
decision_id=_SPAWN_DEC_ID,
plan_id=plan_id,
decision_type=DecisionType.SUBPLAN_SPAWN,
sequence_number=10,
question="Should we spawn a child plan?",
chosen_option="local/sub-action",
context_snapshot=ContextSnapshot(),
)
def _make_spawn_entry(plan_id: str = _PARENT_PLAN_ID) -> SpawnEntry:
"""Create a single spawn entry."""
return SpawnEntry(
decision=_make_spawn_decision(plan_id),
action_name="local/sub-action",
description="Child plan for invariant propagation test",
)
# ---------------------------------------------------------------------------
# Given steps
# ---------------------------------------------------------------------------
@given("a parent plan with invariant_enforced decisions recorded")
def step_parent_with_invariant_decisions(context: Context) -> None:
"""Set up a parent plan with one invariant_enforced decision."""
context.parent_plan = _make_parent_plan()
context.decision_service = DecisionService()
# Record one invariant_enforced decision on the parent plan
inv_dec = context.decision_service.record_decision(
plan_id=_PARENT_PLAN_ID,
decision_type=DecisionType.INVARIANT_ENFORCED,
question="Is this invariant enforced?",
chosen_option="invariant enforced: do not modify production data",
)
context.parent_invariant_decisions = [inv_dec]
context.expected_invariant_count = 1
context.subplan_service = SubplanService(
decision_service=context.decision_service,
)
context.subplan_config = SubplanConfig(
execution_mode=ExecutionMode.SEQUENTIAL,
)
@given("a parent plan with {count:d} invariant_enforced decisions recorded")
def step_parent_with_n_invariant_decisions(context: Context, count: int) -> None:
"""Set up a parent plan with N invariant_enforced decisions."""
context.parent_plan = _make_parent_plan()
context.decision_service = DecisionService()
inv_decisions: list[Decision] = []
for i in range(count):
inv_dec = context.decision_service.record_decision(
plan_id=_PARENT_PLAN_ID,
decision_type=DecisionType.INVARIANT_ENFORCED,
question=f"Is invariant {i} enforced?",
chosen_option=f"invariant enforced: constraint {i}",
)
inv_decisions.append(inv_dec)
context.parent_invariant_decisions = inv_decisions
context.expected_invariant_count = count
context.subplan_service = SubplanService(
decision_service=context.decision_service,
)
context.subplan_config = SubplanConfig(
execution_mode=ExecutionMode.SEQUENTIAL,
)
@given("a parent plan with a non_overridable invariant_enforced decision")
def step_parent_with_non_overridable_invariant(context: Context) -> None:
"""Set up a parent plan with a non_overridable invariant_enforced decision."""
context.parent_plan = _make_parent_plan()
context.decision_service = DecisionService()
# Record a non_overridable invariant decision
inv_dec = context.decision_service.record_decision(
plan_id=_PARENT_PLAN_ID,
decision_type=DecisionType.INVARIANT_ENFORCED,
question="Is this non-overridable global invariant enforced?",
chosen_option="non_overridable invariant enforced: never delete user data",
rationale="Global non-overridable invariant must propagate to all child plans",
)
context.parent_invariant_decisions = [inv_dec]
context.non_overridable_chosen_option = inv_dec.chosen_option
context.expected_invariant_count = 1
context.subplan_service = SubplanService(
decision_service=context.decision_service,
)
context.subplan_config = SubplanConfig(
execution_mode=ExecutionMode.SEQUENTIAL,
)
@given("a parent plan with no invariant_enforced decisions")
def step_parent_with_no_invariant_decisions(context: Context) -> None:
"""Set up a parent plan with no invariant_enforced decisions."""
context.parent_plan = _make_parent_plan()
context.decision_service = DecisionService()
context.parent_invariant_decisions = []
context.expected_invariant_count = 0
context.subplan_service = SubplanService(
decision_service=context.decision_service,
)
context.subplan_config = SubplanConfig(
execution_mode=ExecutionMode.SEQUENTIAL,
)
@given("a valid spawn entry for the parent plan")
def step_valid_spawn_entry(context: Context) -> None:
"""Create a single valid spawn entry."""
context.spawn_entries = [_make_spawn_entry()]
# ---------------------------------------------------------------------------
# When steps
# ---------------------------------------------------------------------------
@when("I spawn a child plan via SubplanService")
def step_spawn_child_plan(context: Context) -> None:
"""Call SubplanService.spawn() and capture the result."""
context.spawn_error = None
try:
context.spawn_result = context.subplan_service.spawn(
parent_plan=context.parent_plan,
config=context.subplan_config,
spawn_entries=context.spawn_entries,
)
except Exception as exc:
context.spawn_error = exc
context.spawn_result = None
# ---------------------------------------------------------------------------
# Then steps
# ---------------------------------------------------------------------------
@then(
"the decision service should have recorded invariant_enforced decisions"
" for the child plan"
)
def step_child_has_invariant_decisions(context: Context) -> None:
"""Assert that invariant_enforced decisions were recorded for the child plan.
Bug #9131: spawn() does not propagate invariant_enforced decisions to
child plans. This assertion will fail until the bug is fixed.
"""
assert context.spawn_error is None, (
f"Spawn raised an unexpected error: {context.spawn_error}"
)
result: SpawnResult = context.spawn_result
assert result is not None, "Spawn result is None"
assert len(result.child_plans) > 0, "No child plans were created"
child_plan: Plan = result.child_plans[0]
child_plan_id: str = child_plan.identity.plan_id
# Query the decision service for invariant_enforced decisions on the child plan
child_invariant_decisions: list[Decision] = context.decision_service.list_by_type(
child_plan_id,
DecisionType.INVARIANT_ENFORCED,
)
assert len(child_invariant_decisions) > 0, (
f"Child plan {child_plan_id!r} has no invariant_enforced decisions. "
f"Bug #9131: SubplanService.spawn() does not propagate "
f"invariant_enforced decisions from parent plan to child plans."
)
@then("the child plan should have {count:d} invariant_enforced decisions recorded")
def step_child_has_n_invariant_decisions(context: Context, count: int) -> None:
"""Assert that the child plan has exactly N invariant_enforced decisions.
Bug #9131: spawn() does not propagate invariant_enforced decisions.
"""
assert context.spawn_error is None, (
f"Spawn raised an unexpected error: {context.spawn_error}"
)
result: SpawnResult = context.spawn_result
assert result is not None, "Spawn result is None"
assert len(result.child_plans) > 0, "No child plans were created"
child_plan: Plan = result.child_plans[0]
child_plan_id: str = child_plan.identity.plan_id
child_invariant_decisions: list[Decision] = context.decision_service.list_by_type(
child_plan_id,
DecisionType.INVARIANT_ENFORCED,
)
assert len(child_invariant_decisions) == count, (
f"Child plan {child_plan_id!r} has {len(child_invariant_decisions)} "
f"invariant_enforced decisions, expected {count}. "
f"Bug #9131: SubplanService.spawn() does not propagate "
f"invariant_enforced decisions from parent plan to child plans."
)
@then("the child plan should have the non_overridable invariant decision propagated")
def step_child_has_non_overridable_invariant(context: Context) -> None:
"""Assert that the non_overridable invariant decision was propagated.
Bug #9131: spawn() does not propagate invariant_enforced decisions.
"""
assert context.spawn_error is None, (
f"Spawn raised an unexpected error: {context.spawn_error}"
)
result: SpawnResult = context.spawn_result
assert result is not None, "Spawn result is None"
assert len(result.child_plans) > 0, "No child plans were created"
child_plan: Plan = result.child_plans[0]
child_plan_id: str = child_plan.identity.plan_id
child_invariant_decisions: list[Decision] = context.decision_service.list_by_type(
child_plan_id,
DecisionType.INVARIANT_ENFORCED,
)
assert len(child_invariant_decisions) > 0, (
f"Child plan {child_plan_id!r} has no invariant_enforced decisions. "
f"Bug #9131: non_overridable invariant was not propagated."
)
# Verify the non_overridable invariant text was propagated
expected_option: str = context.non_overridable_chosen_option
propagated_options: list[str] = [d.chosen_option for d in child_invariant_decisions]
assert expected_option in propagated_options, (
f"Non-overridable invariant chosen_option {expected_option!r} not found "
f"in child plan decisions: {propagated_options}. "
f"Bug #9131: non_overridable invariant was not propagated."
)
@then("the spawn should succeed with no invariant decisions recorded for the child")
def step_spawn_succeeds_no_invariants(context: Context) -> None:
"""Assert that spawn succeeds cleanly when parent has no invariant decisions."""
assert context.spawn_error is None, (
f"Spawn raised an unexpected error: {context.spawn_error}"
)
result: SpawnResult = context.spawn_result
assert result is not None, "Spawn result is None"
assert result.total_spawned == 1, (
f"Expected 1 spawned child, got {result.total_spawned}"
)
# When parent has no invariant decisions, child should also have none
assert len(result.child_plans) > 0, "No child plans were created"
child_plan: Plan = result.child_plans[0]
child_plan_id: str = child_plan.identity.plan_id
child_invariant_decisions: list[Decision] = context.decision_service.list_by_type(
child_plan_id,
DecisionType.INVARIANT_ENFORCED,
)
assert len(child_invariant_decisions) == 0, (
f"Child plan {child_plan_id!r} has {len(child_invariant_decisions)} "
f"invariant_enforced decisions, expected 0 (parent had none)."
)
@@ -0,0 +1,33 @@
@tdd_issue @tdd_issue_11121
Feature: TDD Issue #11121 — cleanup_stale destroys git worktree branch on re-invoked execute
As a developer
I want to verify that _create_sandbox_for_plan does NOT delete the cleveragents/plan-<id>
branch when the plan is already in execute/complete state
So that plan apply can subsequently find and merge the correct artifacts
Bug #11121: _create_sandbox_for_plan() calls GitWorktreeSandbox.cleanup_stale()
unconditionally every time agents plan execute is invoked including when the plan
is already in execute/complete state (execution finished, awaiting apply). This deletes
the cleveragents/plan-<id> git branch that holds the execution output, so when
agents plan apply runs next it finds no branch and merges zero artifacts.
@tdd_issue @tdd_issue_11121 @mock_only
Scenario: cleveragents/plan-<id> branch survives a second _create_sandbox_for_plan call when plan is execute/complete
Given a temp git repo with an execute-output branch for tdd 11121
And a mocked plan service with the plan in execute/complete state for tdd 11121
When I call _create_sandbox_for_plan a second time on the execute/complete plan for tdd 11121
Then the cleveragents plan branch should still exist after the second call for tdd 11121
@tdd_issue @tdd_issue_11121 @mock_only
Scenario: plan apply finds non-zero artifacts after re-invoked execute on execute/complete plan
Given a temp git repo with an execute-output branch for tdd 11121
And a mocked plan service with the plan in execute/complete state for tdd 11121
When I call _create_sandbox_for_plan a second time on the execute/complete plan for tdd 11121
Then the apply sandbox changes should find at least one artifact for tdd 11121
@tdd_issue @tdd_issue_11121 @mock_only
Scenario: cleveragents/plan-<id> branch survives a second _create_sandbox_for_plan call when plan is execute/processing
Given a temp git repo with an execute-output branch for tdd 11121
And a mocked plan service with the plan in execute/processing state for tdd 11121
When I call _create_sandbox_for_plan a second time on the execute/processing plan for tdd 11121
Then the cleveragents plan branch should still exist after the second call for tdd 11121
@@ -0,0 +1,38 @@
@tdd_issue @tdd_issue_9131 @mock_only
Feature: TDD Issue #9131 — invariant_enforced decisions not propagated to child plans on subplan spawn
As a developer
I want to verify that SubplanService.spawn() propagates invariant_enforced decisions
from the parent plan to each child plan's decision tree
So that child plans start Strategize with the parent's enforced invariant set
The spec states: "recorded as invariant_enforced decisions that propagate to child plans."
SubplanService.spawn() currently creates child Plan objects with no invariant_enforced
decisions, violating the spec's propagation requirement.
@tdd_issue @tdd_issue_9131
Scenario: Child plan receives parent invariant_enforced decisions on spawn
Given a parent plan with invariant_enforced decisions recorded
And a valid spawn entry for the parent plan
When I spawn a child plan via SubplanService
Then the decision service should have recorded invariant_enforced decisions for the child plan
@tdd_issue @tdd_issue_9131
Scenario: Child plan receives all parent invariant_enforced decisions
Given a parent plan with 3 invariant_enforced decisions recorded
And a valid spawn entry for the parent plan
When I spawn a child plan via SubplanService
Then the child plan should have 3 invariant_enforced decisions recorded
@tdd_issue @tdd_issue_9131
Scenario: Non-overridable global invariants are propagated to child plans
Given a parent plan with a non_overridable invariant_enforced decision
And a valid spawn entry for the parent plan
When I spawn a child plan via SubplanService
Then the child plan should have the non_overridable invariant decision propagated
@tdd_issue @tdd_issue_9131
Scenario: Child plan with no parent invariant_enforced decisions spawns cleanly
Given a parent plan with no invariant_enforced decisions
And a valid spawn entry for the parent plan
When I spawn a child plan via SubplanService
Then the spawn should succeed with no invariant decisions recorded for the child
+3
View File
@@ -179,6 +179,9 @@ def unit_tests(session: nox.Session):
template_path = _create_template_db(session)
session.env["CLEVERAGENTS_TEMPLATE_DB"] = template_path
# Remove ANSI escape codes.
session.env["TERM"] = "dumb"
behave_cmd = session.bin + "/behave-parallel"
parallel_args = _behave_parallel_args(session.posargs)
+79
View File
@@ -0,0 +1,79 @@
*** Settings ***
Documentation Integration tests for CLI global options --data-dir, --config-path, and -v.
...
... Verifies that the spec-required global options are properly accepted
... and propagated end-to-end (issue #6785).
Resource ${CURDIR}/common.resource
Suite Setup Setup Test Environment
Suite Teardown Cleanup Test Environment
*** Variables ***
${HELPER} ${CURDIR}/helper_cli_global_options.py
*** Test Cases ***
Data Dir Option Accepted With Valid Directory
[Documentation] Verify --data-dir is accepted without error for an existing directory.
${result}= Run Process ${PYTHON} ${HELPER} data-dir-accepted cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} cli-global-options-data-dir-accepted-ok
Config Path Option Accepted With Valid File
[Documentation] Verify --config-path is accepted without error for an existing file.
${result}= Run Process ${PYTHON} ${HELPER} config-path-accepted cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} cli-global-options-config-path-accepted-ok
Verbosity Flag Accepted
[Documentation] Verify -v flag is accepted without crashing.
${result}= Run Process ${PYTHON} ${HELPER} verbosity-accepted cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} cli-global-options-verbosity-accepted-ok
Data Dir And Config Path Combined
[Documentation] Verify --data-dir and --config-path can be combined end-to-end.
${result}= Run Process ${PYTHON} ${HELPER} combined-options cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} cli-global-options-combined-ok
Data Dir Overrides Settings Data Dir
[Documentation] Verify --data-dir properly overrides CLEVERAGENTS_DATA_DIR.
${result}= Run Process ${PYTHON} ${HELPER} data-dir-override cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} cli-global-options-data-dir-override-ok
Config Path Overrides Config Service Path
[Documentation] Verify --config-path properly overrides the config service path.
${result}= Run Process ${PYTHON} ${HELPER} config-path-override cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} cli-global-options-config-path-override-ok
Verbosity Sets Log Level Correctly
[Documentation] Verify -v count correctly maps to log levels.
${result}= Run Process ${PYTHON} ${HELPER} verbosity-levels cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} cli-global-options-verbosity-levels-ok
Help Shows All Three Options
[Documentation] Verify --help output includes --data-dir, --config-path, and -v.
${result}= Run Process ${PYTHON} -m cleveragents --help
... timeout=60s on_timeout=kill
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} --data-dir
Should Contain ${result.stdout} --config-path
Should Contain ${result.stdout} -v
+204
View File
@@ -0,0 +1,204 @@
"""Helper script for cli_global_options.robot integration tests.
Each sub-command verifies one end-to-end behaviour of the new global CLI
options (--data-dir, --config-path, -v) introduced by issue #6785.
"""
from __future__ import annotations
import os
import sys
import tempfile
from pathlib import Path
from unittest.mock import patch
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _invoke(args: list[str]) -> tuple[int, str]:
"""Invoke the Typer app and return (exit_code, combined_output)."""
from typer.testing import CliRunner
from cleveragents.cli.main import app
runner = CliRunner()
result = runner.invoke(app, args, catch_exceptions=True)
output = result.output or ""
return result.exit_code, output
# ---------------------------------------------------------------------------
# Test cases (sub-commands)
# ---------------------------------------------------------------------------
def test_data_dir_accepted() -> None:
with tempfile.TemporaryDirectory() as tmp:
code, output = _invoke(["--data-dir", tmp, "version"])
assert code == 0, f"Expected exit 0, got {code}. Output: {output}"
print("cli-global-options-data-dir-accepted-ok")
def test_config_path_accepted() -> None:
with tempfile.NamedTemporaryFile(suffix=".toml", delete=False) as tmp:
tmp.write(b"# test config\n")
tmp_path = tmp.name
try:
code, output = _invoke(["--config-path", tmp_path, "version"])
assert code == 0, f"Expected exit 0, got {code}. Output: {output}"
print("cli-global-options-config-path-accepted-ok")
finally:
os.unlink(tmp_path)
def test_verbosity_accepted() -> None:
code, output = _invoke(["-v", "version"])
assert code == 0, f"Expected exit 0, got {code}. Output: {output}"
print("cli-global-options-verbosity-accepted-ok")
def test_combined_options() -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
with tempfile.NamedTemporaryFile(suffix=".toml", delete=False) as tmp_cfg:
tmp_cfg.write(b"# test\n")
tmp_cfg_path = tmp_cfg.name
try:
code, output = _invoke(
[
"--data-dir",
tmp_dir,
"--config-path",
tmp_cfg_path,
"version",
]
)
assert code == 0, f"Expected exit 0, got {code}. Output: {output}"
print("cli-global-options-combined-ok")
finally:
os.unlink(tmp_cfg_path)
def test_data_dir_override() -> None:
"""Verify --data-dir sets CLEVERAGENTS_DATA_DIR env var during invocation."""
with tempfile.TemporaryDirectory() as tmp:
code, output = _invoke(["--data-dir", tmp, "version"])
assert code == 0, f"Expected exit 0, got {code}. Output: {output}"
# Verify the env var was set to (or derived from) the tmp dir
env_val = os.environ.get("CLEVERAGENTS_DATA_DIR")
if env_val:
resolved_env = str(Path(env_val).resolve())
resolved_tmp = str(Path(tmp).resolve())
assert resolved_env == resolved_tmp, (
f"CLEVERAGENTS_DATA_DIR={env_val!r} != {tmp!r}"
)
print("cli-global-options-data-dir-override-ok")
def test_config_path_override() -> None:
"""Verify --config-path sets CLEVERAGENTS_CONFIG_PATH env var during invocation."""
with tempfile.NamedTemporaryFile(suffix=".toml", delete=False) as tmp:
tmp.write(b"# test\n")
tmp_path = tmp.name
try:
code, output = _invoke(["--config-path", tmp_path, "version"])
assert code == 0, f"Expected exit 0, got {code}. Output: {output}"
env_val = os.environ.get("CLEVERAGENTS_CONFIG_PATH")
if env_val:
assert str(Path(env_val).resolve()) == str(Path(tmp_path).resolve()), (
f"CLEVERAGENTS_CONFIG_PATH={env_val!r} != {tmp_path!r}"
)
print("cli-global-options-config-path-override-ok")
finally:
os.unlink(tmp_path)
def test_verbosity_levels() -> None:
"""Verify the verbosity count → log level mapping."""
from unittest.mock import MagicMock
from cleveragents.cli.formatting import OutputFormat
from cleveragents.cli.main import main_callback
expected_levels = {
0: "CRITICAL",
1: "ERROR",
2: "WARNING",
3: "INFO",
4: "DEBUG",
5: "DEBUG",
}
for count, expected in expected_levels.items():
captured_levels: list[str] = []
def _make_capture(levels: list[str]): # type: ignore[no-untyped-def]
def _capture(*, env: str = "development", log_level: str = "INFO") -> None:
levels.append(log_level)
return _capture
mock_ctx = MagicMock()
mock_ctx.obj = {}
with patch(
"cleveragents.config.logging.configure_structlog",
side_effect=_make_capture(captured_levels),
):
main_callback(
ctx=mock_ctx,
version=None, # type: ignore[arg-type]
show_secrets=False,
fmt=OutputFormat.RICH,
data_dir=None,
config_path=None,
verbose=count,
)
assert captured_levels, f"configure_structlog not called for count={count}"
actual = captured_levels[-1].upper()
assert actual == expected, (
f"verbose={count}: expected log_level={expected!r}, got {actual!r}"
)
print("cli-global-options-verbosity-levels-ok")
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
_COMMANDS: dict[str, object] = {
"data-dir-accepted": test_data_dir_accepted,
"config-path-accepted": test_config_path_accepted,
"verbosity-accepted": test_verbosity_accepted,
"combined-options": test_combined_options,
"data-dir-override": test_data_dir_override,
"config-path-override": test_config_path_override,
"verbosity-levels": test_verbosity_levels,
}
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: helper_cli_global_options.py <command>", file=sys.stderr)
sys.exit(1)
cmd = sys.argv[1]
fn = _COMMANDS.get(cmd)
if fn is None:
print(f"Unknown command: {cmd}", file=sys.stderr)
sys.exit(1)
try:
fn() # type: ignore[call-arg]
except Exception as exc:
print(f"FAILED: {exc}", file=sys.stderr)
import traceback
traceback.print_exc(file=sys.stderr)
sys.exit(1)
+10 -5
View File
@@ -83,9 +83,10 @@ Plan Generation Graph Builds Workflow With Correct Nodes
... assert 'analyze_requirements' in nodes
... assert 'generate_plan' in nodes
... assert 'validate' in nodes
... assert 'handle_retry' in nodes
... print(f'Graph has {len(nodes)} nodes')
${result}= Run Process ${PYTHON} -c ${script} shell=True
Should Contain ${result.stdout} Graph has 4 nodes
Should Contain ${result.stdout} Graph has 5 nodes
Should Be Equal As Integers ${result.rc} 0
LangGraph Graphs Package Exports Workflow Classes
@@ -192,7 +193,9 @@ Load Context Node Generates Summary With Sample Contexts
Should Retry Returns Retry When Validation Fails And Retries Available
[Documentation] Test _should_retry returns "retry" appropriately
[Documentation] Test _should_retry returns "retry" appropriately.
... Note: _should_retry is a read-only conditional-edge function in LangGraph.
... It does NOT mutate state; retry_count is incremented inside _validate.
${script}= Catenate SEPARATOR=\n
... import sys
... sys.path.insert(0, '${SRC_DIR}')
@@ -205,7 +208,7 @@ Should Retry Returns Retry When Validation Fails And Retries Available
... }
... decision = graph._should_retry(state)
... assert decision == 'retry'
... assert state['retry_count'] == 1
... assert state['retry_count'] == 0, f'_should_retry must not mutate state, got {state["retry_count"]}'
... print('Should retry: retry decision correct')
${result}= Run Process ${PYTHON} -c ${script} shell=True
Should Contain ${result.stdout} Should retry: retry decision correct
@@ -231,7 +234,9 @@ Should Retry Returns End When Validation Passes
Should Be Equal As Integers ${result.rc} 0
Should Retry Returns End When Max Retries Reached
[Documentation] Test _should_retry returns "end" when max retries reached
[Documentation] Test _should_retry returns "end" when max retries exceeded.
... Because _validate increments retry_count before _should_retry is called,
... the boundary is retry_count > max_retries (i.e. retry_count=4 for max_retries=3).
${script}= Catenate SEPARATOR=\n
... import sys
... sys.path.insert(0, '${SRC_DIR}')
@@ -240,7 +245,7 @@ Should Retry Returns End When Max Retries Reached
... graph = PlanGenerationGraph(llm=FakeListLLM(responses=['test']*3), max_retries=3)
... state = {
... 'validation_result': {'status': 'FAIL'},
... 'retry_count': 3
... 'retry_count': 4
... }
... decision = graph._should_retry(state)
... assert decision == 'end'
@@ -154,6 +154,8 @@ class PlanGenerationGraph:
2. analyze_requirements: Analyzes user prompt for requirements
3. generate_plan: Generates code changes based on requirements
4. validate: Validates generated changes
5. handle_retry: Increments the retry counter (bridges conditional edge to
state update, since LangGraph conditional edges cannot persist mutations)
The workflow includes conditional edges for retry logic and checkpointing
for resumable execution.
@@ -273,6 +275,7 @@ class PlanGenerationGraph:
workflow.add_node("analyze_requirements", self._analyze_requirements)
workflow.add_node("generate_plan", self._generate_plan)
workflow.add_node("validate", self._validate)
workflow.add_node("handle_retry", self._handle_retry)
# Set entry point
workflow.set_entry_point("load_context")
@@ -282,12 +285,16 @@ class PlanGenerationGraph:
workflow.add_edge("analyze_requirements", "generate_plan")
workflow.add_edge("generate_plan", "validate")
# Route retries through handle_retry node to persist the retry_count
# increment (conditional edge functions cannot mutate state).
workflow.add_edge("handle_retry", "analyze_requirements")
# Add conditional edge for retry logic
workflow.add_conditional_edges(
"validate",
self._should_retry,
{
"retry": "analyze_requirements",
"retry": "handle_retry",
"end": END,
},
)
@@ -497,9 +504,15 @@ class PlanGenerationGraph:
state: Current workflow state
Returns:
Updated state with validation results
Updated state with validation results and incremented retry_count.
The retry_count is incremented here (inside a node) so that
LangGraph persists the new value into the graph state. It must
NOT be mutated inside ``_should_retry`` because conditional-edge
functions are read-only from LangGraph's perspective — any
mutations they make to the state dict are silently discarded.
"""
changes = state.get("generated_changes", [])
retry_count = state.get("retry_count", 0)
if not changes:
return {
@@ -507,6 +520,7 @@ class PlanGenerationGraph:
"status": "FAIL",
"message": "No changes to validate",
},
"retry_count": retry_count + 1,
}
# Create validation chain
@@ -527,14 +541,15 @@ class PlanGenerationGraph:
)
validation = str(result)
# Simple validation check (in real implementation, parse the LLM response)
is_valid = "PASS" in validation.upper() or len(all_code) > 10
# Reliance on the LLM response to determine pass/fail.
is_valid = "PASS" in validation.upper()
return {
"validation_result": {
"status": "PASS" if is_valid else "FAIL",
"message": validation,
},
"retry_count": retry_count + 1,
}
except Exception as e:
@@ -543,27 +558,53 @@ class PlanGenerationGraph:
"status": "FAIL",
"message": f"Validation failed: {e!s}",
},
"retry_count": retry_count + 1,
}
def _should_retry(self, state: PlanGenerationState) -> str:
"""Determine if workflow should retry based on validation.
This is a conditional-edge function LangGraph treats it as read-only.
Any mutations made to *state* here are silently discarded and never
persisted back into the graph state. The retry counter is therefore
incremented inside ``_validate`` (a proper node whose return dict IS
merged into the state) so that the counter advances correctly across
iterations.
Args:
state: Current workflow state (read-only in this context)
Returns:
"retry" or "end" based on validation status and retry count
"""
validation = state.get("validation_result", {})
# retry_count was already incremented by _validate before this edge
# function is called, so compare against max_retries directly.
retry_count = state.get("retry_count", 0)
# Check if validation failed and retries available
if validation.get("status") == "FAIL" and retry_count <= self.max_retries:
return "retry"
return "end"
def _handle_retry(self, state: PlanGenerationState) -> dict[str, Any]:
"""Bridge node for the retry conditional edge.
Conditional edge functions in LangGraph cannot persist state
mutations this node exists solely to satisfy the graph
compiler requirement that the retry path goes through a node
(not directly from a conditional edge back to a node).
The retry counter is already incremented inside ``_validate``
(a node whose return dict IS merged into the state).
Args:
state: Current workflow state
Returns:
"retry" or "end" based on validation and retry count
Empty update (no state mutation needed)
"""
validation = state.get("validation_result", {})
retry_count = state.get("retry_count", 0)
# Check if validation failed and retries available
if validation.get("status") == "FAIL" and retry_count < self.max_retries:
# Increment retry count
state["retry_count"] = retry_count + 1
return "retry"
return "end"
return {}
def _format_context_summary(self, contexts: list[Context]) -> str:
"""Format context files into a summary string.
@@ -38,6 +38,10 @@ if TYPE_CHECKING:
from cleveragents.domain.models.acms.crp import (
ContextRequest,
)
from cleveragents.domain.models.acms.strategy import (
ContextStrategy as DomainContextStrategy,
StrategyCapabilities as DomainStrategyCapabilities,
)
from cleveragents.domain.models.core.context_fragment import (
ULID_PATTERN,
ContextBudget,
@@ -95,13 +99,27 @@ logger = structlog.get_logger(__name__)
# ---------------------------------------------------------------------------
# Strategy capabilities (spec ~line 25167)
# StrategyCapabilities — pipeline-specific variant (backward-compatible)
# ---------------------------------------------------------------------------
# The domain-model ``StrategyCapabilities`` in
# ``domain/models/acms/strategy.py`` declares backend-usage flags
# (uses_text, uses_vector, uses_graph, etc.). ACMSPipeline uses a
# simpler dataclass for its internal strategy selectors. Both are
# re-exported here for backward compatibility; the pipeline-specific
# variant is used only by :class:`RelevanceStrategy`,
# :class:`RecencyStrategy`, :class:`TieredStrategy`, and
# :class:`SpecStrategyAdapter`.
@dataclass(frozen=True)
class StrategyCapabilities:
"""Capabilities declared by a context strategy."""
"""Pipeline-specific capabilities declared by a context strategy.
This is a backward-compatible variant of the domain-model
``DomainStrategyCapabilities`` (see :mod:`cleveragents.domain.models.acms.strategy`).
For production strategies that implement the true spec protocol, use
``DomainStrategyCapabilities`` instead.
"""
supports_semantic_search: bool = False
supports_graph_navigation: bool = False
@@ -111,18 +129,33 @@ class StrategyCapabilities:
# ---------------------------------------------------------------------------
# Context strategy protocol (spec ~line 25167)
# ContextStrategy — pipeline-compatible protocol for ACMSPipeline
# ---------------------------------------------------------------------------
# The domain-model ``ContextStrategy`` Protocol in
# ``domain/models/acms/strategy.py`` uses the full spec signature
# ``can_handle(request, backends) -> confidence`` /
# ``assemble(request, backends, budget, plan_context) -> fragments``.
# ACMSPipeline's internal strategies (Relevance, Recency, Tiered) and
# the SpecStrategyAdapter use a simpler pipeline-compatible Protocol
# because they operate on pre-fetched fragments rather than querying
# backends directly. SpecStrategyAdapter bridges between the two.
# When issue #3491 resolves protocol consolidation, this local Protocol
# can be replaced with ``DomainContextStrategy`` from
# ``cleveragents.domain.models.acms.strategy``.
@runtime_checkable
class ContextStrategy(Protocol):
"""Protocol for context strategies.
"""Pipeline-compatible Protocol for context strategies.
Based on ``docs/specification.md`` ~line 25167. Each strategy declares
a ``name`` and ``capabilities``, can report its confidence for a request
via ``can_handle``, produce fragments via ``assemble``, and explain its
approach via ``explain``.
ACMSPipeline's internal strategies (Relevance, Recency, Tiered) and
the SpecStrategyAdapter implement this interface. The domain-model
``DomainContextStrategy`` uses a different signature that queries
backends directly during :meth:`assemble`. SpecStrategyAdapter bridges
between the two by ranking pre-fetched fragments by relevance score.
When issue #3491 is resolved, this Protocol can be removed in favour of
``DomainContextStrategy`` from ``cleveragents.domain.models.acms.strategy``.
"""
@property
@@ -1183,6 +1183,14 @@ class ConfigService:
project_root: Path | _AutoDiscover | None = _AUTO_DISCOVER,
) -> None:
self._config_dir: Path = config_dir or _DEFAULT_CONFIG_DIR
# Honour CLEVERAGENTS_CONFIG_PATH env var when no explicit path is given.
# This allows the --config-path CLI flag (which sets the env var in
# main_callback) to propagate into all ConfigService instances created
# during subcommand execution (ADR-024 §Resolution Chain).
if config_path is None:
_env_config_path = os.environ.get("CLEVERAGENTS_CONFIG_PATH", "").strip()
if _env_config_path:
config_path = Path(_env_config_path)
self._config_path: Path = config_path or (self._config_dir / "config.toml")
self._event_bus = event_bus
# ``_AUTO_DISCOVER`` means "auto-discover"; ``None`` means "no project root".
@@ -7,6 +7,17 @@ presence, and ``max_parallel`` bounds before creating child plans.
Spawn metadata (spawn decision ID, parent/root plan IDs, execution mode) is
persisted alongside each child plan for status output and provenance tracking.
Invariant propagation
---------------------
Per the specification (Glossary Invariant): "recorded as
``invariant_enforced`` decisions that propagate to child plans."
When a child plan is spawned, all ``invariant_enforced`` decisions from the
parent plan's decision tree are re-recorded on the child plan's decision tree.
This ensures that child plans start Strategize with the full invariant set
enforced on the parent, including ``non_overridable`` global invariants.
Design decisions:
- Dependency injection: ``DecisionService`` and ``UnitOfWork`` are
injected via the constructor (consistent with existing service patterns).
@@ -19,6 +30,7 @@ Based on:
- docs/specification.md L18170-L18295 (subplan spawning)
- ADR-006 (Plan Lifecycle)
- Forgejo issue #197
- Forgejo issue #9131 (invariant_enforced propagation fix)
"""
from __future__ import annotations
@@ -154,6 +166,8 @@ class SubplanService:
2. Validating each entry (resource scopes, merge strategy, parallelism).
3. Building ``SubplanStatus`` objects for the parent plan.
4. Persisting ``SpawnMetadata`` for status queries.
5. Propagating ``invariant_enforced`` decisions from the parent plan to
each child plan's decision tree (spec: Glossary → Invariant).
Args:
decision_service: Service for querying decision records.
@@ -193,6 +207,12 @@ class SubplanService:
and ``SpawnMetadata`` for each entry. The caller is responsible for
persisting the updated parent plan.
Invariant propagation: all ``invariant_enforced`` decisions from the
parent plan are re-recorded on each child plan's decision tree so that
child plans start Strategize with the parent's full invariant set.
This satisfies the spec requirement: "recorded as ``invariant_enforced``
decisions that propagate to child plans."
Args:
parent_plan: The parent plan that will own the child plans.
config: Subplan execution configuration.
@@ -228,8 +248,17 @@ class SubplanService:
if not validation.valid:
raise SpawnValidationError(validation.errors)
# Build statuses, metadata, and real child Plan objects
# Retrieve parent plan's invariant_enforced decisions once, before
# creating child plans. These will be propagated to every child.
parent_id: str = parent_plan.identity.plan_id
parent_invariant_decisions: list[Decision] = (
self._decision_service.list_by_type(
parent_id,
DecisionType.INVARIANT_ENFORCED,
)
)
# Build statuses, metadata, and real child Plan objects
root_id: str = parent_plan.identity.root_plan_id or parent_id
mode: str = config.execution_mode.value
@@ -283,6 +312,16 @@ class SubplanService:
)
child_plans.append(child_plan)
# Propagate invariant_enforced decisions from parent to child.
# Each parent invariant decision is re-recorded on the child plan
# so that the child starts Strategize with the same invariant set.
# Spec: Glossary → Invariant: "recorded as invariant_enforced
# decisions that propagate to child plans."
self._propagate_invariant_decisions(
child_plan_id=subplan_id,
parent_invariant_decisions=parent_invariant_decisions,
)
# Attach subplan statuses to the parent plan for lifecycle tracking
parent_plan.subplan_statuses = list(parent_plan.subplan_statuses) + statuses
@@ -293,6 +332,7 @@ class SubplanService:
"root_plan_id": root_id,
"count": len(statuses),
"execution_mode": mode,
"invariant_decisions_propagated": len(parent_invariant_decisions),
},
)
@@ -304,6 +344,40 @@ class SubplanService:
child_plans=child_plans,
)
# ------------------------------------------------------------------
# _propagate_invariant_decisions
# ------------------------------------------------------------------
def _propagate_invariant_decisions(
self,
child_plan_id: str,
parent_invariant_decisions: list[Decision],
) -> None:
"""Propagate invariant_enforced decisions from parent to child plan.
Re-records each parent ``invariant_enforced`` decision on the child
plan's decision tree. The child plan starts Strategize with the same
invariant set as the parent, satisfying the spec requirement:
"recorded as ``invariant_enforced`` decisions that propagate to child
plans."
Args:
child_plan_id: ULID of the child plan to record decisions on.
parent_invariant_decisions: The parent plan's ``invariant_enforced``
decisions to propagate.
"""
for parent_decision in parent_invariant_decisions:
self._decision_service.record_decision(
plan_id=child_plan_id,
decision_type=DecisionType.INVARIANT_ENFORCED,
question=parent_decision.question,
chosen_option=parent_decision.chosen_option,
rationale=parent_decision.rationale,
context_snapshot=parent_decision.context_snapshot,
confidence_score=parent_decision.confidence_score,
alternatives_considered=list(parent_decision.alternatives_considered),
)
# ------------------------------------------------------------------
# validate_spawn
# ------------------------------------------------------------------
+13
View File
@@ -627,6 +627,19 @@ def _create_sandbox_for_plan(
container = get_container()
plan = service.get_plan(plan_id)
# Guard: when plan is already execute/processing or execute/complete,
# the sandbox branch holds output awaiting apply or is actively being
# used by an in-progress execution. Do NOT destroy it via cleanup_stale.
if (
plan is not None
and plan.phase == PlanPhase.EXECUTE
and plan.state in (ProcessingState.PROCESSING, ProcessingState.COMPLETE)
):
flat_root = os.path.join(os.getcwd(), ".cleveragents", "sandbox")
os.makedirs(flat_root, exist_ok=True)
return flat_root, []
project_names = [pl.project_name for pl in getattr(plan, "project_links", [])]
sandboxes: list[_SandboxInfo] = []
+112 -6
View File
@@ -3,6 +3,7 @@
Based on ADR-009: CLI Framework using Typer.
"""
import os
import sys
from pathlib import Path
from typing import TYPE_CHECKING, Annotated, Any
@@ -249,6 +250,13 @@ def _print_basic_help() -> None:
"""Print a lightweight help message without heavy imports."""
typer.echo("CleverAgents - AI-powered development assistant (actor-first)")
typer.echo("Usage: cleveragents [OPTIONS] COMMAND [ARGS]...")
typer.echo("\nGlobal options:")
typer.echo(" --data-dir PATH Override global data directory")
typer.echo(" --config-path PATH Override global configuration file path")
typer.echo(" -v Increase log verbosity (repeatable)")
typer.echo(" --format -f FMT Output format: rich/color/table/plain/json/yaml")
typer.echo(" --version Show version and exit")
typer.echo(" --help -h Show this message and exit")
typer.echo("\nCommon commands:")
typer.echo(" project Project management")
typer.echo(" actor context Actor context management")
@@ -272,6 +280,21 @@ def _print_basic_help() -> None:
)
# ---------------------------------------------------------------------------
# Verbosity (-v) → log level mapping (ADR-021 §Global CLI Flags)
# ---------------------------------------------------------------------------
# Mapping: 0 = silent (CRITICAL), 1 = ERROR, 2 = WARNING, 3 = INFO,
# 4 = DEBUG, 5+ = DEBUG (TRACE not available in Python stdlib).
_VERBOSITY_LOG_LEVELS: tuple[str, ...] = (
"CRITICAL", # 0 — no -v flag (silent)
"ERROR", # 1 — -v
"WARNING", # 2 — -vv
"INFO", # 3 — -vvv
"DEBUG", # 4 — -vvvv
"DEBUG", # 5+ — -vvvvv (TRACE mapped to DEBUG)
)
def version_callback(value: bool) -> None:
"""Handle --version flag."""
if value:
@@ -316,20 +339,98 @@ def main_callback(
),
),
] = OutputFormat.RICH,
data_dir: Annotated[
Path | None,
typer.Option(
"--data-dir",
help=(
"Override the global data directory for this invocation "
"(database, caches, sessions, logs). "
"Overrides CLEVERAGENTS_DATA_DIR and core.data-dir config key."
),
metavar="PATH",
),
] = None,
config_path: Annotated[
Path | None,
typer.Option(
"--config-path",
help=(
"Override the global configuration file path for this invocation. "
"Overrides CLEVERAGENTS_CONFIG_PATH and the default config location."
),
metavar="PATH",
),
] = None,
verbose: Annotated[
int,
typer.Option(
"-v",
count=True,
help=(
"Increase log verbosity (repeatable). "
"No flag = silent; -v = ERROR; -vv = WARN; "
"-vvv = INFO; -vvvv = DEBUG; -vvvvv = TRACE."
),
),
] = 0,
) -> None:
"""CleverAgents - AI-powered development assistant."""
# Suppress debug-level logs on stdout for ALL commands so machine-readable
# output formats (json, yaml, plain) receive clean stdout. Commands that
# need verbose logging can override this after parsing --log-level flags.
from cleveragents.config.logging import configure_structlog
configure_structlog(log_level="WARNING")
# -----------------------------------------------------------------------
# Validate and wire --data-dir
# -----------------------------------------------------------------------
if data_dir is not None:
resolved_data_dir = data_dir.resolve()
if resolved_data_dir.exists() and not resolved_data_dir.is_dir():
get_err_console().print(
f"[red]Error: --data-dir '{data_dir}' exists but is not"
" a directory.[/red]"
)
raise typer.Exit(1)
# Wire: set env var so Settings and other components pick it up.
# Settings.__new__ reads CLEVERAGENTS_DATA_DIR on first access, so
# setting the env var before any Settings-reading code runs is the
# correct (and non-destructive) way to propagate the CLI override.
os.environ["CLEVERAGENTS_DATA_DIR"] = str(resolved_data_dir)
# -----------------------------------------------------------------------
# Validate and wire --config-path
# -----------------------------------------------------------------------
if config_path is not None:
resolved_config_path = config_path.resolve()
if not resolved_config_path.exists():
get_err_console().print(
f"[red]Error: --config-path '{config_path}' does not exist.[/red]"
)
raise typer.Exit(1)
if not resolved_config_path.is_file():
get_err_console().print(
f"[red]Error: --config-path '{config_path}' is not a file.[/red]"
)
raise typer.Exit(1)
# Wire: set env var so ConfigService instances created later use this path
os.environ["CLEVERAGENTS_CONFIG_PATH"] = str(resolved_config_path)
# -----------------------------------------------------------------------
# Configure log verbosity from -v count (ADR-021 §Global CLI Flags)
# -----------------------------------------------------------------------
log_level = _VERBOSITY_LOG_LEVELS[min(verbose, len(_VERBOSITY_LOG_LEVELS) - 1)]
configure_structlog(log_level=log_level)
_register_subcommands()
# Store the selected output format in the Typer context so all subcommands
# can read it via ctx.obj["format"] without needing their own --format flag.
# -----------------------------------------------------------------------
# Store all global options in ctx.obj for subcommand access
# -----------------------------------------------------------------------
ctx.ensure_object(dict)
ctx.obj["format"] = fmt.value
ctx.obj["data_dir"] = str(data_dir.resolve()) if data_dir is not None else None
ctx.obj["config_path"] = (
str(config_path.resolve()) if config_path is not None else None
)
ctx.obj["verbose"] = verbose
@app.command()
@@ -760,8 +861,13 @@ def main(args: list[str] | None = None) -> int:
return 130
except Exception as e:
from cleveragents.core.error_handling import classify_error, wrap_unexpected
from cleveragents.shared.redaction import redact_value
err_console = get_err_console()
# Always print the original exception type/message to stderr so that
# actionable details (e.g. "No such option: --flag") remain visible
# even when the log level is set to CRITICAL (structlog suppressed).
err_console.print(f"[dim]{type(e).__name__}: {redact_value(str(e))}[/dim]")
safe = wrap_unexpected(e)
info = classify_error(safe)
err_console.print(