Skip to content

Campaign bundle: PDD end-to-end staging fixes#1998

Open
Serhan-Asad wants to merge 88 commits into
mainfrom
stack/e2e-staging-v2
Open

Campaign bundle: PDD end-to-end staging fixes#1998
Serhan-Asad wants to merge 88 commits into
mainfrom
stack/e2e-staging-v2

Conversation

@Serhan-Asad

@Serhan-Asad Serhan-Asad commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Bundles the PDD CLI fixes discovered while driving the six campaign issues end to end through the staging workflow. Individual fix PRs were accumulated on stack/e2e-staging-v2; nothing from this session was merged directly to main.

Newly finalized in this pass:

The branch also preserves the previously validated campaign stack fixes for sync, provider fallback, and checkup behavior.

Verification

Issues resolved by this bundle

Closes #1900
Closes #1903
Closes #1926
Closes #1936
Closes #1938
Closes #1939
Closes #1941
Closes #1957
Closes #1968

Landing order

  1. Merge this PDD bundle first after required review/CI.
  2. Merge the companion pdd_cloud campaign bundle after this one.

Manager review/merge is required. This PR intentionally does not auto-merge.

Serhan-Asad and others added 30 commits July 7, 2026 16:31
…ace contract (#1900)

The public-surface gate compared OLD-vs-NEW generated code, so it could not
distinguish an intended interface change from generator drift, and its repair
directive had no stable target ("restore compatible signatures" — compatible
with the very code being regenerated). This dead-ended `pdd change -> pdd sync`
whenever a module's declared interface legitimately evolved (live case
pdd_cloud#2971: an added symbol plus unrelated `list_secret_metadata` drift).

Make `_verify_public_surface_regression` declaration-aware, per-symbol:

- Symbols declared in the prompt's <pdd-interface> (`type: module`) are validated
  against the DECLARED signature (a stable target) via `signature_entries_compatible`,
  with defaults resolved in the generated namespace (#1558). Binding-kind/async —
  which <pdd-interface> cannot express — stay anchored to the prior generation so
  async<->sync / function<->class drift is still caught; `BREAKING-CHANGE: change
  signature` remains the escape hatch for those.
- Undeclared symbols keep the old-code baseline (+ `BREAKING-CHANGE`), so
  backward-compat protection for helpers/re-exports is unchanged (no regression).
- `PublicSurfaceRegressionError` carries structured `signature_detail:` lines
  (full expected-vs-actual, no truncation); the agentic subprocess parser recovers
  them into the rebuilt repair directive and hard-failure block. `removed:` /
  `signature_changed:` lines stay byte-identical (cloud parser + tests depend on them).

Scoped to `type: module` function declarations; cli/command names and dotted
`Class.__init__`/new-class presence stay owned by the conformance gate.

Adds tests/test_issue_1900_surface_contract.py (18 gate-level tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…enerator_main

PDD-Auto-Heal-Checkpoint: success
… / parser hardening

Follow-up to the #1900 public-surface declaration contract (the preceding
auto-heal bot commit for examples/architecture/metadata is left untouched):

- Declared dotted names (`Class.method`, `Class.__init__`) are presence-only in the
  surface gate: `_snapshot_public_signatures` receiver-strips `self`/`cls`, so
  comparing a declared method signature that includes `self` false-positived on
  UNCHANGED code. Their parameter contract stays with the `<pdd-interface>`
  signature gate; top-level functions keep full declared-signature validation.
- `BREAKING-CHANGE: change signature <sym>` now relaxes ONLY the un-declarable
  binding-kind/async for a declared symbol, never its declared params/return — an
  added-required-param that violates the declaration is no longer bypassable by prose.
- `_parse_signature_detail_lines` tolerates ` | ` inside a signature/default
  (dropped the ordered-index precheck; right-anchored rsplit + try/except only), so
  union types / delimiter-like defaults keep the stable declared repair target.
- Sync README + the `code_generator_main` generator prompt (5b) to the
  declaration-aware behavior (incl. `PublicSurfaceRegressionError.signature_details`).

Adds 4 regression tests (22 total in tests/test_issue_1900_surface_contract.py).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…code baseline (#1900)

Review follow-up: making dotted-method / non-paren-class declared symbols
presence-only ALSO removed them from the old-code baseline, dropping the signature
protection they had before #1900 — a declared method could silently gain a required
arg or flip binding kind, and a declared class's constructor ABI could drift.

Only symbols actually validated against the declared signature (top-level names with
a parseable paren signature) are now recorded in `declared_validated` and excluded
from the old-code baseline; every other declared symbol (dotted methods,
`class Service`-style non-paren declarations) falls back to the previous-generation
old-vs-new comparison, which catches added-required-params, binding-kind flips, and
constructor-ABI drift, and still honors BREAKING-CHANGE + #1558. The receiver-strip
false positive stays fixed (the old-code path compares receiver-stripped snapshot
entries on both sides). README + generator prompt (5b) wording synced to match.

Adds 4 regression tests (method +required, staticmethod->instance flip, class ctor
drift, BREAKING-CHANGE opt-out) and strengthens the presence-only test that had
encoded the over-skip. 26 tests in tests/test_issue_1900_surface_contract.py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolves the architecture.json conflict (main #1871/#1777 vs the auto-heal bot
commit) by taking main's registry; the auto-heal bot re-generates any real drift
for the changed modules on the next push. No source/test changes.
…face too (#1900)

Review follow-up: dotted methods and constructors were presence-only with an
old-code fallback, which left NEW methods/ctors unchecked (an added required arg
passed) and kept the `pdd change → sync` dead-end for method contract changes. Make
them first-class declared-contract citizens, exactly like top-level functions.

`_declared_signature_to_entry` gains `strip_receiver`: a declared method/ctor
signature is receiver-stripped (leading `self`/`cls` dropped) to match
`_snapshot_public_signatures`; `Class.__init__` maps to the class `[class]` entry
(constructor ABI); binding-kind/async come from the pre-generation entry when present
(else the generated one) so a `@staticmethod`→instance flip is still caught; and
`BREAKING-CHANGE: change signature` relaxes only those un-declarable facets, never the
declared params. Only symbols actually compared against the declaration join
`declared_validated` (keyed on the snapshot key) and are excluded from the old-code
baseline; description-only / non-paren declarations stay presence-only and fall back
to old-vs-new so no pre-#1900 protection is lost.

Net: a new declared method/ctor with an added required arg now raises; editing a
declared method contract is authorized without a `BREAKING-CHANGE` permit; the
declared-`self` false positive stays fixed. README + generator prompt (5b) synced.

Adds 3 tests and reworks 2 (29 in tests/test_issue_1900_surface_contract.py).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolves the <pdd-interface> conflict in code_generator_main_python.prompt: keeps
both PublicSurfaceRegressionError.signature_details (#1900) and the new
ProseOutputError entry (#1649). code_generator_main.py / agentic_sync_runner.py /
README auto-merged (disjoint regions). Full sweep 599 passed.
The auto-heal bot commit (3632d6a) regenerated context/agentic_sync_runner_example.py
in a way that dropped the top-level AsyncSyncRunner shim (breaking other prompts'
<include select="class:AsyncSyncRunner">), un-mocked _sync_one_module (so the example
spawned real pdd sync subprocesses), and left project_dependencies.csv stale with
trailing whitespace. The #1900 change is internal (surface gate + detail parser) and
does not alter the public interfaces these examples demonstrate, so restore main's
known-good generated artifacts. PR diff is now purely the #1900 source/tests/docs.
…allable, fix repair advice (#1900)

Review follow-up (three real gaps in the declared-interface surface contract):

- Declared `_`-prefixed helpers (e.g. agentic_common's `_extract_step_report`) were
  false-positived as removed: `declared_missing` checked presence against the public
  surface, which excludes underscore names. The declaration is authoritative (like
  `__all__`), so union the declared symbols actually defined in the code into the
  `patch_targets` fed to `_snapshot_public_surface` / `_snapshot_public_signatures`
  for both before and after — a present declared `_helper` is no longer flagged, a
  genuinely removed one still is, and its signature is now validated too.

- A declared callable regenerated as a non-callable (`def f` → `f = 1` /
  `from pkg import f as f`) passed: `signature_entries_compatible` returns None and the
  symbol was still marked validated, so the old-code baseline was skipped. Now only add
  to `declared_validated` when the comparison is conclusive (`compat is not None`); a
  None result falls through to the old-code baseline, which flags the
  `[function]`→`[assignment]`/`[import]` break (and does not false-positive on an
  unchanged callable-assignment).

- The hard-failure / repair advice told users to add `BREAKING-CHANGE`, which for a
  declared symbol only relaxes binding-kind/async, never params — a dead-end. Advice is
  now declaration-aware: a declared-param violation points at editing the
  `<pdd-interface>` declaration; the `BREAKING-CHANGE` guidance stays for
  undeclared/removed symbols.

Adds 7 tests (36 in tests/test_issue_1900_surface_contract.py); the generator prompt's
`<include>` now lists the new helpers; README deduped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ines, underscore-class ctor (#1900)

Review follow-up (three edge gaps in the declared-interface surface contract):

- A declared callable regenerated as a non-callable (`def f` → `f = 1` /
  `from pkg import f as f`) slipped through when the prompt also carried
  `BREAKING-CHANGE: change signature f` — the old-code fallback skipped it via
  `allowed_signature_changes`. Now, when the declared comparison is inconclusive
  (`compat is None`) and the OLD entry was a callable contract, flag the
  declared-callable-became-non-callable DIRECTLY, regardless of `BREAKING-CHANGE`
  (which authorizes parameter changes, not de-callable-ing a declared callable — use
  `BREAKING-CHANGE: remove` for that). An unchanged callable-assignment
  (`f = lambda: x`) still passes.

- `signature_detail:` lines are now JSON-encoded (`json.dumps` of
  `{symbol, expected, actual, source}`) at both emit sites and parsed via
  `json.loads`, ending the recurring field corruption when a signature default
  contains the ` | actual: ` / ` | source: ` delimiter substring.

- Declared underscore-CLASS constructor drift (`_Service.__init__`) was missed because
  the patch-target signature capture only built entries for top-level functions.
  Extracted `_class_constructor_signature` (shared with `_walk_class`) and used it so a
  patch-target class gets its `[class]` constructor-ABI entry; a declared underscore
  class / constructor is now validated like a public one.

Adds 5 tests (41 in tests/test_issue_1900_surface_contract.py); the generator prompt
spec notes the JSON detail-line format.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sses; sync self-hosting metadata (#1900)

Review follow-up:

- Declared NESTED constructors whose class path contains an underscore behaved
  inconsistently — `_Outer.Inner.__init__` passed silently, `Outer._Inner.__init__` was
  mischaracterized as `removed`. They now behave like the public control:
  `_symbol_exists_in_module` resolves nested class paths (not just methods),
  `_resolve_class_node` walks dotted segments to a `ClassDef`, and the patch-target
  signature capture emits the `[class]` constructor-ABI entry for a nested declared class
  (keyed `Outer.Inner`) — so ctor drift raises a `changed` signature with a
  `pdd-interface` detail. Surgical: only DECLARED/patch-target nested classes are
  captured; the public `_walk_class` recursion (undeclared underscore-nested) is
  untouched, so no protection is broadened. `test_split_validation.py` confirms the
  `_symbol_exists_in_module` generalization didn't regress sibling-test patch resolution.

- Self-hosting metadata synced to the #1900 behavior so future self-regeneration keeps
  it: the `agentic_sync_runner` prompt documents the JSON `signature_detail:` parsing +
  declaration-aware repair; the `code_generator_main` prompt `<include>` lists the new
  helpers (`_class_constructor_signature`, `_resolve_class_node`,
  `_symbol_exists_in_module`, `_patch_target_signature_entry`); and architecture.json's
  `code_generator_main` + `agentic_sync_runner` descriptions carry the declared-contract /
  JSON-detail behavior.

Adds 4 tests (45 in tests/test_issue_1900_surface_contract.py).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cal test path (#1903)

PDD derived a module's test-output path purely from `.pddrc`/defaults
(`tests/test_{name}{ext}`), blind to the project's real test runner. On a
jest/Next.js project it maintained a root `tests/` shadow while the co-located
`__test__/*.test.tsx` the runner collects went stale — `generate`/`change`/`sync`
reported "tests pass" (PDD ran its own shadow) while CI failed. A `.tsx` module
also derived a `.ts` test extension.

PDD now adopts a single, unambiguous, existing co-located test as its canonical
test path so generate/change/sync target the real test CI runs:

- content_selector: pure `find_collocated_test` (Python siblings + JS/TS
  jest/vitest/Next.js conventions; single match else None; suffix-gated; never
  raises), the `resolve_test_output_path` adopter, and presence-based
  explicit-vs-default provenance (`_pins_test_output_location` /
  `configured_test_output_pinned`).
- sync_determine_operation: `get_pdd_file_paths` wraps the raw derivation and
  adopts across every return branch, threading a single authoritative
  explicit-vs-default signal read from the raw `.pddrc` context (never the
  `resolved_config` construct_paths pollutes).
- cmd_test_main: adopts for direct `pdd test`, keeps the native/cloud generation
  destination in sync with the adopted write target, and pins sync's
  merge-into-existing write to the real test (no numbered `page.test_1.tsx`
  shadow).
- code_generator_main: `_is_test_output_path` recognizes adopted `.test/.spec`
  `.mjs/.cjs` and the singular `__test__` dir, so adopted human tests stay behind
  the TestChurnError guard.

Explicit user pins (CLI `--output`, `PDD_TEST_OUTPUT_PATH`, explicit `.pddrc
test_output_path`/`outputs.test.path`) are always honored; the standard Python
`src/foo.py -> tests/test_foo.py` flow is unchanged. Regression tests drive the
real `construct_paths`/`architecture.json`.

Source-of-truth: the four owning `prompts/*_python.prompt` files
(content_selector, sync_determine_operation, cmd_test_main, code_generator_main)
are updated to describe this behavior so PDD's regeneration / drift-heal
reproduces it (in this repo the `.py` is generated from the prompt).

Adoption is applied consistently across the write paths AND the
`pdd test --evidence` manifest resolver, so evidence/replay audits point at the
file PDD actually writes, not the shadow.

Part of #1903 (scoped: adopts an existing co-located test; greenfield
runner-config detection and never-block-on-churn are tracked as follow-ups
#1916 / #1917).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…on (#1926)

Introduces a single `FingerprintTransaction` context manager that enforces
the 'artifact write ⇒ fingerprint write' invariant across all mutating PDD
commands (sync, generate, update, auto-deps, fix, CI heal). Replaces
scattered `save_fingerprint` call sites with one commit-or-fail code path
featuring null-hash guards, atomic temp-file+rename persistence, and
`FingerprintFinalizeError` propagation to Click (non-zero exit on failure).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-fresh (#1938)

pdd sync regenerated mature modules "rebirth-shaped" (full regeneration),
dropping declared public symbols. PDD already ships edit-shaped generation
(incremental_code_generator + code_patcher_LLM), but sync hardcoded
force_incremental_flag=False, so it deferred to the deliberately-conservative
diff_analyzer_LLM ("prefer complete regeneration"), which rebirthed modules on
ordinary small prompt deltas and lost symbols.

Make sync prefer surgical generation for mature modules, with a --fresh opt-out:
- New `pdd sync --fresh` flag (default off), threaded sync -> sync_main ->
  sync_orchestration and the one-session generate loop.
- Both sync generate call sites now pass force_incremental_flag=(not fresh):
  default -> surgical/edit-shaped (existing code + prompt delta -> minimal edit,
  declared symbols preserved); --fresh -> standard generation.
- Reuses existing infra only. Safety nets preserved: new/empty modules still
  full-generate; undeterminable original prompt falls back to full generation;
  conformance repair retries still force full regeneration (#1724); the
  public-surface / declared-interface gate (#1900) is unchanged and remains the
  guarantee.
- --fresh is single-module only: raises UsageError on global/agentic sync paths
  (mirrors --snapshot-context) instead of silently no-op'ing.

Prompt sources edited in tandem with generated .py (pdd is self-hosted);
sync_main's architecture.json signature aligned; README documents the flag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ily is down (#1941)

The review loop dead-locked instead of degrading when only one AI provider
family was alive. With `--reviewer codex --fixer claude
--fallback-reviewer-on-failure`, a codex hard-failure promoted claude (the
fixer's own role) to fallback-review the PR; when claude reported real
findings the loop hit `break` with "secondary reviewer ... reported findings
(fallback)" and never ran a fix round — even though a fresh same-family claude
session could execute the concrete findings and be re-reviewed.

Fix: in the fallback-reviewer "findings" branch of `run_checkup_review_loop`,
instead of terminating, hand the actionable findings to the fixer on the next
iteration (mirrors the existing fallback-clean gate-findings handoff just
above), reassign the active reviewer to the fallback role, and let the normal
fix + fresh-verify path run. Disclose the weaker guarantee honestly:

- new `ReviewLoopState.role_independence` (`"independent"` |
  `"degraded (<role> unavailable)"`), rendered as a `role-independence:` report
  line and a `role_independence` key in final-state.json;
- `same_role_review_fix` is stamped true for the degraded run;
- the fix-failure stop reason names the vacancy explicitly instead of a bare
  "could not address".

The superseded primary already renders `(optional, superseded by <fallback>)`,
so the pdd_cloud verdict adapter drops it from the required-reviewer set and
ships on the degraded-but-clean result — no cloud change needed. Behavior is
unchanged when both families are alive.

Adds regression tests: the deadlock→ship path (asserts the review-loop Machine
Verdict passes + final-state disclosure), both-families-alive stays
independent, and degraded-fixer-also-fails names the vacancy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address review of #1942: the code change added runtime same-role degradation
and a new `role_independence` state/report/final-state field, but the
prompt-driven source-of-truth surfaces still described the pre-#1941 contract.
Left unreconciled, `pdd sync` / the auto-heal bot would regenerate the code
from the stale prompt and drop the fix.

Reconcile all four surfaces with the implemented behavior:
- pdd/prompts/checkup_review_loop_python.prompt: specify the auto-degrade rule
  under `fallback_reviewer_on_failure` (fallback-reviewer findings → fresh
  same-family fix + required verify instead of dead-end), the `role-independence:`
  report header line, the `role_independence` final-state.json key + revised
  `same_role_review_fix` semantics (config-time OR runtime), the appended-field
  ordering, and the degraded fix-failure stop-reason suffix.
- architecture.json: add the #1941 auto-degrade sideEffect + `role_independence`
  to the final-state.json field list for run_checkup_review_loop.
- README.md: document the auto-degrade behavior + disclosure under
  `--fallback-reviewer-on-failure`.
- context/checkup_review_loop_example.py: add `role_independence` to the
  ReviewLoopState example, the final-state payload, the report-header comment,
  and the rendered-report example.

No code/behavior change; docs-only reconciliation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gnostics (#1941)

Review hardening for #1942: the cloud verdict adapter reads a
`role-independence:` header line to route a degraded run to `ship_degraded`,
but untrusted reviewer stderr rendered in `### Reviewer Diagnostics` could echo
`role-independence: independent` and — since the adapter previously took the
last match in the whole report — override the authoritative header, silently
downgrading a degraded disclosure back to a plain ship (or the reverse).

Add `_defang_role_independence_inline` to `_defang_adapter_trip_wires` so any
`role-independence:`/`role_independence=` marker leaking from reviewer stderr is
neutralized at the render boundary, exactly like the existing
`reviewer-status:` / `fresh-final-review:` / `max-*-reached:` markers. The
on-disk `final-state.json` and `reviewer_status_details` keep the original text.

Prompt spec (27b) updated to list the new marker. Companion adapter-side
defense (header-scoped parse) is in promptdriven/pdd_cloud#3126.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…#1957)

The checkup source-of-truth guards reconstructed each module's owning
prompt path by string-joining a hardcoded `pdd/prompts/` prefix onto the
`filename` stored in `architecture.json`. That prefix only ever resolved
on pdd's own self-hosted checkout (prompts live under `pdd/prompts`, with
a `prompts -> pdd/prompts` symlink). On every OTHER repo the guard probed
a nonexistent path, classified the owning prompt as `missing_prompt`, and
refused to repair — blocking the bug→fix→checkup chain on all external
repos (observed on staging E2E against promptdriven/test_repo #4234/#4235).

`_load_prompt_source_map` and `_extract_arch_pairs` now resolve the base
prompts root via `_resolve_target_prompts_root(worktree)`, which reuses
the same `.pddrc` discovery/parse helpers generate/sync use
(`construct_paths._find_pddrc_file` / `_load_pddrc_config`) and falls back
to `prompts` then `pdd/prompts`, picking the first existing directory and
resolving symlinks to the real git-tracked path. A prompt is only reported
deleted when it is missing from the target repo's resolved root.

Owning prompt `pdd/prompts/checkup_review_loop_python.prompt` updated to
match (10a/10b mapping rules + new resolver spec).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Agentic provider selection already fell through to the next provider on a
permanent error *within a single run_agentic_task call*, but each call
rebuilt its candidate list from scratch. A permanently-dead provider (e.g.
Codex whose staged ChatGPT auth is present but expired) was therefore
re-attempted on every step of a multi-step workflow, and a call whose only
feasible candidate was the dead provider had nowhere to fall through to —
so cloud one-session sync died on codex's 401 despite healthy anthropic/
google credentials in the same run.

Add a run-scoped permanent-failure registry in the single provider-selection
source of truth (run_agentic_task):

- mark_provider_permanently_failed(provider, classification) records a
  provider when _classify_permanent_error fires; get_disabled_providers()
  exposes the {provider: classification} map; reset_disabled_providers()
  clears it. The registry is mirrored into the PDD_AGENTIC_DISABLED_PROVIDERS
  env var so re-entrant `pdd` subprocesses inherit the same skip-list.
- run_agentic_task drops disabled providers from candidates AFTER the
  PDD_AGENTIC_PROVIDER hard filter and BEFORE the single_provider_attempt
  truncation, so a known-dead first provider no longer consumes the single
  attempt and later steps skip it instead of re-paying its timeout.
- When every feasible provider was already disabled this run, fail fast with
  an aggregated "All agent providers failed permanently earlier this run:
  <provider>: <class>; ..." message instead of re-burning or the generic
  "no providers available" line.

The registry is subordinate to PDD_AGENTIC_PROVIDER: it only ever removes a
provider that proved dead, never adds one, so a staged-auth hint cannot
re-pin a provider once its auth is known bad. No behavior change when the
first provider works.

Updates the owning prompt (agentic_common_python.prompt) with the new
exports and the registry requirement, and adds regression tests plus
conftest isolation for the new env var / registry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rough

Run-scoped provider permanent-failure fall-through (#1936).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Prompt-declared <pdd-interface> as the public-surface contract (#1900).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surgical (edit-shaped) regeneration for mature modules + --fresh (#1938).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
)

PR#1940 made `pdd sync` prefer surgical (edit-shaped) regeneration for mature
modules, which needs an "original" prompt to diff the current prompt against.
In the cloud sync-after-change flow the prompt edit is already committed on the
branch (on-disk == HEAD) and the pre-change version is not reliably found by the
local history search, so incremental resolution silently degraded to full
regeneration and re-dropped declared symbols (live failure: test_repo#4232, 5
consecutive surface-gate blocks).

Add a last-resort fallback in code_generator_main's original-prompt resolution:
when no explicit original, no uncommitted-vs-HEAD delta, and no distinct prior
version in local history resolve an original, read it from the base branch via
the existing get_git_content_at_ref() helper. Base ref is configurable via
PDD_SYNC_BASE_REF (default origin/main) and is only used when it differs from
the current prompt.

This replaces the prior degenerate fallback that set the "original" to the
current HEAD content (identical prompts), which produced a no-op incremental
patch that was then discarded for full regeneration anyway. When the base ref
also yields nothing usable, can_attempt_incremental now stays False and an
honest full regeneration runs, surfaced by a single unconditional (non-verbose)
degradation notice so operators are not blind to mature-module full regens.

Tests cover: base-ref resolves a distinct original -> incremental used;
base-ref missing (None) -> full generation + loud notice, no crash; base-ref
identical to current -> not used; PDD_SYNC_BASE_REF override honored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-resilience

Auto-degrade to same-family fixer when one provider family is down (#1941).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…esilience

Resolve owning-prompt paths against the repo-under-repair layout, not pdd's self-hosted pdd/prompts (#1957).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…p-resilience

Adopt the runner-collected co-located test as PDD's canonical test path (#1903).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… CodeQL)

CodeQL flagged 4 py/path-injection alerts (#339-#342) on the new adoption
code: candidates derived from a caller-supplied code-file path — issue-
influenced in the hosted/agentic flow — flowed into resolve()/write-target
selection without containment, so a traversal-shaped input could adopt a
test path outside the repo.

find_collocated_test now rejects candidates that resolve outside cwd, and
both adoption sinks (resolve_test_output_path, _adopt_collocated_test)
re-assert containment before retargeting. Traversal inputs degrade to
no-adoption (derived path unchanged). Adds 4 containment regression tests;
5 existing tests updated to run with cwd=the project like the rest of the
suite (matches real CLI invocation).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Serhan-Asad

Copy link
Copy Markdown
Collaborator Author

Second review — reviewed commit: 6415086fbcc95e03cb3ee2efda83286eaaa57fa0

Intent

This latest revision preserves the campaign bundle's end-to-end PDD fixes while closing the first review's lifecycle gaps: revalidate generated mock contracts after CI/pre-checkup agents edit the worktree, isolate permanent-provider failures to a logical workflow while still propagating them to child processes, and record fail-closed fingerprint-finalization errors as failed operations rather than successful audit entries.

Failure modes

  1. [P1] The pre-commit validator and the CI commit operate on different file sets, so unvalidated dirty files can be pushed.

    • Trigger: The workflow starts with a pre-existing modified/untracked production or test file (the initial hash snapshot intentionally records it), and a later CI/pre-checkup remediation edits some other file.
    • Failure: _detect_changed_files(..., initial_file_hashes) excludes the pre-existing dirty file, so the new mock-contract hook does not validate it; _commit_ci_fix then stages every modified/untracked non-artifact file and pushes the excluded file too. This can silently publish user-owned out-of-scope changes and can still push a schema-divergent query/mock pair despite the new gate.
    • Evidence: The CI hook validates only workflow deltas at pdd/agentic_e2e_fix_orchestrator.py:3749; the pre-checkup path does the same at pdd/agentic_e2e_fix_orchestrator.py:1953; but _commit_ci_fix enumerates and stages the entire dirty worktree at pdd/ci_validation.py:1267 and pdd/ci_validation.py:1281.
    • Suggested regression: Seed one dirty divergent code/test pair before capturing initial_file_hashes, let the remediation agent change a separate file, then assert the actual files_to_commit set is either fully passed to contract validation or excludes the pre-existing files from staging/push.
  2. [P2] The workflow-scoped provider registry is lost at thread/direct-library boundaries.

    • Trigger: Interactive pdd sync runs its orchestration in Textual's worker thread, or a server/library calls a multi-step orchestrator directly without entering provider_failure_scope; one step marks a provider permanently failed and a later step calls run_agentic_task again.
    • Failure: ContextVar values from the CLI thread are not propagated automatically to a new thread, and direct orchestrators have no outer scope. The decorator therefore creates and tears down a fresh registry around each individual call, so the dead provider is retried on every step, reintroducing repeated auth timeouts/budget exhaustion in the very workflows issue Agentic provider selection must fall through on permanent auth errors instead of failing the task (dead codex kills sync despite healthy anthropic/google) #1936 targets.
    • Evidence: The CLI enters the scope only in its current context at pdd/core/cli.py:707; interactive sync explicitly executes the workflow in @work(thread=True) at pdd/sync_tui.py:1341/pdd/sync_tui.py:1388; an unscoped call creates a per-call scope at pdd/agentic_common.py:1923.
    • Suggested regression: Run two run_agentic_task calls inside the actual Textual worker (and separately through a direct orchestrator API): make provider A permanently fail in call 1 and assert call 2 skips A. Propagate a copied context into the worker or establish one scope inside each top-level orchestrator thread.
  3. [P2] Routing can select a known-dead provider and suppress every healthy fallback.

    • Trigger: Provider A failed permanently earlier in the same valid scope, provider B remains installed/healthy, and a subsequent call supplies a routing policy whose selected harness is A.
    • Failure: Routing first replaces the candidate list with [A]; disabled-provider filtering then sees no healthy candidate and returns immediately. Provider B is never attempted and routing escalation/fallback is never reached, so a recoverable task fails with “All agent providers failed permanently earlier this run.”
    • Evidence: Candidate restriction happens at pdd/agentic_common.py:51175127, while disabled filtering occurs afterward at pdd/agentic_common.py:5146 and returns early at pdd/agentic_common.py:51515174.
    • Suggested regression: Within one provider_failure_scope, disable the routing policy's first harness, expose a healthy second provider, and assert the routed call falls back/escalates to that provider. I reproduced the current behavior locally: disabled anthropic + healthy google produced success=False with attempted=[].

Performance

I rescanned the current full diff's provider selection/scope path, TUI worker boundary, remediation loops, mock validator, and metadata logging/finalization. The ContextVar implementation adds no workflow-long lock or GIL-heavy work. The new pre-commit checks can perform O(repository-files) schema/source scans once per CI or pre-checkup repair attempt (plus the existing final check), but they remain terminal remediation work rather than request-handler/event-loop hot paths; no new network/subprocess loop was introduced beyond the existing CI retries. I found no separate release-blocking performance regression, though large-monorepo scan time should remain monitored.

Nits

  • pdd/agentic_common.py:1822 still says the registry is mirrored into the process environment, while the new implementation intentionally exports it only into child provider environments. Updating that comment would keep the concurrency contract unambiguous.

Verdict: changes requested

The three original direct-path defects are fixed, but the boundary cases above remain actionable.

Verification in the repository's pdd Conda environment:

  • Provider/permanent-failure/routing focused selection: 47 passed.
  • CI validation + mock-contract wiring + operation-log suites: 147 passed.
  • E2E remediation/mock-contract focused selection: 8 passed.
  • git diff --check origin/main...HEAD: passed.
  • Additional direct routing reproduction: disabled routed harness with a healthy fallback returned failure without attempting any provider.

@Serhan-Asad

Copy link
Copy Markdown
Collaborator Author

Addressed the second review pass in 9356d1f9a6f5712970ec49eb96dfeecdef31fcdb:

  • CI repair validation and commit now share the same exact allowlist; pre-existing dirty files are excluded and commits use git commit --only.
  • Provider failure state now has an explicit workflow scope, is propagated into the Textual worker thread, and is shared by direct multi-step orchestrator calls without leaking between workflows.
  • Disabled providers are filtered before routing-policy pinning, so an unavailable preferred harness can fall back to a healthy provider.
  • Refreshed canonical metadata fingerprints for all touched generated modules; the local drift audit reports 14/14 in sync, with 0 conflicts, failures, stale metadata, or unbaselined modules.

Validation:

  • focused regression: 654 passed
  • broader orchestrator regression groups: 2,770 passed, 2 skipped
  • exact dry-run drift audit: clean
  • git diff --check: clean

pdd checkup --validate-arch-includes continues to report six pre-existing dependency/include mismatches in checkup_review_loop; this patch does not alter those dependency declarations.

@Serhan-Asad

Copy link
Copy Markdown
Collaborator Author

Follow-up CI remediation landed in c39bf07a2: the workflow-scope instruction added to checkup_review_loop_python.prompt had been placed before the metadata header, causing the metadata parser to stop before six existing <pdd-dependency> tags. That made both Run Unit Tests and Public CLI Regression fail the same architecture/include gate.

The instruction is now in the prompt body, the canonical fingerprint is refreshed, pdd checkup --validate-arch-includes reports no mismatches, and the exact module drift audit is clean.

@Serhan-Asad

Copy link
Copy Markdown
Collaborator Author

Third review — reviewed commit: 9356d1f9a6f5712970ec49eb96dfeecdef31fcdb

Intent

This revision keeps the full campaign bundle while completing the prior lifecycle repairs: make CI/pre-checkup validation and commit operate on one workflow-owned file allowlist, preserve provider-failure state across direct orchestrator and Textual worker boundaries without leaking it into independent workflows, and filter known-dead providers before a routing policy pins its preferred harness so healthy providers remain usable.

Failure modes

  1. [P1] The final drift-sync publisher can still commit unrelated pre-staged changes.

    • Trigger: A user has an unrelated file staged before the e2e-fix workflow, and the pre-checkup drift-sync phase produces a legitimate prompt/example edit.
    • Failure: The new CI helper correctly uses git commit --only, but the final drift-sync path still calls _commit_and_push, which stages workflow paths and then runs a plain git commit. Git includes the entire existing index, so the unrelated pre-staged file is silently committed and pushed even though the final mock-contract check did not validate it.
    • Evidence: The final publisher calls _commit_and_push at pdd/agentic_e2e_fix_orchestrator.py:3881; that helper stages selected paths at pdd/agentic_e2e_fix_orchestrator.py:1809 but commits without --only/a pathspec at pdd/agentic_e2e_fix_orchestrator.py:1822. The exact protection added to _commit_ci_fix is visible at pdd/ci_validation.py:1295 and should be reused here.
    • Suggested regression: Start a temp repository with one unrelated pre-staged file plus one workflow-owned drift edit, invoke the final publisher, and assert only the owned path appears in the new commit while the unrelated path remains staged.
  2. [P2] Direct one-session sync still starts a fresh provider epoch for each repair attempt.

    • Trigger: A server/library calls run_one_session_sync directly (not through the Click CLI or a decorated outer orchestrator), the first agentic attempt permanently fails provider A before falling through, and a surface/test-churn repair causes another outer attempt.
    • Failure: run_agentic_task auto-scopes each standalone call, but run_one_session_sync itself is not decorated. Its next churn attempt therefore forgets A's permanent failure and retries the dead provider, restoring repeated auth timeouts/budget burn in the cloud one-session workflow explicitly cited by issue Agentic provider selection must fall through on permanent auth errors instead of failing the task (dead codex kills sync despite healthy anthropic/google) #1936.
    • Evidence: run_one_session_sync is an undecorated public workflow at pdd/one_session_sync.py:546; it can call run_agentic_task repeatedly in the loop at pdd/one_session_sync.py:786/pdd/one_session_sync.py:805; standalone calls create and tear down a scope at pdd/agentic_common.py:1928. The other public multi-step orchestrators are now decorated with provider_failure_workflow, but this one is not.
    • Suggested regression: Invoke run_one_session_sync directly, force a permanent provider-A failure followed by a gate-driven retry, and assert the second run_agentic_task call excludes A. Decorate this workflow (or the shared sync entry point) with provider_failure_workflow.
  3. [P2 logging] All-disabled routed calls now emit no routing audit record.

    • Trigger: A call supplies routing_policy, but every feasible provider has already been marked permanently failed in the current workflow.
    • Failure: Disabled filtering correctly stops execution, but it happens before select_config initializes routing_record. _emit_routing_outcome(None, ...) is a no-op, so the routed failure leaves no JSONL routing record, breaking audit/observability for exactly the provider-outage path operators need to diagnose.
    • Evidence: routing_record starts as None at pdd/agentic_common.py:5101; the all-disabled early return occurs at pdd/agentic_common.py:51265149; routing selection does not run until pdd/agentic_common.py:5157. I reproduced this with disabled Anthropic+Google and default_policy(): the call failed as expected but produced zero routing-*.jsonl records.
    • Suggested regression: Call run_agentic_task with a routing policy and all feasible providers disabled, then assert one failed routing record includes the selected config/fallback reason and zero provider cost.

Performance

I scanned the revised allowlist/commit loop, provider scope/decorator path, Textual worker boundary, routing selection, mock-contract scans, and metadata finalization. The provider changes use ContextVars without workflow-long locks; the allowlist adds only O(number-of-dirty-files) set/filter work around existing Git subprocesses. Mock-contract validation remains O(repository files) and can run per remediation attempt, but it is terminal workflow work rather than an event-loop/request hot path. No separate release-blocking performance regression was found.

Nits

None beyond the behavioral findings above.

Verdict: changes requested

The previous three findings are fixed on their cited paths, but the adjacent publisher/scope/logging cases above remain actionable.

Verification against 9356d1f9a6f5712970ec49eb96dfeecdef31fcdb in the pdd Conda environment:

  • Provider/scope/routing focused selection: 49 passed.
  • Sync TUI provider-worker selection: 1 passed.
  • CI/mock-contract/operation-log remediation selection: 10 passed.
  • Full CI validation + mock wiring + operation log + sync TUI suites: 174 passed.
  • git diff --check origin/main...9356d1f9a6f5712970ec49eb96dfeecdef31fcdb: passed.
  • Direct all-disabled routed-call reproduction: failure returned correctly, but routing audit record count was 0.

@Serhan-Asad

Copy link
Copy Markdown
Collaborator Author

Addressed the third review pass in cb500dd0b:

  • _commit_and_push now commits its computed workflow allowlist with git commit --only ... -- <files>, preserving unrelated pre-staged index entries.
  • run_one_session_sync now owns a provider_failure_workflow scope, so direct server/library repair attempts share dead-provider state and reset it at workflow exit.
  • routed calls now select their routing record before provider-health filtering but defer config application; an all-disabled fail-fast emits a failed zero-cost audit row with fallback_reason=all_feasible_providers_disabled.
  • prompt sources and canonical fingerprints were updated for all three modules.

Validation:

  • 530 provider/routing tests passed
  • 109 one-session tests passed
  • 400 E2E/commit-scope tests passed; the sole environment-dependent test passed separately with PDD_PATH set
  • architecture/include validation: clean
  • exact drift audit: 3/3 in sync, 0 conflicts/failures/stale/unbaselined
  • git diff --check: clean

@Serhan-Asad

Copy link
Copy Markdown
Collaborator Author

Fourth review — reviewed commit: cb500dd0b0ddf718b8fb79f5488e9ff4db4d330f

Intent

This current head completes the campaign bundle and closes the remaining audit gaps: keep workflow commits scoped away from unrelated staged entries, give direct one-session sync one provider-health epoch across repair attempts, emit routing telemetry even when all feasible providers are already disabled, and restore the canonical % Write ... metadata header at the beginning of the checkup review-loop prompt without losing the provider-scope requirement.

Failure modes

I found no actionable findings. I explicitly exercised exactly these three concrete production failure modes from the prior pass:

  1. Unrelated pre-staged content enters a workflow commit.

    • Trigger: The caller has an unrelated staged file when drift-sync produces a workflow-owned edit.
    • Failure if unprotected: A plain git commit would silently publish the caller's staged content with the automated fix.
    • Evidence/result: The publisher now commits with git commit --only ... -- <files_to_commit> at pdd/agentic_e2e_fix_orchestrator.py:1823. The real-Git regression test confirms the unrelated file remains staged while only the owned file reaches HEAD. This failure mode is mitigated.
  2. A direct one-session repair retries a provider already known permanently dead.

    • Trigger: run_one_session_sync is called as a library API, provider A fails permanently on attempt one, and the churn/surface gate requests another attempt.
    • Failure if unprotected: Each standalone run_agentic_task call would start fresh, re-burning A's auth timeout and budget.
    • Evidence/result: The workflow now owns a shared epoch via @provider_failure_workflow at pdd/one_session_sync.py:546, enclosing the retry loop and agent calls at pdd/one_session_sync.py:787/pdd/one_session_sync.py:806. The regression test observes {} on attempt one and {"openai": "auth"} on attempt two, then confirms the scope resets after return. This failure mode is mitigated.
  3. An all-disabled routed request fails without an audit record.

    • Trigger: A routing policy is supplied after every feasible provider has been marked permanently failed in the current workflow.
    • Failure if unprotected: The call correctly fails fast but leaves no routing JSONL, hiding the selected config and outage reason from operators.
    • Evidence/result: Routing selection now initializes the record before feasibility filtering at pdd/agentic_common.py:5126; the all-disabled branch records all_feasible_providers_disabled and emits the failed zero-cost outcome at pdd/agentic_common.py:51435161. The regression test confirms exactly one failed routing record. This failure mode is mitigated.

Performance

I scanned the final commit/publish path, one-session retry loop, provider ContextVar scopes, routing selection/filtering, mock-contract validation, metadata finalization, and prompt/include validation. The new commit scoping changes only the existing Git argv; the one-session decorator adds constant-time ContextVar entry/exit; routing still filters the bounded provider list once. No new unbounded collection loop, network/disk I/O in a hot callback, event-loop blocking, or lock contention was introduced. The pre-existing mock-contract repository scan remains terminal remediation work rather than a request/event-loop hot path.

Nits

None.

Verdict: ready

The three previously reported findings are fully resolved, the metadata-header fix is consistent with the prompt contract, and I found no new correctness, security, concurrency, API, logging, data-loss, or performance regression.

Verification at cb500dd0b0ddf718b8fb79f5488e9ff4db4d330f in the pdd Conda environment:

  • Provider/scope/routing focused selection: 49 passed.
  • Real-Git staged-entry + direct one-session provider-scope regressions: 2 passed.
  • One-session + CI validation + mock wiring + operation log + sync TUI suites: 283 passed.
  • pdd checkup --validate-arch-includes: passed (No architecture / <include> mismatches found).
  • git diff --check origin/main...HEAD: passed.

Serhan-Asad and others added 2 commits July 11, 2026 14:41
…ed-test churn

Two gaps in the issue #1903 implementation surfaced in review of PR #1998:

1. Greenfield (§A) was unimplemented: with no existing co-located test, PDD
   kept its runner-blind `tests/` shadow. Add `find_runner_collected_test_path`
   (content_selector): when the project configures a jest/vitest runner
   (config file or package.json declaration, walking up to the repo root) and
   the module is JS/TS, write the FIRST test to the runner-collected
   `{module_dir}/__test__/{stem}.test.{ext}` instead of a `tests/` orphan.
   Wired into `resolve_test_output_path`'s no-sibling branch, so `pdd
   generate`/`change`/`sync` all target the collected path. Python keeps its
   pytest-idiomatic `tests/` default; user pins are still honored; total/
   never-raising with CWE-022 containment reused.

2. Never-block (§B.4) was not honored: an unreconcilable (coverage-losing)
   adopted human test hard-failed the issue-driven workflow. In the agentic
   sync runner, on test-churn exhaustion (the child already restored the human
   test and one-session's #2208 coverage-preserving auto-accept already
   refused) keep the original test, emit `PDD_TEST_CHURN_NEEDS_REVIEW`, set
   `ModuleState.needs_review`, and report the module synced so the PR opens
   with the test flagged "needs review" in the progress comment / PR body.
   Scoped to the issue-driven runner only — standalone `pdd test`/`pdd sync
   <module>` keep the strict hard-fail (the safe default outside a PR flow).

Prompts, fingerprints, README (§1122), and regression tests updated together.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The #1941 same-family auto-degrade only ran inside the opt-in
`_maybe_run_fallback_reviewer` else-branch, so it never fired for the
accepted/live config (reviewer=codex, fixer=codex, reviewer_fallback=claude,
codex family down). There the EXPLICIT reviewer_fallback branch promotes claude
to reviewer but falls straight through to the fix round, which then targets the
dead configured fixer (codex) and re-deadlocks with "findings remain".

Apply the degradation after the explicit reviewer_fallback promotion as well:
when the configured fixer IS the just-failed primary's identity (its family is
the one that went down) and the fallback review carries actionable findings,
promote the surviving fallback family to a fresh same-family fixer
(`state.active_fixer`), stamp `same_role_review_fix=true` and
`role_independence="degraded (<primary> unavailable)"`, and let the normal fix +
required fresh-verify run. Automatic for every consumer — no
`--fallback-reviewer-on-failure` opt-in. Trigger is narrow (fixer == the
demonstrably-failed identity) so an independent cross-family fixer is still
preferred and never bypassed.

Adds a regression for the exact acceptance config the prior #1941 test missed
(it used fixer=claude, which already reached the else-branch). Prompt and
fingerprint updated alongside the generated module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Serhan-Asad

Copy link
Copy Markdown
Collaborator Author

Addressed the three P1 review findings on stack/e2e-staging-v2 (commits f775afe, 7a5d3b9).

Finding 1 — greenfield runner-blind tests (#1903 §A): added find_runner_collected_test_path in content_selector. When no co-located test exists yet but the project configures a jest/vitest runner (config file or package.json declaration, walking up to the repo root) and the module is JS/TS, resolve_test_output_path now returns the runner-collected {module_dir}/__test__/{stem}.test.{ext} instead of a tests/ shadow — so generate/change/sync write the first test where the runner looks. Python keeps its pytest tests/ default; pins honored; total/CWE-022-safe. New acceptance fixture drives the real construct_paths (greenfield Next.js/jest, no .pddrc override → co-located path, never a root orphan) plus find_runner_collected_test_path unit tests.

Finding 2 — unreconcilable adopted test blocked the workflow (#1903 §B.4): scoped to the agentic issue-driven runner (per maintainer decision — standalone pdd test/pdd sync <module> keep the strict hard-fail, and #2208 coverage-preserving auto-accept remains the first recovery). On test-churn exhaustion, agentic_sync_runner now keeps the (already-restored) human test, emits PDD_TEST_CHURN_NEEDS_REVIEW, sets ModuleState.needs_review, and reports the module synced so the PR opens with the test flagged needs review in the progress comment / PR body. README §1122 updated; regression added.

Finding 3 — #1941 degrade attached to the wrong path: the same-family auto-degrade now also fires after the explicit reviewer_fallback promotion (not just the opt-in _maybe_run_fallback_reviewer else-branch). When the configured fixer IS the just-failed primary's identity and the fallback review has actionable findings, the surviving fallback family becomes active_fixer with same_role_review_fix=true + role-independence: degraded (<primary> unavailable). Automatic (no --fallback-reviewer-on-failure opt-in); narrow trigger keeps a real cross-family fixer preferred. Regression covers the exact acceptance config (reviewer=codex, fixer=codex, reviewer_fallback=claude) the prior test missed.

Each fix updates prompt + generated module + fingerprint + tests together; fingerprints verified self-consistent with sync_determine_operation's own hashing. Local focused suites green: test_issue_1903_adopt_collocated_test, test_content_selector, test_checkup_review_loop, test_agentic_sync_runner, test_one_session_sync, test_cmd_test_main, test_sync_determine_operation (1021 passed, 2 skipped).

Note: the previously-flagged red CI (test_sync_worker_seeds_parent_provider_failure_scope, a Textual @work event-loop/xdist ordering issue) is unrelated to these files.

Serhan-Asad and others added 3 commits July 11, 2026 14:59
…ft-resolution

Fix nested checkup drift input resolution
…lock

Address three follow-up review refinements on the #1903 fixes:

1. Honor custom jest/vitest config (§A). `find_runner_collected_test_path` no
   longer hardcodes `__test__/{stem}.test.{ext}`. It reads JSON-readable config
   (package.json `jest`/`vitest`, `jest.config.json`, `.jestrc`) and honors
   `testMatch`/`testRegex` (choose `.test`/`.spec` + `__test__`/`__tests__`),
   `roots`/`rootDir` (containment), and `testPathIgnorePatterns` (exclusion) via
   a small micromatch→regex subset. If NO co-located candidate would be
   collected (centralized layout, roots excluding the module), it falls back to
   the derived path — never emits an uncollected test. JS-only configs
   (unparseable) keep the jest-default convention, which the default testMatch
   collects.

2. Scope the never-block to ADOPTED co-located tests (§B.4). The test-churn
   never-block now fires only when the churned test is a co-located human test
   (`_is_adopted_collocated_test_path`: `.test.`/`.spec.` or under
   `__test__`/`__tests__`). A PDD-owned `tests/test_*.py` shadow keeps the
   strict hard-fail even in the issue-driven runner, so coverage loss on a
   PDD-owned test is never silently swallowed.

3. Persist `ModuleState.needs_review` in `_save_state`/`_load_state` so a
   durable resume does not drop the flag.

Prompts, fingerprints, README (§1108/§1122), and regressions updated (glob
engine + resolver validated against real jest configs; PDD-shadow-keeps-hardfail
and resume-persists-flag cases added).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Serhan-Asad

Copy link
Copy Markdown
Collaborator Author

Re: the last review pass — the head had advanced to 6679049 because PR #2002 was merged on top of my commits, not because my commits were absent. f775afe (#1903 greenfield + never-block) and 7a5d3b9 (#1941 auto-degrade) are in the branch history; the review base just landed on the #2002 merge. That said, the three technical points are valid refinements and are now addressed in ac3e7b5 (head → ac3e7b5d9):

1. Custom jest/vitest rootDir/roots/testMatch (§A). find_runner_collected_test_path no longer hardcodes __test__/*.test.tsx. It reads JSON-readable config (package.json jest/vitest, jest.config.json, .jestrc) and honors testMatch/testRegex (picks .test/.spec + __test__/__tests__), roots/rootDir (containment), and testPathIgnorePatterns (exclusion) via a small micromatch→regex subset. If no co-located candidate would be collected (centralized layout, or roots excluding the module dir) it falls back to the derived path — it can no longer emit an uncollected test. JS-only configs (jest.config.js, unparseable in Python) keep the jest-default convention, which the default testMatch collects. Glob engine + resolver validated against real jest patterns (default ?(*.)+(spec|test).[jt]s?(x), **/__tests__/**, <rootDir>/src/**/*.test.{ts,tsx}, centralized, roots include/exclude, testPathIgnorePatterns).

2. Never-block scoped to adopted tests (§B.4). The relief now fires only when the churned test is a co-located human test (_is_adopted_collocated_test_path: .test./.spec., or under __test__/__tests__). A PDD-owned tests/test_*.py shadow keeps the strict hard-fail even in the issue-driven runner, so coverage loss on a PDD-owned test is never silently swallowed. New regression asserts the shadow-keeps-hard-fail path.

3. needs_review persisted. Added to _save_state/_load_state, so a durable resume no longer drops the flag. New round-trip regression.

Prompts, fingerprints (self-consistent with sync_determine_operation), and README (§1108/§1122) updated alongside. Local focused suites: 713 passed, 1 skipped (test_issue_1903_adopt_collocated_test, test_content_selector, test_agentic_sync_runner, test_one_session_sync, test_sync_determine_operation, test_cmd_test_main).

…riven gate

Address the round-3 review of the greenfield/never-block fixes:

1. Centralized layouts no longer false-green. When co-location is not collected
   (a `roots`/`rootDir` or `testMatch` fixed-directory layout), the resolver now
   DERIVES a collected path *under* the configured directory instead of falling
   back to the runner-blind `tests/` shadow. New tests assert the derived path
   satisfies the configured testMatch/roots.

2. Custom JS/TS config files are respected. A `jest.config.js` is unparseable in
   Python, so its text is inspected: if it references a discovery key
   (testMatch/roots/rootDir/testRegex/testPathIgnorePatterns) the resolver
   conservatively REFUSES to claim a path is collected (falls back to derived)
   rather than assuming jest defaults; a plain JS config with no discovery keys
   keeps the default convention (which jest collects).

3. Never-block gated to the issue-driven workflow. The relief now requires
   `self.issue_url` (a real GitHub issue -> PR sync) in addition to the adopted
   co-located classifier. Project-wide `pdd sync` (issue_url=None, no PR) keeps
   the strict test-churn hard-fail, closing the silent-bypass path. New tests
   cover the global-sync-hard-fail case; existing never-block/shadow tests now
   chdir to isolate the resume-state file.

Prompts, README (§1108/§1122), architecture.json (content_selector +
agentic_sync_runner descriptions), and fingerprints updated together. Micromatch
subset + resolver validated against real jest configs (default, centralized,
roots include/exclude, spec-only, opaque JS custom vs plain).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Serhan-Asad

Copy link
Copy Markdown
Collaborator Author

Round-3 review addressed in 26a7891 (head → 26a789173).

1. Centralized layouts no longer false-green (P1-1). When co-location isn't collected (a roots/rootDir or testMatch fixed-directory layout), the resolver now derives a collected path under the configured directory rather than returning the runner-blind tests/ shadow. E.g. testMatch: ["<rootDir>/test/**/*.test.ts"]test/__test__/util.test.ts; roots: ["<rootDir>/test"] with a module under src/test/__test__/page.test.tsx. New tests assert the derived path satisfies the configured testMatch/roots (the old tests that accepted the shadow are replaced).

2. Custom JS/TS configs respected (P1-2). A jest.config.js is unparseable in Python, so its text is inspected: if it references a discovery key (testMatch/roots/rootDir/testRegex/testPathIgnorePatterns) the resolver conservatively refuses to claim a path is collected (falls back to derived) instead of assuming jest defaults; a plain JS config (no discovery keys) keeps the default convention, which jest's default testMatch collects. (I did not shell out to jest --showConfig — it needs node + installed deps and would add a hang-prone subprocess to path resolution, against this repo's interactive_only/non-interactive-safety posture. The text-inspection + refuse-when-uncertain path is the conservative static equivalent.)

3. Never-block gated to the issue-driven workflow (P1-3). Confirmed: agentic_sync.py builds this runner with issue_url=None for project-wide sync. The relief now requires self.issue_url set (a real issue → PR sync) AND the adopted-co-located classifier — so global pdd sync keeps the strict hard-fail (closing the silent bypass). New test_test_churn_global_sync_no_issue_url_still_hard_fails; the existing never-block/shadow tests now set issue_url and chdir to isolate the resume-state file.

On the provenance sub-point (infer ownership from filename): with the issue_url gate in place, the only remaining case is a PDD-created greenfield co-located test that later churns coverage-losingly inside an issue-driven PR run — where it is flagged needs review in the PR body (ModuleState.needs_review, now persisted across resume), i.e. surfaced for human review, not silently bypassed. That matches #1903's stated model ("a human reviewing the PR is the normal flow"). Full authored-vs-generated provenance would require threading an adoption signal from the child sync up through the subprocess boundary; I judged that disproportionate given the flag is always human-visible and the silent-bypass path (global sync) is now closed — happy to add it if you'd prefer the stricter contract.

Also updated: both prompts, README §1108/§1122, and the stale architecture.json descriptions for content_selector + agentic_sync_runner. Fingerprints refreshed + verified self-consistent. Local suites: 716 passed, 1 skipped.

Serhan-Asad and others added 11 commits July 11, 2026 16:54
…rovenance, prompt altitude

Address gpt-5.6-sol (xhigh) review findings on the #1903/#1941 changes:

#1903 correctness (content_selector greenfield):
- Centralized layouts no longer collapse same-stem modules onto one path:
  derive the centralized candidate under the anchor MIRRORING the module's
  relative dir (`_module_rel_dir`), so `src/a/util.ts` and `src/b/util.ts` get
  distinct collected tests — never fork/overwrite.
- Cover the vitest dialect and jest negation: the opaque-JS discovery scan now
  matches jest AND vitest keys (whole-word, so `__dirname` doesn't trip);
  `_candidate_is_collected` applies jest's ordered include/exclude semantics (a
  leading-`!` glob removes matches); parseable configs using `projects`/
  `include`/`exclude` we can't resolve are refused rather than false-greened.

#1903 provenance (agentic_sync_runner never-block classifier):
- `_is_adopted_collocated_test_path` now rejects absolute paths, `..` traversal
  (CWE-022), and PDD-owned `tests/` shadows (JS `tests/foo.test.ts` as well as
  `tests/test_*.py`) — closing the over-acceptance the `.test.`/`.spec.`
  substring heuristic allowed. It remains a NECESSARY (not sufficient) gate
  composed with the issue_url guard + coverage-preserving auto-accept.

#1941 prompting-guide altitude:
- Rewrite the reviewer_fallback auto-degrade spec at behavior level per
  docs/prompting_guide.md (Concision Rules 3/8, Back-Propagation Altitude):
  drop private helper names, exact state assignments, and internal step
  ordering; keep the observable trigger, outcome, disclosure, and failure
  reason.

Prompts, README §1108, fingerprints, and regressions updated in lockstep
(centralized-collision, vitest-custom/plain, jest-negation, jest-projects,
classifier traversal/absolute/JS-shadow). Local suites: 691 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ge cases, prompt altitude

Address the 8 gpt-5.6-sol (xhigh) findings on the #1903 changes:

Never-block classifier (agentic_sync_runner):
- CRITICAL: production churn `output:` paths are ABSOLUTE (construct_paths);
  the round-4 classifier rejected all absolutes and broke the real never-block.
  Now accept an absolute path only after canonical containment in the worktree
  root (passed from `_sync_one_module`), reduce to repo-relative, then apply the
  traversal/shadow/co-location checks. Regressions for absolute in-root (never-
  block) and absolute out-of-root (hard-fail).

Greenfield discovery (content_selector) — eliminate false-greens:
- Delegating/composed JS configs (`require`/`import`/spread/`preset`/`extends`/
  function) are unresolvable -> refuse (a plain literal is required to assume
  defaults).
- Character-class negation `[!x]` now translates to `[^x]` (was "! or x").
- `testMatch` is evaluated with jest's true SEQUENTIAL order (a later positive
  re-includes after an earlier negation; last match wins).
- rootDir-only centralized layouts derive a path UNDER rootDir (previously fell
  through to the runner-blind shadow).
- Default discovery only co-locates for js/jsx/ts/tsx (jest ignores mjs/cjs).

Prompt altitude (#1903 §A, content_selector prompt):
- Rewrite the greenfield spec at behavior/contract level per
  docs/prompting_guide.md (Concision Rules 3/8): drop private-helper recipes,
  glob/regex mechanics, and internal ordering; keep observable invariants
  (never emit an uncollected path, refuse when unresolvable, honor
  collection rules, centralized non-collision, total).

Prompts, README §1108, fingerprints, and regressions updated in lockstep.
Local suites: 700 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full test-authorship provenance threading (issue #1903 §B.4 never-block):
- Capture "adopted from an existing HUMAN co-located test (unpinned)" at PATH
  RESOLUTION time (before generation makes greenfield vs adopted
  indistinguishable): new `content_selector.was_test_adopted`. Thread it as
  `adopted_human` through `_verify_test_churn` -> `TestChurnError` (new field +
  `adopted:` message line) at the cmd_test_main and one_session_sync churn
  sites, into `build_test_churn_hard_failure_from_error`'s block.
- The parent runner now requires THREE guards to never-block: issue_url +
  structured `adopted: true` provenance (`_extract_test_churn_adopted`) + the
  in-repo co-located path classifier. A pinned/greenfield/older-child test reads
  false -> strict hard-fail.

Codex round-3 correctness (content_selector greenfield):
- Ambiguous (>1) existing co-located tests are no longer treated as greenfield
  (would fork a third file) — `resolve_test_output_path` keys on cardinality.
- Explicit `testMatch` that matches nothing (or an unevaluable rule) FAILS
  CLOSED instead of falling through to an uncollected default path.
- Refuse vitest `root`/`dir` discovery and non-literal/call-expression configs
  (`module.exports = buildConfig()`, delegating requires) — can't prove default.
- Character-class negation `[!x]` -> `[^x]` (was "! or x").
- Default-extension gate is dialect-aware (vitest's default collects mjs/cjs).
- Churn-block field extraction is scoped to the block and fails closed on
  conflicting `output:`/`adopted:` values (no unrelated-line leakage).

#1941: the explicit-fallback auto-degrade no longer fires in review-only mode
(which runs no fixer) — `not config.review_only` guard.

Prompts, README, fingerprints (6 modules), and regressions updated in lockstep.
Local suites: 1243 passed, 2 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…handling, prompt altitude

- SECURITY (ReDoS): repo-controlled testMatch/testRegex/testPathIgnorePatterns
  are now matched through a timeout-bounded, fail-closed helper (the `regex`
  module's per-match timeout + length caps), so a catastrophic-backtracking
  runner pattern can never stall sync. All three pattern evaluations route
  through it. Adversarial regression added.
- Jest-version-aware default extensions: Jest 30's default testMatch collects
  .mjs/.cjs (Jest <=29 does not); detect the declared jest major from
  package.json and widen the default-ext set only for jest>=30 or vitest;
  unknown version stays strict.
- Stricter opaque-config refusal: computed property keys (`{['test'+'Match']:…}`)
  and non-literal/identifier exports (`module.exports = config`,
  `export default defineConfig(...)`) are no longer assumed default — refuse.
- Prompt altitude: rewrite the churn-block extraction spec at behavior level
  (drop exact regexes / lock placement / internal call structure) per
  docs/prompting_guide.md.

(Round-4 finding #5 "stale fingerprints" was a false positive — the stored hash
matches PDD's canonical calculate_prompt_hash; the reviewer compared a plain
sha256.) Fingerprints refreshed; regressions added. Local suites: 391 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…und, docs lockstep

Correctness (content_selector greenfield):
- bare `roots` entries resolve against the effective `rootDir` (jest semantics),
  not the config dir — no longer certifies a test jest would exclude.
- explicit-empty `testMatch`/`testRegex` (`[]`) matches nothing -> refuse; a
  config with BOTH testMatch and testRegex (jest rejects) -> refuse.
- runner detection no longer false-positives on any script containing the
  substring "jest"/"vitest": only `test`/`test:*` scripts, real command tokens.
- extglob vs brace alternation: commas are literal inside `@(...)`/`+(...)`
  extglobs (pipe alternates) and only alternate inside `{...}` braces.

Security/DoS:
- aggregate discovery work is bounded (pattern-count + candidate caps) so a
  hostile large config can't stall an issue-driven sync for minutes even with
  the per-match ReDoS timeouts.

Lockstep/docs:
- the runner's OWN structured churn block now renders the `adopted:` provenance
  field (not just the standalone builder).
- content_selector prompt + README document the version-aware mjs/cjs rule
  (jest 30+/vitest collect; jest<=29/unknown refuse); README + architecture.json
  now describe all THREE never-block guards (issue scope + adoption provenance +
  co-located shape).

Regressions added for each. Local suites: 397 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…undness, DoS budget, symlink containment

Security:
- Forgeable never-block signal: the churn `output:`/`adopted:` provenance is now
  required to be UNANIMOUS across EVERY churn block in the captured child output
  (still scoped to churn blocks). Since the never-block branch is only entered on
  a REAL churn, an injected/forged block that disagrees causes a conflict ->
  fail closed (strict hard-fail); an attacker cannot flip a real `adopted:false`
  to `true`. (A fully in-band signal is not attacker-proof; this defaults to the
  safe outcome and raises the bar substantially.)
- Relative churn paths are now canonicalized against the worktree root and
  containment-checked (rejecting a symlink segment that escapes the root), not
  just lexically screened for `..` (CWE-022).
- Aggregate discovery work is bounded by a single wall-clock BUDGET across the
  whole call (plus tighter per-match timeout and element caps), so a flood of
  near-timeout patterns can't accumulate minutes.

Correctness:
- Opaque JS config detection fails closed on dynamic construction (method calls,
  template literals, computed keys, identifier/call exports) that a token
  blacklist missed — a config that builds `testMatch` dynamically is refused.
- Jest/vitest dialect is resolved for the mjs/cjs default: jest<=29 + a vitest
  dependency is AMBIGUOUS -> refuse; only jest>=30 or unambiguous vitest widens.

Regressions added for each. Local suites: 402 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rship, ambiguous-source refusal

- Opaque JS config detection is now a positive WHITELIST: default discovery is
  assumed ONLY for a single plain-object-literal export (`module.exports = {…}` /
  `export default {…}`) with no discovery keys and no dynamic/composition
  constructs — refusing blacklist-evading shapes like a second-statement mutation
  `module.exports = {}; module.exports['test'+'Match'] = […]` (strings/comments
  stripped before analysis).
- Greenfield OWNERSHIP provenance: PDD now records tests it greenfield-creates
  (`.pdd/pdd_created_tests.json`) and `was_test_adopted` excludes them, so a
  PDD-owned greenfield test is never reclassified as human-adopted on a later run
  (would otherwise let coverage-losing churn take the never-block). Recorded at
  the cmd_test_main and one_session creation sites.
- Ambiguous runner config: multiple config sources at the same directory level
  (e.g. an inline package.json jest block AND a vitest.config.ts) -> fail closed,
  so an inline block can't hide an active custom config file.

Prompts (content_selector/cmd_test_main/one_session_sync), fingerprints, and
regressions updated in lockstep. Local suites: 570 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e/tracked provenance

Seven of eight round-8 findings (the eighth, forgeable in-band provenance, is a
cross-process-channel design decision pending):

- #1 Quoted discovery keys: `"testMatch"` was erased by string-stripping and
  slipped past the opaque-config whitelist. Discovery keys are now matched on the
  RAW config text (quoted or not) before the trivial-literal check.
- #2 Multiple config FILES: the collector kept only the first JS/TS config, so
  `jest.config.js` + `vitest.config.ts` read as one source. It now counts every
  config file at a level -> >1 source is ambiguous -> refuse.
- #3 Python adopted siblings: the never-block path classifier rejected
  `src/test_foo.py` / `src/foo_test.py`, hard-failing a supported #1903 Python
  adoption. It now accepts Python sibling conventions outside the top-level
  `tests/` shadow, alongside JS `.test.`/`.spec.`/`__tests__`.
- #5 Ownership durability: the greenfield-ownership manifest moved from the
  routinely-gitignored `.pdd/pdd_created_tests.json` to the TRACKED
  `.pdd/meta/pdd_created_tests.json` (committed with the per-module fingerprints)
  so a fresh checkout / durable worktree keeps PDD's ownership; the legacy path is
  still read as a fallback.
- #6 Durable resume dropped the flag: `DurableSyncRunner` rebuilds state from git
  checkpoint trailers only. The adopted-test `needs_review` note is now encoded
  into the trailer (base64url) and restored on resume, so a resumed run no longer
  reports a clean success and silently swallows coverage loss.
- #7 ReDoS preprocessing: oversized raw `testMatch` globs are now rejected BEFORE
  translation/literal-dir extraction, and the hint scan is bounded, closing the
  gap where expansion ran outside the regex timeout.
- #8 Prompt lockstep: the agentic prompt said "last churn header"; the code parses
  ALL churn blocks with unanimous values. Prompt updated to match.

Prompts (agentic_sync_runner/durable_sync_runner), fingerprints, and regressions
updated in lockstep. Local suites: 225 + 408 passed across the touched files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ance

Closes the recurring forgeable-provenance finding (rounds 5, 6, 8). The never-block
read failure-kind + adoption provenance from the child's in-band stdout/stderr,
which a hostile project test can print — so a self-consistent forged churn block
could downgrade a coverage-loss hard-fail to a "needs review" PR. The round-6
cross-block unanimity only defeated a CONFLICTING injected block, not a lone
self-consistent one.

Now the parent authenticates provenance over a channel untrusted test output
cannot reach:

- The runner mints a secret per-run nonce and hands it to each child sync ONLY
  over a non-inherited pipe FD (`pass_fds`; `PDD_CHURN_NONCE_FD` names it). The
  child inherits the FD; the grandchild test processes it later spawns do not
  (subprocess `close_fds` default), so a hostile test can read the env var but
  not the FD it names — it cannot learn the nonce.
- `TestChurnError` reads that nonce once (cached) and stamps `nonce: <token>` into
  the churn block. Standalone `pdd test`/`sync` (no channel) omit it and never
  never-block.
- `_extract_test_churn_adopted` / `_extract_test_churn_output_path` take an
  `expected_nonce`; the never-block trusts a block's `adopted:`/`output:` ONLY
  when its nonce matches (constant-time). A block without the nonce is dropped,
  not out-voted, so a forgery falls through to the strict hard-fail.

Prompts (agentic_sync_runner/code_generator_main), fingerprints, and regressions
updated in lockstep (forgery-defeat, wrong-nonce, no-nonce, child FD round-trip).
Local suites: 975 passed, 1 skipped across the touched files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rrency, lockstep

Six findings from round-9 review:

- #1 Adoption must consult collection: `resolve_test_output_path` adopted an
  existing co-located sibling by filename alone, so a stale `src/__test__/x.test.ts`
  under a PARSEABLE Jest `testMatch: ["qa/**/*.spec.ts"]` was adopted and verified
  even though the runner excludes it (false-green preserved). It now redirects away
  from a provably-excluded sibling to the runner-collected location (or derived);
  `was_test_adopted` is tightened to require the resolved path BE that sibling, so
  a redirect target is never miscounted as a human adoption.
- #2 Globstar: `**` was translated to `.*` everywhere, but micromatch treats `**`
  as a separator-crossing globstar ONLY as a whole path segment. Embedded `**`
  (`qa**/`, `**bar`) is now a single-segment `[^/]*`, so a pattern can no longer
  certify a path Jest rejects.
- #3 Manifest race: parallel child processes did an unlocked read-modify-write of
  the ownership manifest and clobbered each other. Now under an interprocess
  `flock` + temp-file + atomic `os.replace`.
- #4 State-save race: `_save_state` serialized only the snapshot, not the write.
  The whole snapshot->write->replace now runs under a dedicated `_save_lock` so a
  stale save cannot overwrite a newer one (dropping a needs-review note on resume).
- #5 Prompt lockstep: content_selector prompt named the obsolete
  `.pdd/pdd_created_tests.json`; updated to the canonical tracked
  `.pdd/meta/pdd_created_tests.json` (legacy read-only fallback).
- #6 Prompt/README/architecture lockstep: documented the Python
  `test_<stem>.py` / `<stem>_test.py` never-block classifier forms added in round 8.

Prompts, README, architecture.json, fingerprints, and regressions updated in
lockstep. Local suites: 980 passed, 1 skipped across the touched files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… node_modules ignore, per-block fail-closed, prompt altitude

Six findings from round-10 review:

- #1 Ownership manifest durability (recurring): `.pdd/meta/pdd_created_tests.json`
  was still gitignored and never staged into durable checkpoints, so a fresh
  checkout / durable worktree lost provenance. Added a `.gitignore` exception and
  taught the durable runner to force-add the shared (non-module-prefixed) manifest
  and accept it in checkpoint-staging validation.
- #2 Jest semver: `_detected_jest_major` took the first integer, so `<30` read as
  30 and `^30 || ^29` as 30 — enabling jest-30 `.mjs`/`.cjs` defaults on installs
  that could be <=29 (false-green). Now computes a conservative guaranteed-MINIMUM
  major across the full `||` range; jest-30 defaults enable only when the whole
  range provably guarantees major >= 30.
- #3 Default ignore: greenfield discovery accepted a co-located `.test.*` candidate
  under `node_modules`, which both jest and vitest exclude by default. Such
  candidates are now refused.
- #4 Per-block fail-closed: the churn extractors aggregated fields across trusted
  blocks into a set, so a trusted block MISSING output/adopted was covered for by a
  complete one. Each trusted block is now validated independently (exactly one
  value) before cross-block unanimity, honoring "any absence/conflict fails closed".
- #5 Prompt altitude: requirement 9e transcribed private helpers, `pass_fds`, the
  FD/env mechanics, constant-time compare, and lock placement — against the
  prompting guide's Back-Propagation Altitude. Rewritten as concise behavioral
  rules (unforgeable provenance, fail-closed parsing, persisted review, observable
  output) with the mechanics dropped from the helper note too.
- #6 Prompt accuracy: content_selector prompt claimed centralized runner paths are
  re-adopted by `find_collocated_test`; they are not — reworded to the actual
  behavior (deterministic re-derivation + ownership-manifest tracking).

Prompts, .gitignore, fingerprints, and regressions updated in lockstep. Local
suites: 985 passed, 1 skipped across the touched files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
f"repair — kept the existing test (`{kept}`) unchanged and "
f"flagged it for review (issue #1903); the PR still ships."
)
print(f"{_TEST_CHURN_NEEDS_REVIEW_MARKER}: {kept}", flush=True)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants