ADR-335: Preserve unknown frontmatter keys in PipelineContext round-trip - #63
Conversation
…ed keys save()/load() now read/write the full YAML frontmatter block as a dict via a new extra_frontmatter field, so unrecognized keys (working_branch, base_branch, parent_work_item, and future fields) survive every dev_team.py invocation unchanged instead of being silently dropped. Handles both scalar and multi-line list-valued unknown keys.
build-and-test: Python test resultsStatus: ✅ Passed Test log |
jodavis-claude
left a comment
There was a problem hiding this comment.
Review summary — ADR-335
Reviewed against the task brief's exit criteria, CONTRIBUTING.md, and _doc_Projects.md.
Exit criteria — all met:
PipelineContext.save()/load()now read/write the full frontmatter block generically:KNOWN_FRONTMATTER_KEYSnames every typed field, andextra_frontmatter(populated inload(), merged back insave()) preserves everything else — including list-valued unknown keys via the existing_parse_frontmatter()multi-line- itemsupport.working_branch/base_branch/parent_work_itemnow survive adev_team.pyround-trip, verified generically (not special-cased) byTestExtraFrontmatterRoundTrip.- Unit tests cover both the "unknown key written directly via
Edit" case (scalar + list) and the "pre-populatedextra_frontmatterbefore a run" case (base_branch). - Full regression: all 111 tests in
test_dev_team.pypass locally, andscripts/validate.sh(build + tests) passes clean. CI (build-and-test,gate) is green on the PR.
Correctness/fault tolerance: No swallowed exceptions introduced; the existing try/except (KeyError, ValueError) around started/last_updated parsing is untouched. The extra_frontmatter merge is a straightforward dict comprehension with no failure modes on the inputs _parse_frontmatter can actually produce (str or list[str]).
Security / performance: No injection risk (local file I/O only, no shell/SQL boundary touched), no N+1 or synchronous-I/O concerns — this is a small, local, single-pass dict merge.
Documentation: No _doc_*.md describes PipelineContext's internals at this level of detail, and this is a bug fix restoring already-intended behavior (per the owning spec's Component Breakdown), not a design change — no doc update needed. Scope matches the brief exactly: only dev_team.py and test_dev_team.py touched, confirmed no other file references PipelineContext.
Minor, non-blocking observations (not requesting changes):
extra_frontmatter: dict = field(default_factory=dict)could carry the more precise type hintdict[str, str | list[str]]the brief suggested, for a little extra self-documentation.- Edge case not covered by a test: if
extra_frontmatteris ever populated with an empty list value (not reachable from any current code path —_parse_frontmatternever produces an empty list, and no caller constructs one),save()would write a barekey:line with no- itembullets, and a subsequentload()would read that back as an empty string rather than an empty list, silently changing its type. Worth a one-line guard or a comment if this ever becomes reachable; not a real bug today since nothing produces this input. KNOWN_FRONTMATTER_KEYSis a mutableset(); afrozensetwould better signal it's a fixed constant. Purely stylistic.
No Priority 1–4 issues found. Approving (posted as COMMENT rather than APPROVE since the PR author and reviewer share this bot account).
…overage — all known frontmatter fields, extra_frontmatter (scalar+list), every known body section (including Project Configuration ordering and numbered Fix sections), the write-only Logs section, and started/last_updated parse-failure fallback (missing key and malformed value); 23/23 tests passing)
Removes the now-dead inline PipelineContext class and KNOWN_FRONTMATTER_KEYS set from dev_team.py; dev_team.py now imports PipelineContext from pipeline_context.py so existing `from dev_team import PipelineContext` call sites keep working unchanged. Kept dev_team.py's own _parse_frontmatter/_parse_sections helpers in place since they're still used by unrelated code. Also fixes a bug in pipeline_context.py's load(): its hand-rolled frontmatter parser treated any empty-value scalar key (e.g. `spec_path: `) as the start of a YAML list, returning [] instead of "". This wasn't caught by the component's own tests (which only round-tripped non-empty scalar values) but broke test_dev_team.py once dev_team.py started using the shared implementation. Added a red test demonstrating the round-trip failure, then fixed the parser to only convert to a list once an actual '- ' item line follows, matching dev_team.py's existing _parse_frontmatter semantics.
…adata Addresses PR #63 review feedback worried about the hand-maintained frontmatter-key list drifting as fields are added. Tags each frontmatter-backed field with metadata={"frontmatter": True} inline in the dataclass definition, then derives KNOWN_FRONTMATTER_KEYS via dataclasses.fields() instead of a separately maintained set literal. Adding a new frontmatter field now only requires touching its field() call, not a second list elsewhere in the module. Verified the derived set is identical to the prior hand-written one; all 135 existing tests (pipeline_context + dev_team) stay green.
jodavis-claude
left a comment
There was a problem hiding this comment.
Sign-off review -- ADR-335
Checked both of jodavis's prior threads against the three follow-up commits (a4d7d7c, 64b6323, eda325f) and scanned the modified file set (pipeline_context.py (new), dev_team.py, test_pipeline_context.py (new)).
Thread: "hand-maintained KNOWN_FRONTMATTER_KEYS drift" -- resolved. eda325f derives KNOWN_FRONTMATTER_KEYS from dataclasses.fields(PipelineContext) filtered on a metadata={"frontmatter": True} tag on each frontmatter-backed field, instead of a hand-maintained set literal. Verified the derived set matches the old hand-written one and all 135 tests stay green. Resolving this thread.
Thread: "extract PipelineContext + rebuild save()/load() via TDD" -- real work, but not fully behavior-preserving; leaving open. PipelineContext is genuinely extracted into pipeline_context.py, dev_team.py now imports it instead of defining it inline (with _parse_frontmatter/_parse_sections correctly left in dev_team.py since they're still used there for unrelated PR URL section parsing), and 23 new tests in test_pipeline_context.py give real, thorough coverage of frontmatter fields, extra_frontmatter (scalar + list), every body section including the new project_configuration (first-section ordering), the write-only Logs section, and started/last_updated parse-failure fallback. 64b6323 also fixed a genuine empty-scalar-vs-list frontmatter parsing bug the extraction surfaced, with its own red test. Full regression: 135/135 tests pass locally, CI (build-and-test, gate) is green.
However, comparing the rebuilt save() line-by-line against the pre-rebuild implementation, two behaviors present in the original hand-written code did not make it into the TDD rebuild (see inline comments): save() no longer creates its target directory if missing, and no longer bumps last_updated on every save. Both were previously untested, which is presumably how they were dropped without a failing test to catch it -- but "rebuild via TDD so it's thoroughly covered" should mean at least parity with the prior implementation's behavior, not just the behaviors explicitly named in the task brief. Also noted a minor, non-blocking cosmetic loss (dropped markdown heading on fresh-file save).
Given the two real regressions, requesting changes rather than signing off yet.
PipelineContext.save() no longer called path.parent.mkdir(parents=True, exist_ok=True) before write_text() after the TDD rebuild, reintroducing an unhandled FileNotFoundError when saving to a path whose parent doesn't already exist. This was never test-covered before or after the rebuild (every prior test wrote to tmp_path, which already exists). Restored the mkdir call and added test_save_to_path_with_missing_parent_directory_creates_it_and_writes_file to pin down the behavior going forward.
PipelineContext.save() no longer set self.last_updated = datetime.datetime.now() at the top of the method after the TDD rebuild, so last_updated stayed frozen at construction time instead of tracking "last saved" as it did before the rebuild. No test pinned this down in either version. Restored the bump and added test_save_updates_last_updated_to_current_time_on_each_call to cover it.
Cosmetic fix from PR #63 review: the pre-rebuild save() wrote a "# {work_item_id} Dev Team Context" heading right after the frontmatter block. Nothing parses it back (load() never reads it), but dev_team.py's fresh-context branch creates a file directly through PipelineContext.save() without going through init-context-file.py's template, so a context file created that way was missing the heading after the rebuild. Restored the heading and added test_save_writes_work_item_id_heading_after_frontmatter.
jodavis-claude
left a comment
There was a problem hiding this comment.
Sign-off review — ADR-335 (second pass)
Re-reviewed PR #63 after the three commits addressing the two blocking regressions and one
cosmetic issue flagged on the prior pass.
Prior threads — all now resolved:
save()no longer creating the parent directory — fixed in 884b6fc.path.parent.mkdir(parents=True, exist_ok=True)restored immediately beforewrite_text(), matching pre-rebuild behavior exactly. Regression testtest_save_to_path_with_missing_parent_directory_creates_it_and_writes_filegenuinely exercises a missing-parent path and was confirmed failing (FileNotFoundError) before the fix per the commit message.save()no longer bumpinglast_updated— fixed in 91bcdf4.self.last_updated = datetime.datetime.now()restored at the top ofsave(). Regression testtest_save_updates_last_updated_to_current_time_on_each_callsleeps briefly then asserts the value actually advanced — a real assertion, not a tautology.- Dropped markdown heading (cosmetic) — fixed in e2be40d.
# {work_item_id} Dev Team Contextrestored after the frontmatter block, matchingcontext_template.md's format.test_save_writes_work_item_id_heading_after_frontmatterpins it down; confirmed still inert w.r.t.load()/_parse_sections(). - Overarching "extract + TDD rebuild" thread — with both regressions and the cosmetic gap closed,
PipelineContextis now genuinely extracted into its own module (pipeline_context.py) with full TDD coverage and no known behavior gap versus the pre-rebuild implementation.
Scope of this pass: only pipeline_context.py and test_pipeline_context.py were touched by the three new commits (confirmed via git diff 884b6fc~1..HEAD --stat). No other file changed, so no other file needed re-scanning.
New issues found: none. Each fix is minimal, restores the exact pre-rebuild behavior, and ships with a dedicated regression test written TDD-first (per the commit messages, each test was confirmed failing before its fix). Test structure follows this repo's conventions (AAA, make_sut(), descriptive parametrize ids). Full suite passes locally: 138/138 (27 test_pipeline_context.py + 111 test_dev_team.py). CI (build-and-test, gate) is green on the PR.
Sign-off: approved.
|
Both review threads have been addressed and resolved:
138/138 tests pass, CI is green. Re-requesting review — ready for another look whenever you have time. |
Remove the inaccurate comment above _parse_sections claiming PipelineContext is "re-exported here for backward compatibility" -- PipelineContext was never exported from dev_team.py before the extraction, so there is no such compatibility concern. The "Pipeline context (serializable state)" section divider is removed too since PipelineContext no longer lives in this file at all, and the only function under that header (_parse_sections) is an unrelated body-parsing helper used by dev_team.py's own Step-class logic. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011oezrcK3g7maGFvxDkXALv
…ipeline_context.py Delete two test classes from test_dev_team.py whose entire purpose is exercising PipelineContext's own save()/load() persistence, now fully covered by test_pipeline_context.py: - TestExtraFrontmatterRoundTrip: both its scalar/list-valued unknown-key round-trip test and its prepopulated extra_frontmatter test are byte-for-byte duplicated by TestPipelineContextExtraFrontmatter in test_pipeline_context.py. - TestTroubleshooterInput: its default-value, non-empty round-trip, and empty-string round-trip assertions are all covered generically by TestPipelineContextDefaults and TestPipelineContextSaveLoadRoundTrip in test_pipeline_context.py (troubleshooter_input is included in both the known-scalar-fields and default-empty-string-fields parametrized coverage there). Other classes that construct a PipelineContext (TestSignoffCycleCount, TestReviewCycleCount, TestConsecutiveFailures, TestSignoffBuildResult, TestReviewStepPrUrlExtraction, and the various Step-class test classes) were left untouched -- each of those primarily exercises dev_team.py's own logic (_apply_counter_updates, _handle_agent_failure/_success, ReviewStep's removed PR-URL-section fallback, Step class instantiation) and is not a genuine duplicate of test_pipeline_context.py's coverage. Full regression: 105/105 test_dev_team.py tests and 27/27 test_pipeline_context.py tests pass (132 total, down from 138 before this cleanup, reflecting the 6 removed duplicate tests). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011oezrcK3g7maGFvxDkXALv
Work item
ADR-335: Fix
PipelineContext.save()/load()indev_team.pyso a context file's full YAML frontmatter block round-trips as a dict — preserving any keyPipelineContextdoesn't itself declare as a typed field — instead of silently dropping every such key (working_branch,base_branch,parent_work_item, and every new field later tasks in the Concurrent Development spec will add) on everydev_team.pyinvocation.Changes
plugins/dev-team/skills/workflow-orchestrate/scripts/dev_team.py— added a module-levelKNOWN_FRONTMATTER_KEYSconstant listing every frontmatter keyPipelineContextdeclares as a typed field, and a newextra_frontmatter: dictdataclass field (default{}).load()now populatesextra_frontmatterfrom every parsed frontmatter key not inKNOWN_FRONTMATTER_KEYS.save()now writes eachextra_frontmatterentry back after the typed-field lines — as a scalarkey: valueline, or as akey:line followed by- itembullets for list-valued entries — reusing the existing_parse_frontmatter()/_parse_sections()helpers unchanged.plugins/dev-team/skills/workflow-orchestrate/scripts/test_dev_team.py— addedTestExtraFrontmatterRoundTripwith amake_sut()factory, a parametrizedtest_unknown_field_survives_load_save_roundtrip(scalarworking_branchcase and list-valuedextra_listcase), andtest_prepopulated_extra_frontmatter_field_survives_save_load_roundtrip(constructs aPipelineContextwith a pre-populatedextra_frontmatter={"base_branch": "main"}, simulating a value set before aworkflow-orchestraterun, and asserts it survives asave()/load()cycle).Design decisions
known_keysset into the module-levelKNOWN_FRONTMATTER_KEYSconstant to remove drift risk against the field listsave()writes, and consolidated the scalar/list-valued round-trip tests into one parametrized test.missing-test-harnessskill, no E2E harness exists anywhere in this repo for Python code such asdev_team.py, and the task brief's exit criteria call only for unit tests.Testing completed
python3 -m pytest test_dev_team.py -qfromplugins/dev-team/skills/workflow-orchestrate/scripts/— all 111 tests pass (109 pre-existing + 2 new test methods, one parametrized), confirming every previously-passing known-field round-trip is unaffected.