Brief reads every fact from the Implementation aggregate; project config binds at open (was: brief ignores target) #534

Closed
opened 2026-09-08 17:13:55 +00:00 by jared · 4 comments
Owner

Brief reads every fact from the Implementation aggregate; project config binds at open

Reframed 2026-09-08 from "programmer brief ignores implementation target". The target drop in BriefFacts#project is one symptom of a class-level rule violation: the brief queries tables, loads yaml, and shells out to git on its own instead of asking the models that own those facts. Every fact a brief needs already exists before the brief is built. The brief's only job is to read and decorate them.

Symptom (original report)

Live run of /os-sdlc:implement #525 (session f3ac4424, 2026-09-08). Programmer SubagentStart hook raised TargetSelectionError at plugins/os-sdlc/lib/os_sdlc/runner/brief_facts.rb:134 (Project.load(@project_root) with no target in a multi-target project). No dispatch row was written; the stop settled against a stale dispatch from 2026-09-04. Hot-patched in-run with sed, restored after.

Root cause

  • BriefFacts#project calls Project.load without the target the implementation row already holds (implementations.target, ADR-0139). Written in d4c0d4c after multi-target support existed; every test on this path uses a flat single-target fixture, so the single-target fast path hid it.
  • Four bare Project.load(root) calls exist in the runner path: brief_facts.rb:133, autocorrect_prepass.rb:95, tea_tracker.rb:146, scope_policy.rb:15. Any of them fails the same way.
  • Project.load(dir, target: nil) has an optional keyword with a silent fallback, which invites the omission.

Design (decided with the user, 2026-09-08)

  1. Project config binds at open and persists on the row. OpenImplementation parses .sdlc/project.yaml once with Project.load(root, target:) (keyword becomes required) and stores the resolved attribute set as JSON in a new implementations.project_config text column. Relative paths only; root is never stored (ADR-0055) and stays derived from cwd. Implementation#project rebuilds the PORO via Project.from_json(project_config). Config is frozen for the life of the implementation, same rule as the map (ADR-0160). Schema bump to 21 (ADR-0129 wipe-and-recreate).
  2. No Project.load outside implementation-open. The four bare call sites read implementation.project. The required keyword makes a bare call a load-time error.
  3. BriefFacts asks, never queries. Constructor becomes BriefFacts.new(implementation). Drop tracker: (ticket body is on the row since ADR-0146; the fallback is a fossil) and project_root:/db: (models know their datasets).
    • ticket_brief -> implementation.ticket_body
    • gate_results -> implementation.latest_dispatch&.step_summaries || {}
    • interpreted_behavior / probe_evidence / seam -> Handoff.latest(implementation, from: agent_type)&.content
    • lint_offense_history -> StepResult.lint_failures_for(implementation) formatted by an extracted LintOffenseLine (the nested LintFailureRow class moves out)
    • test_changes -> TestDiff.new(implementation.project).to_s, the one live message (git diff), in its own named class
    • Truncation at MAX_GATE_OUTPUT_CHARS stays in the brief: prompt budget is a brief concern.
  4. Tests. Runner-path tests (subagent-start, brief facts, gate commands) run against the multi-target fixture (write_multi_target_project_yaml) as well as the flat one. Add a test that a SubagentStop for implementation B cannot settle against implementation A's pending dispatch (the stale-dispatch half of the original symptom).

Out of scope

  • #535 diff-test gate scenario-glob fallback (separate ticket).
  • Any change to routing, landing, or the ledger.

ADRs

  • Extends ADR-0139 (target binds at open) and ADR-0146 (ticket body binds at open). No reversal found by /os-adr:find on 2026-09-08.
  • Constraints: ADR-0055 (root never stored), ADR-0074/0088 (attribute set and target resolution), ADR-0129 (schema bump), ADR-0137/0153/0162 (facts are runner-owned, read-only, map-requested), ADR-0168 (aggregate is sole interpreter; open logic stays in Implementation.open/OpenImplementation).
  • Record a new ADR before coding: "Resolved project config binds at implementation-open and persists on the implementations row; BriefFacts reads only through the aggregate. Amends ADR-0139/0146."

Sequencing

First phase 5 slice (os-sdlc architecture refactor plan). Land after #535.

## Brief reads every fact from the Implementation aggregate; project config binds at open Reframed 2026-09-08 from "programmer brief ignores implementation target". The target drop in `BriefFacts#project` is one symptom of a class-level rule violation: the brief queries tables, loads yaml, and shells out to git on its own instead of asking the models that own those facts. Every fact a brief needs already exists before the brief is built. The brief's only job is to read and decorate them. ### Symptom (original report) Live run of `/os-sdlc:implement #525` (session f3ac4424, 2026-09-08). Programmer SubagentStart hook raised `TargetSelectionError` at `plugins/os-sdlc/lib/os_sdlc/runner/brief_facts.rb:134` (`Project.load(@project_root)` with no target in a multi-target project). No dispatch row was written; the stop settled against a stale dispatch from 2026-09-04. Hot-patched in-run with sed, restored after. ### Root cause - `BriefFacts#project` calls `Project.load` without the target the implementation row already holds (`implementations.target`, ADR-0139). Written in d4c0d4c after multi-target support existed; every test on this path uses a flat single-target fixture, so the single-target fast path hid it. - Four bare `Project.load(root)` calls exist in the runner path: `brief_facts.rb:133`, `autocorrect_prepass.rb:95`, `tea_tracker.rb:146`, `scope_policy.rb:15`. Any of them fails the same way. - `Project.load(dir, target: nil)` has an optional keyword with a silent fallback, which invites the omission. ### Design (decided with the user, 2026-09-08) 1. **Project config binds at open and persists on the row.** `OpenImplementation` parses `.sdlc/project.yaml` once with `Project.load(root, target:)` (keyword becomes required) and stores the resolved attribute set as JSON in a new `implementations.project_config` text column. Relative paths only; root is never stored (ADR-0055) and stays derived from cwd. `Implementation#project` rebuilds the PORO via `Project.from_json(project_config)`. Config is frozen for the life of the implementation, same rule as the map (ADR-0160). Schema bump to 21 (ADR-0129 wipe-and-recreate). 2. **No `Project.load` outside implementation-open.** The four bare call sites read `implementation.project`. The required keyword makes a bare call a load-time error. 3. **BriefFacts asks, never queries.** Constructor becomes `BriefFacts.new(implementation)`. Drop `tracker:` (ticket body is on the row since ADR-0146; the fallback is a fossil) and `project_root:`/`db:` (models know their datasets). - `ticket_brief` -> `implementation.ticket_body` - `gate_results` -> `implementation.latest_dispatch&.step_summaries || {}` - `interpreted_behavior` / `probe_evidence` / `seam` -> `Handoff.latest(implementation, from: agent_type)&.content` - `lint_offense_history` -> `StepResult.lint_failures_for(implementation)` formatted by an extracted `LintOffenseLine` (the nested `LintFailureRow` class moves out) - `test_changes` -> `TestDiff.new(implementation.project).to_s`, the one live message (git diff), in its own named class - Truncation at `MAX_GATE_OUTPUT_CHARS` stays in the brief: prompt budget is a brief concern. 4. **Tests.** Runner-path tests (subagent-start, brief facts, gate commands) run against the multi-target fixture (`write_multi_target_project_yaml`) as well as the flat one. Add a test that a SubagentStop for implementation B cannot settle against implementation A's pending dispatch (the stale-dispatch half of the original symptom). ### Out of scope - #535 diff-test gate scenario-glob fallback (separate ticket). - Any change to routing, landing, or the ledger. ### ADRs - Extends ADR-0139 (target binds at open) and ADR-0146 (ticket body binds at open). No reversal found by `/os-adr:find` on 2026-09-08. - Constraints: ADR-0055 (root never stored), ADR-0074/0088 (attribute set and target resolution), ADR-0129 (schema bump), ADR-0137/0153/0162 (facts are runner-owned, read-only, map-requested), ADR-0168 (aggregate is sole interpreter; open logic stays in `Implementation.open`/`OpenImplementation`). - Record a new ADR before coding: "Resolved project config binds at implementation-open and persists on the implementations row; BriefFacts reads only through the aggregate. Amends ADR-0139/0146." ### Sequencing First phase 5 slice (os-sdlc architecture refactor plan). Land after #535.
jared changed title from Runner programmer brief ignores implementation target in multi-target project to Brief reads every fact from the Implementation aggregate; project config binds at open (was: brief ignores target) 2026-09-08 20:06:48 +00:00
Author
Owner

Reframed 2026-09-08 after auditing session f3ac4424: symptom is one of four bare Project.load calls; design now binds config at open and makes BriefFacts read only through the aggregate. First phase 5 slice, lands after #535.

Reframed 2026-09-08 after auditing session f3ac4424: symptom is one of four bare Project.load calls; design now binds config at open and makes BriefFacts read only through the aggregate. First phase 5 slice, lands after #535.
Author
Owner

