Skip to content

feat: config-driven contract + concurrent-writer safety (rebased on main)#202

Merged
labgadget015-dotcom merged 2 commits into
mainfrom
feat/config-driven-contract-v2
Jul 17, 2026
Merged

feat: config-driven contract + concurrent-writer safety (rebased on main)#202
labgadget015-dotcom merged 2 commits into
mainfrom
feat/config-driven-contract-v2

Conversation

@labgadget015-dotcom

Copy link
Copy Markdown
Owner

Same work as #189, superseded because the previous branch had diverged from main and could not be fast-forwarded. This branch is built on the current tip of main (no conflicts).

What it does

Closes out the major/minor findings the dual code review flagged but deferred from #188.

1. Config drift eliminated (major)

config.yaml blocks were declared but never read — ledger.py/recommendation_contract.py/message_formatter.py hardcoded everything. New config_loader.py reads config.yaml via PyYAML (stdlib fallback), so debounce windows, status tags, max_step_len, and require_* severity sets are now config-driven.

2. Concurrent-writer safety (major)

_append_with_seq() holds an exclusive fcntl.flock across seq-assignment + write, with fsync per write. POSIX-only; no-op fallback elsewhere.

3. Status validation (minor)

transition() raises ValueError on unknown statuses (e.g. assiged typo) via VALID_STATUSES.

Verification (run on this branch)

  • PYTHONPATH=autopilot python3 autopilot/tests/test_recommendation_contract.py → 14/14 passed
  • pytest tests/unit → 1001 passed, 0 failed

Supersedes #189.

AgentMedia1942 and others added 2 commits July 16, 2026 07:59
Addresses the deferred review findings from PR #180/#188:

1. [MAJOR] config.yaml blocks are now read (were declared but never used).
   New config_loader.py reads config.yaml via PyYAML with stdlib fallback,
   so debounce windows, status tags, max_step_len, and require_* sets are
   config-driven instead of hardcoded. Silent config drift eliminated.

2. [MAJOR] Concurrent-writer safety: ledger _append_with_seq() now holds an
   exclusive fcntl.flock across seq-assignment + write, preventing the
   _next_seq race and large-entry interleaving between the recommender
   (record) and executor (transition) agents. fsync per write. POSIX-only;
   no-op fallback on non-fcntl platforms.

3. [MINOR] Status validation: transition() now raises ValueError on unknown
   statuses (e.g. 'assiged' typo) via VALID_STATUSES, so a typo can't
   silently break de-dup.

Tests: 14/14 passing (added 4: config-overrides-max_step_len,
config-overrides-debounce, transition-rejects-unknown-status,
concurrent-writers-do-not-lose-entries with 8x25 parallel writes).
Copilot AI review requested due to automatic review settings July 17, 2026 07:42
@labgadget015-dotcom
labgadget015-dotcom merged commit 01d5764 into main Jul 17, 2026
1 check passed
@labgadget015-dotcom
labgadget015-dotcom deleted the feat/config-driven-contract-v2 branch July 17, 2026 07:42
@labgadget015-dotcom

Copy link
Copy Markdown
Owner Author

🤖 DRC Agent Analysis

Recommendation: 🟠 P1 IMPORTANT

Summary: Config Schema Validation Layer (Dreamer ID 2) as recommended by REALIST, with Merge & Harden In-Place (Dreamer ID 1) as prerequisite

Next steps:

  1. Step 1: Confirm CI is green on PR feat: config-driven contract + concurrent-writer safety (rebased on main) #202 and squash-merge to main — this is a hard prerequisite and should take under 30 minutes
  2. Step 2: Audit existing requirements.txt or pyproject.toml for Pydantic version pins; run pip install pydantic>=2.0 --dry-run in a clean venv to confirm no conflicts before writing any code
  3. Step 3: Create autopilot/config_schema.py with a Pydantic v2 BaseModel covering all config.yaml keys — use Optional with defaults for non-critical fields, strict required+range for safety-critical ones
  4. Step 4: Wrap config_loader.py load path with ConfigSchema(**parsed_dict) inside try/except ValidationError, raising ConfigValidationError with human-readable field diff on failure
  5. Step 5: Write 8-10 unit tests in autopilot/tests/test_config_schema.py covering valid load, missing required key, wrong type, and out-of-range numeric value

