Skip to content

fix(hygiene): untrack the per-run langsmith worker-attempt artifact - #72

Merged
stranske merged 2 commits into
mainfrom
claude/agitated-almeida-13a122
Aug 23, 2026
Merged

fix(hygiene): untrack the per-run langsmith worker-attempt artifact#72
stranske merged 2 commits into
mainfrom
claude/agitated-almeida-13a122

Conversation

@stranske

@stranske stranske commented Aug 23, 2026

Copy link
Copy Markdown
Owner

langsmith-fleet-worker-attempt.json was tracked at the repo root, holding one run's execution
telemetry (operation_role: worker, a resolved selected_model, and the pr_number of whichever PR
last merged — #61 when this branch was cut). stranske/Workflows reusable-codex-run.yml rewrites it
into the checkout root on every agent round, purely to stage the actions/upload-artifact step on the
next line — so the autofix bot re-emitted it on PR #66 against main's copy, an add/add conflict on a
path neither side had a reason to author.

The defect recurred during this branch's own review: #68's merge re-added the file, turning the
rebase into a modify/delete conflict. That is the mechanism demonstrating itself, not a coincidence.

Why this is wrong in this repo's terms, not merely noisy

  • CLAUDE.md §1, tool-vs-evidence. Generic capabilities and gates are committed; this instance's
    evidence is not. One run's telemetry is evidence, and runtime state belongs at $ORCH_STATE_DIR.
  • CLAUDE.md §2, learning-loop integrity. The record carries operation_role: worker and a
    resolved selected_model — exactly the causal execution provenance the learner consumes. That is
    supposed to reach it through feedback.py's tables; a git-churned copy is a second, unmanaged
    store of the same evidence.

Nothing in the tree reads the path. This repo's emitters are langsmith_*.py (underscores), they read
langsmith-fleet.ndjson, and langsmith_fetch extracts to ~/.codex/orchestrator/langsmith-artifacts,
outside the tree.

Both steps are required, and the order is not interchangeable

Verified against a scratch repository:

state git add -A result
ignored + untracked skips it
ignored + tracked stages it anyway

So ignoring alone would have been inert while the path stayed in the index, and untracking alone would
have let the next round re-add it. It also means the runner's git reset HEAD -- denylist is only
load-bearing for paths that are already tracked.

A pattern, and ROOT-ANCHORED

/langsmith-fleet*.json + /langsmith-fleet*.ndjson + artifacts/langsmith/.

A pattern because the runner names the file after the role it recorded, so a verifier or
evaluator attempt arrives as a sibling and escapes a literal.

Root-anchored because bounding by extension is not enough — the first draft of this PR got that
wrong.
Unanchored, a gitignore pattern matches at every depth, so langsmith-fleet*.json also
swallows a contract schema at docs/contracts/schemas/langsmith-fleet-v1.schema.json — a path
stranske/Workflows genuinely tracks, and whose sibling artifact-manifest-v1.schema.json this tree
already keeps. A schema is a .json, so the extension bound does not save it. Caught by a
concurrent session reviewing the upstream half of this fix. The nested CI artifact directory is named
separately for the same reason: anchoring it to the one directory that holds it cannot reach a schema
elsewhere. Same near-miss the vendored-node_modules exception records, same conclusion — the
anchored pattern is the safe one.

Placed above the managed BEGIN/END WORKFLOWS STATUS FILES block: --check is presence-only so
extra patterns pass, but a Template-Version bump rewrites that block and would silently drop the
line. scripts/sync_status_file_ignores.py --check still reports all canonical patterns present, and
no tracked file is shadowed by any pattern (checked with git check-ignore over git ls-files).

Guard

test_repo_artifact_hygiene.py (11 tests) puts every question to git (check-ignore, ls-files)
rather than reimplementing gitignore precedence — .gitignore's own header forbids the alternative,
recording that trailing comments once made every pattern inert and staged 795 files instead of 141.
New prerequisite env_prereq.git_repo_absent() gates on whether git can answer here, never on
.gitignore itself: gating on the measured file would leave the check unable to fail once the file
went missing.

Three break → revert directions, each demonstrated and reverted byte-identical:

break fails
too narrow — the bare literal the four sibling cases
too broad by kind*langsmith-fleet* docs/langsmith-fleet-contract.md
too broad by depth — slash dropped docs/contracts/schemas/langsmith-fleet-v1.schema.json

Verification

Full verify.py green on the identical code content: 421 passed, 0 failed, 0/26 skipped, 84/84
selftests, 5/5 gates
. Rebased since onto 4122a07; the only change was .verify-floor.json's two
integers, re-measured at 422 collected = floor 422, with the guard tests, env_prereq --selftest
and the sync check re-run directly. CI runs the authoritative full verify.py on a clean machine.

Floor re-measured on each merge result as main moved underneath four times (402+10, 407+10, 410+11,
411+11) — which is exactly what the collection equality exists to force. No ceiling moved: the
tests run on any checkout and skip only in the exec-mirror layout, which is a file copy, not a .git.

Upstream — the actual root cause, fixed separately

Workflows/docs/WORKFLOW_ARTIFACT_CHECKLIST.md is the owning doc, and its "Recovery from Artifact
Pollution" procedure is exactly the two steps taken here. The recurrence cause is add-scope:
reusable-codex-run.yml and reusable-18-autofix.yml run git add -A and then subtract a
hand-curated exclusion list of ~25 names, which a newly-added emitter escapes by default.

Workflows had already diagnosed this verbatim in PR #2856 (2026-07-31) — "while tracked, every run
that rewrote it produced a diff that codex-autofix then committed onto whatever PR happened to be
open"
— but fixed only its own repo, never templates/consumer-repo/.gitignore, so consumers stayed
exposed and this repo repeated the defect 23 days later. Six repos carried a tracked copy.

  • stranske/Workflows#3210 — template + FALLBACK_PATTERNS at Template-Version 6, the emitter moved
    to $RUNNER_TEMP so the file never enters the checkout, and a guard test that turns the checklist
    into a gate (nothing referenced it before; a grep for callers found zero).
  • Per-repo untracking: Travel-Plan-Permission#1470, Counter_Risk#957, Pension-Data#850, Ready#537,
    trip-planner#1759.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 6 minutes.

View limit details

Limit details: You’ve used the included review currently available. Your 73 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: eb0b760c-eda0-40ca-9fbf-701830909c7e

📥 Commits

Reviewing files that changed from the base of the PR and between 4b5764a and 2ee6f3e.

📒 Files selected for processing (4)
  • .gitignore
  • .verify-floor.json
  • env_prereq.py
  • test_repo_artifact_hygiene.py

Comment @coderabbitai help to get the list of available commands.

@stranske
stranske force-pushed the claude/agitated-almeida-13a122 branch from c8a2d60 to b98e9d0 Compare August 23, 2026 19:27
@stranske
stranske force-pushed the claude/agitated-almeida-13a122 branch from b98e9d0 to 20b83e2 Compare August 23, 2026 19:28
@agents-workflows-bot

agents-workflows-bot Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Workflow source detected

PR #72 now has valid workflow source context (origin=local_request).

No linked GitHub issue is required for this PR.

@agents-workflows-bot

agents-workflows-bot Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Automated Status Summary

Head SHA: 6299481
Latest Runs: ⏳ pending — Gate
Required: core tests (3.12): ⏳ pending, core tests (3.13): ⏳ pending, docker smoke: ⏳ pending, gate: ⏳ pending

Workflow / Job Result Logs
(no jobs reported) ⏳ pending

Coverage Overview

  • Coverage history entries: 1

Coverage Trend

Metric Value
Current 48.45%
Baseline 0.00%
Delta +48.45%
Minimum 70.00%
Status ❌ Below minimum

Top Coverage Hotspots (lowest coverage)

File Coverage Missing
outcomes.py 9.0% 243
watch.py 9.6% 431
capability_recurrence_check.py 10.4% 421
durability_sweep.py 12.2% 339
keepalive_shadow.py 13.0% 282
capability_outcome_bridge.py 13.5% 295
keepalive_outcomes.py 14.0% 339
adversarial.py 14.1% 164
langsmith_fetch.py 14.3% 409
gh_capacity.py 14.6% 228
runtime_ac_panel.py 14.7% 290
capability_advisor.py 14.8% 807
redirect_shadow.py 16.9% 476
cross_repo_lane.py 17.3% 268
experiment_recovery.py 18.4% 164

Low Coverage Files (<50.0%)

File Coverage Missing
outcomes.py 9.0% 243
watch.py 9.6% 431
capability_recurrence_check.py 10.4% 421
durability_sweep.py 12.2% 339
keepalive_shadow.py 13.0% 282
capability_outcome_bridge.py 13.5% 295
keepalive_outcomes.py 14.0% 339
adversarial.py 14.1% 164
langsmith_fetch.py 14.3% 409
gh_capacity.py 14.6% 228
runtime_ac_panel.py 14.7% 290
capability_advisor.py 14.8% 807
redirect_shadow.py 16.9% 476
cross_repo_lane.py 17.3% 268
experiment_recovery.py 18.4% 164

Updated automatically; will refresh on subsequent CI/Docker completions.


Keepalive checklist

Scope

No scope information available

Tasks

  • No tasks defined

Acceptance criteria

  • No acceptance criteria defined

@stranske

Copy link
Copy Markdown
Owner Author

Runner dispatch state for codex on PR #72. Do not edit.

@github-actions

github-actions Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Workflow state fingerprint for Agents Gate Followups. Do not edit.

@stranske
stranske force-pushed the claude/agitated-almeida-13a122 branch from 20b83e2 to 976bf44 Compare August 23, 2026 19:31
@github-actions

Copy link
Copy Markdown
Contributor

Workflow state fingerprint for Keepalive Loop Reporter. Do not edit.

`langsmith-fleet-worker-attempt.json` was TRACKED at the repo root, holding one
run's execution telemetry (`operation_role: worker`, a resolved `selected_model`,
and the `pr_number` of whichever PR last merged). `stranske/Workflows`
`reusable-codex-run.yml` rewrites it into the checkout root on every agent round,
purely to stage the `actions/upload-artifact` step on the next line — so the
autofix bot re-emitted it on the next PR and #66 hit an add/add conflict on a
path neither side had a reason to author. That was dropped there; the cause was
not. It recurred during this branch's own review: #68's merge re-added the file,
turning the rebase into a modify/delete conflict, which is the defect
demonstrating itself.

Wrong in this repo's own terms, not merely noisy. CLAUDE.md §1's tool-vs-evidence
split puts one run's telemetry outside the tree. §2 requires execution
provenance to reach the learner through `feedback.py`'s tables, and a
git-churned `worker` record with a resolved model is a second, unmanaged store of
exactly that evidence. Nothing here reads the path — this tree's emitters are
`langsmith_*.py` and they read `langsmith-fleet.ndjson` from `$ORCH_STATE_DIR`.

Both steps are needed, and the order is not interchangeable. Verified against a
scratch repository: `git add -A` SKIPS an ignored UNTRACKED path but STAGES an
ignored TRACKED one. So ignoring alone would have been inert while the path
stayed in the index, and untracking alone would have let the next round re-add
it. It also means the runner's `git reset HEAD --` denylist is only load-bearing
for paths that are already tracked.

A PATTERN, not the literal name: the runner names the file after the ROLE it
recorded, so a verifier or evaluator attempt arrives as a sibling and escapes a
literal.

ROOT-ANCHORED, and the leading slash is load-bearing. The first draft bounded the
patterns by extension and treated that as sufficient; it is not. Unanchored, a
gitignore pattern matches at EVERY depth, so `langsmith-fleet*.json` also
swallowed a contract schema at
`docs/contracts/schemas/langsmith-fleet-v1.schema.json` — a path
`stranske/Workflows` really tracks, whose sibling
`artifact-manifest-v1.schema.json` this tree already keeps. A schema IS a
`.json`, so extension-bounding cannot save it. Found by a concurrent session
reviewing the upstream half of this fix. The nested CI artifact directory is
named separately as `artifacts/langsmith/` for the same reason: anchoring it to
the one directory that holds it cannot reach a schema elsewhere. Same near-miss
the vendored-`node_modules` exception records, same conclusion.

