Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e91bacf
Resolve builder contribution lineage at the exact head
jeffhuber Sep 14, 2026
caae260
Make the Code Mower gate consume the shared lineage resolver
jeffhuber Sep 14, 2026
420198a
Record delivery provenance and reconcile the active builder label
jeffhuber Sep 14, 2026
dbbb3c4
Merge remote-tracking branch 'origin/main' into claude/963-builder-li…
jeffhuber Sep 14, 2026
47d6129
Publish, continue and resolve builder lineage through real consumers
jeffhuber Sep 14, 2026
b12523e
Merge accepted main db4506d2 into claude/963-builder-lineage
jeffhuber Sep 14, 2026
c9d4448
Resolve builder lineage from the pull request, not the local store
jeffhuber Sep 14, 2026
52820a4
Trust gate lineage through decision authorities, not reviewer bots
jeffhuber Sep 14, 2026
181663d
Synchronize vendored lineage tools with their canonical sources
jeffhuber Sep 14, 2026
9d4f1a4
Rename the unused vendored-probe loop variable
jeffhuber Sep 14, 2026
47402a5
Carry the validated head branch through the delivery snapshot
jeffhuber Sep 14, 2026
810baaa
Require a trusted authority wherever lineage is published or read
jeffhuber Sep 14, 2026
cb8b4ad
Render provenance authority from reviewed config, not the proposed head
jeffhuber Sep 14, 2026
82f4dbc
Collapse cumulative replay, and move attribution only on a transition
jeffhuber Sep 14, 2026
c332663
Read the whole comment history, and refuse a marker that will not parse
jeffhuber Sep 14, 2026
48a91ad
Bound the provenance read, and require the labels before reconciling
jeffhuber Sep 14, 2026
c7a1b97
Floor the reviewer's own lane, count configured branch identity, and
jeffhuber Sep 14, 2026
2a1eeca
Validate the lowest comment transport, and finish the runner regression
jeffhuber Sep 14, 2026
8786737
Keep the branch contract through the floor, and refuse ambiguous markers
jeffhuber Sep 14, 2026
47a973b
Run the provenance workflow from the base branch, not the proposal
jeffhuber Sep 14, 2026
2c99b1b
Validate comment records, not just their shape
jeffhuber Sep 14, 2026
42b5fcf
Validate the raw comment source, and tell absence from a present null
jeffhuber Sep 14, 2026
d94827c
Tell a REST comment count from a comment history
jeffhuber Sep 14, 2026
45dbc9d
Validate gate comment pages, refuse an announced empty chain, run on …
jeffhuber Sep 15, 2026
25b9668
Bind the loop value in the trailer history lambda
jeffhuber Sep 15, 2026
d6c6d68
Require every slurped comment page to be an array
jeffhuber Sep 15, 2026
0706c53
Close five lineage admission and label-mutation gaps
jeffhuber Sep 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 80 additions & 16 deletions .github/workflows/code-mower-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -286,10 +286,13 @@ jobs:
attested_non_current_audit_heads,
audit_run_in_flight_detail_for_lane,
audit_verdict_newer_than_in_flight,
episodes_from_comment_body,
flatten_paginated_comments,
flatten_paginated_items,
github_actions_comment_attested,
latest_current_audit_verdict,
latest_current_audit_verdict_detail,
resolve_builder_lineage,
)
except ImportError: # pragma: no cover - unit-test/package fallback
from code_mower.context_review import required_for_checkout
Expand All @@ -298,12 +301,17 @@ jobs:
attested_non_current_audit_heads,
audit_run_in_flight_detail_for_lane,
audit_verdict_newer_than_in_flight,
episodes_from_comment_body,
flatten_paginated_comments,
flatten_paginated_items,
github_actions_comment_attested,
latest_current_audit_verdict,
latest_current_audit_verdict_detail,
resolve_builder_lineage,
)

LINEAGE_MARKER = "CODE_MOWER_BUILDER_LINEAGE"

# github_actions_comment_attested verifies the hidden CODE_MOWER_AUDIT_RUN marker
# and requires comment_id/body_sha256 to match the issue comment being evaluated.
def emit(state, description):
Expand Down Expand Up @@ -342,7 +350,26 @@ jobs:
if isinstance(item, dict)
}
lanes = [lane for lane in lanes if isinstance(lane, dict)]
comments = flatten_paginated_items(comments_payload)
# Comment pages are validated under the shared record contract
# *before* anything flattens, filters or stringifies them. The
# generic flattener drops members it cannot use -- right for a mixed
# timeline, wrong for a comment history, where a dropped comment is a
# dropped marker -- and the `str(... or "")` below would turn a
# present non-string body into plausible text. A history nobody could
# read would otherwise reach the gate looking ordinary. Timeline
# events keep the generic flattener: they are not comments and are
# never read for lineage.
comment_history_readable = True
comment_history_problem = ""
try:
comments = flatten_paginated_comments(comments_payload)
except Exception as exc:
comment_history_readable = False
comment_history_problem = str(exc)
# The gate is already failing on this. Salvaging the readable
# members would hand records nobody could validate to every other
# consumer below, so nothing downstream sees this history at all.
comments = []
events = flatten_paginated_items(events_payload)
audit_runs = audit_runs_payload if isinstance(audit_runs_payload, list) else []
configured_decision_authorities = (
Expand Down Expand Up @@ -432,16 +459,43 @@ jobs:
context_required=context_required,
)

builder_matches = []
if isinstance(exclusion, dict) and exclusion.get("enabled"):
label_map = mapping("labels")
for label in labels:
if label in label_map:
builder_matches.append(str(label_map[label]))
author_map = {str(k).lower(): str(v) for k, v in mapping("authors").items()}
if pr_author.lower() in author_map:
builder_matches.append(author_map[pr_author.lower()])
builder_matches = list(dict.fromkeys(builder_matches))
# Contribution lineage, not one label and one author. The hidden
# marker is a transport and never an authorization, so episodes are
# read only from the repository's configured decision authorities --
# the same trust contract the publisher, both labelers and every
# reviewer wrapper apply. Permission to post an audit verdict is not
# takeover authority, so reviewer bot authors are deliberately not
# consulted here. An unconfigured checkout trusts nobody, reads no
# episodes, and falls back to the ordinary single-builder answer.
lineage_authorities = {
item.strip().lower().lstrip("@")
for item in decision_authorities
if item.strip()
}
lineage_episodes = []
lineage_readable = comment_history_readable
for comment in comments:
comment_body = str(comment.get("body") or "")
if LINEAGE_MARKER not in comment_body:
continue
comment_author = str(((comment.get("user") or {}).get("login")) or "")
if comment_author.strip().lower().lstrip("@") not in lineage_authorities:
continue
try:
lineage_episodes.extend(episodes_from_comment_body(comment_body))
except Exception:
lineage_readable = False
builder_lineage = resolve_builder_lineage(
labels=sorted(labels),
author=pr_author,
config=exclusion if isinstance(exclusion, dict) else {"enabled": False},
repo=os.environ.get("GITHUB_REPOSITORY", ""),
pr_number=pr_number,
branch=str(((pr_payload.get("head") or {}).get("ref")) or ""),
head_sha=head_sha,
episodes=lineage_episodes,
)
builder_matches = list(builder_lineage.contributors)

def lane_display(lane):
return str(lane.get("display_name") or lane.get("id") or lane.get("done") or "")
Expand Down Expand Up @@ -531,8 +585,16 @@ jobs:
)
)

if len(builder_matches) > 1:
emit("failure", "conflicting Code Mower builder identity")
if not lineage_readable:
emit(
"failure",
"Code Mower builder contribution evidence is unreadable"
+ (": " + comment_history_problem if comment_history_problem else ""),
)
elif builder_lineage.status == "conflict":
emit("failure", "conflicting Code Mower builder identity: " + builder_lineage.owner_action)
elif builder_lineage.status == "waiting":
emit("pending", "waiting for builder lineage: " + builder_lineage.owner_action)
elif owner_label in labels:
emit("pending", owner_label + ": waiting on owner")
elif owner_sitting_label and owner_sitting_label in labels:
Expand All @@ -548,11 +610,13 @@ jobs:
elif blocked:
emit("failure", "blocked audit: " + ", ".join(blocked))
else:
excluded_builder = builder_matches[0] if builder_matches else ""
# Every verified contributor to this head is excluded, not just
# the one the active label happens to name.
excluded_builders = set(builder_matches)
required = [
lane
for lane in lanes
if str(lane.get("author_lane") or lane.get("id") or "") != excluded_builder
if str(lane.get("author_lane") or lane.get("id") or "") not in excluded_builders
]
in_flight = in_flight_lanes(required)
missing = [
Expand All @@ -562,7 +626,7 @@ jobs:
or latest_current_verdict(lane) != "done"
]
if not required:
if excluded_builder:
if excluded_builders:
emit("failure", "no independent Code Mower audit lane remains")
else:
emit("failure", "no Code Mower merge-authority lanes configured")
Expand Down
10 changes: 10 additions & 0 deletions code-mower-package-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,11 @@
"source": "src/code_mower/builder_experiment.py",
"target": "src/code_mower/builder_experiment.py"
},
{
"kind": "core",
"source": "tools/builder_lineage.py",
"target": "src/code_mower/builder_lineage.py"
},
{
"kind": "core",
"source": "src/code_mower/builder_runs.py",
Expand Down Expand Up @@ -1372,6 +1377,11 @@
"source": "src/code_mower/provider_runners/github_pr.py",
"target": "src/code_mower/provider_runners/github_pr.py"
},
{
"kind": "reviewer",
"source": "src/code_mower/provider_runners/lineage.py",
"target": "src/code_mower/provider_runners/lineage.py"
},
{
"kind": "reviewer",
"source": "src/code_mower/provider_runners/pr_worktree.py",
Expand Down
Loading