Strategic fit: Consulting: high · Product: high · Tech debt: reduces


Analysed by GadgetLab DRC Agent (Dreamer → Realist → Critic) · Run run_1784274130527

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR makes the autopilot recommendation pipeline behave according to autopilot/config.yaml (instead of hardcoded constants) and adds file-level locking to make the decisions ledger safe under concurrent writers, plus validation to prevent unknown status typos from silently breaking de-duplication.

Changes:

  • Added autopilot/config_loader.py and wired config-driven values into the contract validator, ledger debounce policy, and message status tags.
  • Implemented POSIX fcntl.flock-based exclusive locking for seq assignment + JSONL appends to prevent concurrent write races.
  • Added status validation (VALID_STATUSES) so transition() rejects unknown lifecycle statuses.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
autopilot/config_loader.py New cached loader with defaults + optional PyYAML parsing to drive runtime behavior from config.yaml.
autopilot/decisions/ledger.py Adds file locking + fsync per append; moves debounce windows and ledger path to config; rejects unknown statuses.
autopilot/recommendation_contract.py Reads validation rules (max step length / required fields by severity) from config instead of hardcoding.
autopilot/message_formatter.py Uses config-driven status tags (with defaults) when rendering Slack messages.
autopilot/tests/test_recommendation_contract.py Adds regression tests for config overrides, unknown status rejection, and concurrent writer safety.
autopilot/README.md Updates module listing to reflect new config loader and ledger locking behavior.

Comment on lines 44 to 48
@@ -46,11 +48,14 @@ def _load(path: str = DEFAULT_LEDGER_PATH) -> list[dict]:
with open(path, encoding="utf-8") as f:
Comment on lines +21 to 25
# Fallback tags if config is unavailable; live values come from config.yaml.
_STATUS_TAG_FALLBACK = {
"open": "🟡 Open",
"assigned": "🔵 Assigned",
"inflight": "🟠 In-flight",
Comment on lines 82 to 85
# P0 must have an owner; else withhold + escalate
if r.severity == "P0" and r.owner == "unassigned":
if r.severity in require_owner_for and r.owner == "unassigned":
errs.append("P0 with no owner — withhold, escalate to #morning-digest")

Comment on lines +149 to +169
def test_config_overrides_max_step_len():
"""validate() should honour max_step_len from a custom config file."""
import config_loader

config_loader.reset_cache()
cfg = tempfile.NamedTemporaryFile(
mode="w", suffix=".yaml", delete=False, encoding="utf-8"
)
cfg.write("message_contract:\n max_step_len: 20\n")
cfg.close()
try:
config_loader.DEFAULT_CONFIG_PATH = cfg.name
config_loader.reset_cache()
r = _good()
r.steps[0] = "x" * 25 # over the custom 20 limit, under default 120
ok, errs = validate(r)
assert not ok
assert any("step 1" in e and "20" in e for e in errs)
finally:
config_loader.reset_cache()
os.unlink(cfg.name)
Comment on lines +176 to +200
config_loader.reset_cache()
cfg = tempfile.NamedTemporaryFile(
mode="w", suffix=".yaml", delete=False, encoding="utf-8"
)
cfg.write(
"recommendation_debounce:\n"
" repost_policy:\n"
" open: {min_hours_between: 100000, never_repost: false}\n"
)
cfg.close()
ledger = tempfile.NamedTemporaryFile(suffix=".jsonl", delete=False)
ledger.close()
try:
config_loader.DEFAULT_CONFIG_PATH = cfg.name
config_loader.reset_cache()
r = _good()
from decisions.ledger import record, should_post

record(r, ledger.name)
allow, reason = should_post(r, ledger.name)
assert allow is False, reason # 100000h window always suppresses
finally:
config_loader.reset_cache()
os.unlink(cfg.name)
os.unlink(ledger.name)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants