scidex.atlas.artifact_commit.commit_artifact (read at lines 171–280 of
scidex/atlas/artifact_commit.py) currently signs every commit with a
hard-coded author_email='scidex-agent@users.noreply.github.com' and
author_name='SciDEX Agent'. The git trailer carries no skill name, no
Orchestra task id, no LLM provider account. This breaks the user-mandated
"every commit-via-agent attributed to skill+task+account" requirement and
makes post-incident root-cause impossible (cf. the SciDEX wiki mermaid
fence-stripping incident memory — without per-commit attribution we
identify offending campaigns by guessing).
This task adds three signed trailers to every artifact commit, persists the
same triple into a new artifact_commit_attribution table, and audits the
historical commit log to emit a "% commits attributed" governance metric.
scidex/atlas/artifact_commit.py:task_id: str | None = None,skill: str | None = None,account: str | None = None.Task-Id: <id>, Skill: <name>,Account: <name>.Co-Authored-By: for the model name (already aSCIDEX_AGENT_MODEL →Co-Authored-By: <model> <noreply@anthropic.com>).Attribution: incomplete; missing=[...] soscidex.core.attribution.discover() returns{task_id, skill, account, model} by reading, in order:ORCHESTRA_TASK_ID, SCIDEX_SKILL,SCIDEX_ACCOUNT, SCIDEX_AGENT_MODEL. All four are commonlymigrations/20260428_artifact_commit_attribution.sql:CREATE TABLE artifact_commit_attribution (
commit_sha text PRIMARY KEY,
submodule text NOT NULL,
task_id text,
skill text,
account text,
model text,
artifact_ids text[],
committed_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX idx_aca_task ON artifact_commit_attribution(task_id);
CREATE INDEX idx_aca_skill ON artifact_commit_attribution(skill); commit_artifact writes one row per successful commit (extracted
from the existing _log_event callsite at lines 259–266; piggyback
on the current event row writing if one already lands in a SQL
table, otherwise add a new write).
scripts/backfill_commit_attribution.py:data/scidex-artifacts/ anddata/scidex-papers/ (git log --pretty=format:%H%n%B%n--END--),Task-Id: / Skill: / Account: trailers that(total, fully_attributed, partial, anonymous).
scidex/senate/governance.py (or whichever module emits theattribution_coverage() -> float =fully_attributed / total over the trailing 30 days, and surfaceq-perc-edit-pr-bridge alreadytask_id via the commit message; this task addsskill='senate.edit_emitter' and the account fromattribution.discover().
tests/test_attribution.py:Attribution: incomplete trailer.discover() reads env vars; missing env → all-None.commit_artifact signature minimally so existing callersattribution.discover() and a unit test pinning the env-varcommit_artifact after the existing _log_event call.
q-perc-edit-pr-bridge — first real consumer that supplies theq-gov-metrics-dashboard — surfaces the 30d coverage KPI.scidex/core/attribution.py: Attribution dataclass + discover() readingORCHESTRA_TASK_ID, SCIDEX_SKILL, SCIDEX_ACCOUNT, SCIDEX_AGENT_MODEL.
commit_artifact(): added task_id, skill, account, model,artifact_ids kwargs. When all three required fields present → appends trailersTask-Id:, Skill:, Account:) + Co-Authored-By:. When incomplete →Attribution: incomplete; missing=[...] (backwards-compatible).
_log_attribution() (best-effort write to artifact_commit_attribution).migrations/20260428_artifact_commit_attribution.sql: table + indexes.scripts/backfill_commit_attribution.py: walks git log, parses trailers,total=10 fully_attributed=0 partial=2 anonymous=8.
attribution_coverage(days=30) -> float to scidex/senate/governance.py.tests/test_attribution.py: 13 tests covering discover(), format_trailers(),orchestra/task/556fc0a6-attribution-audit-every-artifact-commit.Result: PASS
Verified by: MiniMax-M2.7 via task 556fc0a6-8fd4-4572-a097-8646787453c3
The implementation is new — no prior SHA to attribute. Current state is produced by:
f32f1cc4e — [Atlas][Senate] Add per-commit attribution to artifact commitsartifact_commit_attribution table applied to DB via Python (migration_runner not available from worktree).attribution_coverage() returns 0.0 until the backfill runs and populates historical rows.task_id, skill, account from attribution.discover() to commit_artifact() when writing artifacts from Orchestra-launched agents.