Design point 5 (added 2026-09-08, user decision): handoff facts resolve by convention, not by a translation table. Delete HANDOFF_FACT_AGENT_TYPES. In the map, an enrich: entry is either a named fact (ticket_brief, gate_results, lint_offense_history, test_changes) or an agent type; an agent type resolves to Handoff.latest(implementation, from: name)&.content. Map edit: interpreted_behavior -> change-interpreter, probe_evidence -> code-probe, seam -> seam-designer in poodr-implementation.yaml. Map::Validator refuses any enrich name that is neither a named fact nor a member of AgentType::ALL. Brief section headings follow the new names; any agent prompt that cites the old labels gets the same rename.

Design point 5 (added 2026-09-08, user decision): handoff facts resolve by convention, not by a translation table. Delete HANDOFF_FACT_AGENT_TYPES. In the map, an enrich: entry is either a named fact (ticket_brief, gate_results, lint_offense_history, test_changes) or an agent type; an agent type resolves to Handoff.latest(implementation, from: name)&.content. Map edit: interpreted_behavior -> change-interpreter, probe_evidence -> code-probe, seam -> seam-designer in poodr-implementation.yaml. Map::Validator refuses any enrich name that is neither a named fact nor a member of AgentType::ALL. Brief section headings follow the new names; any agent prompt that cites the old labels gets the same rename.
Author
Owner

Child issues

Decomposition of #534 into six ordered children (A→F):

  • #537 A: Project: target keyword required; JSON round-trip
  • #538 B: Schema 21: project_config column; bind at open; Implementation#project
  • #539 C: No Project.load outside open — four call sites
  • #540 D: Fact owners on the models
  • #541 E: BriefFacts asks, never queries
  • #542 F: Handoff facts by convention

Close-out: parent closes when all six children are merged and the multi-target live run passes.

## Child issues Decomposition of #534 into six ordered children (A→F): - **#537 A**: Project: target keyword required; JSON round-trip - **#538 B**: Schema 21: project_config column; bind at open; Implementation#project - **#539 C**: No Project.load outside open — four call sites - **#540 D**: Fact owners on the models - **#541 E**: BriefFacts asks, never queries - **#542 F**: Handoff facts by convention Close-out: parent closes when all six children are merged and the multi-target live run passes.
Author
Owner

Resolution

Done: Project config binds at open and the brief reads every fact from the Implementation aggregate (ADR-0169). Children A-F all merged: #537 Project.load requires target: and JSON round-trips without root; #538 schema 21 stores project_config at open, Implementation#project rebuilds it, stale-dispatch guard; #539 no Project.load in runner/ outside open (Project::Source role, guard test); #540 fact owners on the models; #541 BriefFacts asks by name and drops the tracker fallback; #542 handoff facts by convention with an enrich validator and the alias table deleted.

Evidence: All six children closed 2026-09-09 with merges on main: 1f8bb6c/1e7b6f4 (#537), 164227d/2d7f1c9 (#538), afb5440/b9eb84d (#539), acc1862..10a055a (#540), 1a7189c..6ed104f (#541), 8a05410..c265f9f (#542). Multi-target live run: every child from #537 on was driven by /os-sdlc:implement on this multi-target project with --target os-sdlc; from #539 on the runs used the merged bound-project brief with no hot-patch, and #540, #541 and #542 ran on the schema-21 runner end to end (implementation_complete on #540, #541, #542). Suite 1121 runs green on main c265f9f.

Follow-ups: #544 (test-side conflict routing), #545 (Project.load sites outside runner/), #546 (verifier early-complete and dispatch cap) remain open; none new

## Resolution **Done:** Project config binds at open and the brief reads every fact from the Implementation aggregate (ADR-0169). Children A-F all merged: #537 Project.load requires target: and JSON round-trips without root; #538 schema 21 stores project_config at open, Implementation#project rebuilds it, stale-dispatch guard; #539 no Project.load in runner/ outside open (Project::Source role, guard test); #540 fact owners on the models; #541 BriefFacts asks by name and drops the tracker fallback; #542 handoff facts by convention with an enrich validator and the alias table deleted. **Evidence:** All six children closed 2026-09-09 with merges on main: 1f8bb6c/1e7b6f4 (#537), 164227d/2d7f1c9 (#538), afb5440/b9eb84d (#539), acc1862..10a055a (#540), 1a7189c..6ed104f (#541), 8a05410..c265f9f (#542). Multi-target live run: every child from #537 on was driven by /os-sdlc:implement on this multi-target project with --target os-sdlc; from #539 on the runs used the merged bound-project brief with no hot-patch, and #540, #541 and #542 ran on the schema-21 runner end to end (implementation_complete on #540, #541, #542). Suite 1121 runs green on main c265f9f. **Follow-ups:** #544 (test-side conflict routing), #545 (Project.load sites outside runner/), #546 (verifier early-complete and dispatch cap) remain open; none new
jared closed this issue 2026-09-09 18:54:56 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#534
No description provided.