Placed ABOVE the managed `BEGIN/END WORKFLOWS STATUS FILES` block on purpose: a
Template-Version bump rewrites that block and would silently drop the line.
`scripts/sync_status_file_ignores.py --check` still reports all canonical
patterns present.

Guarded by `test_repo_artifact_hygiene.py` (11 tests), which puts every question
to git rather than reimplementing gitignore precedence — `.gitignore`'s own
header forbids the alternative, recording that trailing comments once made every
pattern inert and staged 795 files instead of 141. The new prerequisite is
`env_prereq.git_repo_absent()`, gated on whether git can answer here and never on
`.gitignore` itself, since gating on the measured file would leave the check
unable to fail once the file went missing.

Three break->revert directions are held, each demonstrated and reverted
byte-identical: too narrow (the bare literal fails the four sibling cases), too
broad by KIND (`*langsmith-fleet*` fails on the contract doc), too broad by DEPTH
(dropping the slash fails on the schema).

Floor 410 -> 421, re-measured on each merge result as main moved twice underneath
(402+10, then 407+10, then 410+11). No ceiling moved — the tests run on any
checkout and skip only in the exec-mirror layout, which is a file copy, not a
`.git`.

Upstream, handled separately: Workflows PR #2856 (2026-07-31) diagnosed this
verbatim but fixed only its own repo, never `templates/consumer-repo/.gitignore`,
so consumers stayed exposed and this repo repeated the defect 23 days later.

Co-authored-by: Cursor <cursoragent@cursor.com>
@stranske
stranske force-pushed the claude/agitated-almeida-13a122 branch from 976bf44 to dc38c43 Compare August 23, 2026 21:47
@stranske

Copy link
Copy Markdown
Owner Author

Closer rebase evidence (cursor closer lane)

Rebased claude/agitated-almeida-13a122 onto post-#71 origin/main after #71 merged.

  • New head: dc38c43f636006bb880eb5af06ef9ee7cc6a5807
  • Conflict resolution: .verify-floor.json union — main at 416 collected + this branch's 11 test_repo_artifact_hygiene.py tests = 427 collected (pytest --collect-only -q on merge result). Ceilings unchanged at 26/7/2.
  • Artifact: kept deletion of langsmith-fleet-worker-attempt.json (already removed on main via fix(ci): stop tracking the per-run worker-attempt artifact #71); root-anchored .gitignore patterns retained from this branch.

CI is running on the new head. Merge will wait for required checks + the 7-minute post-push review window.

…rop a claim #3210 falsifies

This branch ended up with two independent explanations of the same rule, because a
concurrent session added `4b5764a` while this branch already carried its own block.
Both were correct; having both in a file whose header is specifically about
gitignore-comment discipline is not.

Kept from the second block, because the first did not have it:
- the artifact was tracked from 2118f57, not merely "on main until today";
- ten commits across PRs #59/#61/#62/#63 rewrote it, one add/add conflict per
  concurrent PR, silently widening unrelated diffs;
- untracking loses no provenance — the producer already uploads the same bytes as
  Actions artifact `langsmith-fleet-v1-worker-attempt-<pr>` with 30-day retention,
  which is the only reason the file is written at all.

Dropped from it, because it is about to become false: "that block mirrors Workflows'
templates/consumer-repo/.gitignore, which does NOT carry this pattern". Workflows
PR #3210 adds exactly these two patterns to that template at Template-Version 6. The
placement above the managed block is still right, but the reason is that a version
bump rewrites the block wholesale — not that the template lacks the pattern.

Also drops that block's bare literal `langsmith-fleet-worker-attempt.json`, which was
already subsumed by the root-anchored `/langsmith-fleet*.json` above it.

Comments only. Verified identical behaviour with `git check-ignore`: the root artifact
and artifacts/langsmith/ stay ignored, docs/contracts/schemas/langsmith-fleet-v1.schema.json
and langsmith_*.py stay committable, and no tracked file is shadowed by any pattern.
427 collected = floor 427; black, ruff and the 11 guard tests unchanged.
@stranske
stranske merged commit a034f8e into main Aug 23, 2026
65 checks passed
@stranske
stranske deleted the claude/agitated-almeida-13a122 branch August 23, 2026 22:36
stranske added a commit that referenced this pull request Aug 25, 2026
… tracked binary (#119)

`.coverage`, a 90 KB coverage.py SQLite database, has been TRACKED on main since #109 —
a typing PR whose every other changed file is about mypy. Nobody chose to commit it; it
was in the working tree when the branch was staged, the same accident that put
src/UNKNOWN.egg-info/ on #113.

It is build output, and the churn is mechanical rather than occasional: verify.py's
coverage_reset() UNLINKS ROOT/.coverage and ROOT/.coverage.* before every instrumented
run and coverage_combine_and_report() writes them again. So while tracked, the repo's own
verdict command showed up as a deletion of a tracked binary followed by a re-add of
different bytes, and an uncommitted copy blocked a branch switch.

BOTH patterns, because two different steps write them: `--parallel-mode` writes one
`.coverage.<host>.<pid>.<random>` per instrumented child (~90 subprocesses per run) and
`coverage combine` merges them into `.coverage`. Ignoring only the combined file leaves
the per-child data files unignored.

ROOT-ANCHORED per the rule #72 established for every debris pattern in .gitignore, and
true of the producer here: verify.py globs and unlinks ROOT, so the checkout root is the
only place these land. `/.coverage.*` and NOT `/.coverage*` — the second also swallows
`.coveragerc`, coverage.py's own config file, which is source.

And UNTRACKING is what makes the patterns bite: `git add -A` skips an ignored UNTRACKED
path but stages an ignored TRACKED one, so the pattern alone would have changed nothing.

test_repo_artifact_hygiene.py had NO case for this file — `grep -n coverage` on it
returned nothing — so nothing would have caught a recurrence. Five added, matching the
suite's three halves: two parametrized ignore cases, one untracked case, and two
must-stay-committable cases. Break -> revert performed in all three directions the
patterns can fail (too narrow, too broad by kind, ignored-but-re-tracked); each broke
exactly one case, and the revert was byte-identical. Recorded in the file.

The depth direction the langsmith patterns record has no in-tree near-miss here and is
not claimed to; the anchoring is kept for the producer's reason instead.

Verified: 458 passed, 0 failed, 0 skipped, 85/85 selftests, 5/5 gates. Floor 453 -> 458,
measured on the merge result after fast-forwarding onto origin/main 5c769e0 and
re-fetching to confirm main had not moved again.

Co-authored-by: Tim Stranske <tim@stranskemo.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant