fix: relocatable hook commands, pivot-safe update, detection-repair loop (#4469) - #4473
Open
Trecek wants to merge 33 commits into
Open
fix: relocatable hook commands, pivot-safe update, detection-repair loop (#4469)#4473Trecek wants to merge 33 commits into
Trecek wants to merge 33 commits into
Conversation
Kills the absolute-hook-path ENOENT class (issue #4469): every published hooks.json now emits python3 "${CLAUDE_PLUGIN_ROOT}/hooks/_dispatch.py" <name> instead of a pkg_root()-derived absolute path, so hook validity is a property of the plugin artifact rather than of the venv interpreter, install path, or continued existence of the process that generated it. A-I1: _build_hook_command gains an explicit relocatable=True/False mode (hooks.json vs settings.json) instead of a module-level HOOKS_DIR default silently deciding the destination. generate_hooks_json() emits the relocatable form unconditionally; sync_hooks_to_settings() keeps baking an absolute path (settings.json is per-machine, never redistributed, and already enforced by test_registered_hooks_use_absolute_paths). Codex hook generation (no expansion-token equivalent) gains resolve_codex_hooks_dir(), preferring the retained versioned plugin-cache incarnation when installed and falling back to the dev-source checkout otherwise. A-I2: find_broken_hook_scripts and validate_plugin_cache_hooks are now token-aware — shlex-based parsing (the quoted relocatable form defeats bare .split()) with an explicit expansion_root parameter, fail-closed when a token-bearing command has no root to expand against. Also fixes a real production bug uncovered while implementing this: validate_plugin_cache_hooks globbed one level too shallow (*/hooks.json instead of */hooks/hooks.json), so it silently found nothing against every real installed cache — the startup/doctor detection this powers was a no-op. Several test fixtures across test_plugin_cache.py and test_doctor.py encoded the same wrong one-level layout and are corrected in the same change. A-I3: documents the two-form boundary (hooks.json = token, settings.json = absolute) in cli/_hooks.py and hooks/AGENTS.md. A-I5: catalog_projection_context() gains an explicit durable_scripts_root parameter (defaulting to pkg_root() for backward compatibility with every existing caller) so a projected skill document's {{AUTOSKILLIT_SCRIPTS}} placeholder need not always reference the venv tree, deletable mid-session by a concurrent autoskillit update. cli/session/_session_cook.py resolves interactive_plugin_authority() before projection and, for the IMPLICIT_INSTALLED load mode (marketplace-plugin sessions — the exact scenario this protects), supplies the eagerly-resolvable retained plugin cache root via current_installed_plugin_root() instead of pkg_root(). Other load modes keep pkg_root() explicitly, since their true destination is only resolved later via a leased plugin artifact binding inside the interactive retry loop; pulling that binding acquisition earlier would change its lease lifetime across retries and is deliberately not attempted here. Adds tests/contracts/test_hook_path_relocatability.py (T-A1..T-A6) and updates the legacy tests whose fixtures assumed the old absolute/one-level forms: test_hook_dispatch.py and test_hook_executability.py (shlex parsing / token-expansion harness for subprocess execution), test_plugin_cache.py and test_doctor.py (two-level cache layout). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…g (Phase B) Hardens the self-update transaction against the incident's exact failure mode (issue #4469): a post-pivot in-process metadata read raised PackageNotFoundError, and the except handler's own logging call crashed a SECOND time trying to lazily import rich._emoji_codes from the just-deleted site-packages tree — losing the failure findings and the exit code, and crucially preventing the republication child from ever running. B-I1: core/logging.py's module-import-time default processor chain and configure_logging()'s console branch both now render exceptions via structlog.dev.plain_traceback (stdlib traceback, imported eagerly) instead of structlog's default RichTracebackFormatter. Rich's exception formatter lazily imports submodules on first use; autoskillit's own update can delete the tree backing that import mid-process. Exception rendering must depend only on modules fully imported at configure time — a crash class, not a cosmetic choice. B-I2: a last-resort _report_post_pivot_failure() helper wraps every post-pivot except handler (FRESH_VERSION_METADATA_GATE, INSTALL_CHILD_INVOCATION, POST_UPDATE_ARTIFACT_VERIFICATION) — tries the structured logger, falls back to sys.stderr.write() on ANY exception from logging itself, never print() (ARCH-001's _PRINT_EXEMPT boundary), never raises past the call. B-I3: run_update_transaction gains an injectable fresh_version_prober parameter. The post-pivot version read no longer trusts in-process metadata (the parent's own import machinery is invalid past the pivot by construction) — the production default runs the new `autoskillit` entrypoint as a fresh subprocess under the maintenance environment and parses its --version stdout. InstallInfo gains an `entrypoint` field, resolved pre-pivot via shutil.which() against the ambient (richest-PATH) environment, so the probe still works when the sealed maintenance environment's own PATH lacks uv's tool-shim directory. version_reader remains the pre-pivot-read seam; when explicitly injected without fresh_version_prober it also covers the post-pivot read, for backward compatibility with existing test doubles — every real production caller (no explicit override) gets the safe out-of-process default. B-I4: hoists the three function-local third-party imports found in cli/update/ (packaging.version x2 in _update_checks.py, regex in _update_checks_fetch.py — the plan named only the first two) to module top, and adds an AST-walk architectural guard (tests/arch/test_update_import_discipline.py) enforcing it going forward. B-I5: upgrade_command() pins --python <major>.<minor> of the running interpreter for both git-vcs upgrade shapes, captured pre-pivot. A routine update now rebuilds the venv on the same interpreter line instead of silently flipping it (the incident's proximate trigger). Tests: T-B1/T-B2/T-B5 reproduce the incident's double-crash via a real subprocess with rich purged from sys.modules and blocked from re-import (a meta_path blocker alone is insufficient — structlog eagerly imports rich as a side effect of `import structlog`, verified against this repo's pinned version) and assert the process survives. T-B3 proves the post-pivot gate no longer consults version_reader when a prober is supplied, plus a companion test of the production default's subprocess shape. T-B4 updates test_install_info.py's upgrade-command shape assertions and adds entrypoint-resolution coverage. T-B6 is the import guard above. All existing tests in test_update_transaction.py and test_install_info.py were directly re-executed (not just statically reviewed) against the new code and pass unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ke (Phase C) Closes the detection→repair loop the incident exposed: staleness detection existed in three places (server startup, doctor, the update-failure handler) but repair was reachable from none of them. A crash between the transaction's irreversible pivot and a completed republication child left no on-disk breadcrumb distinguishing "never updated" from "republication still owed" — this phase makes that a persisted fact instead of an inference, and wires real repair into every place that can observe it. C-I1: workspace/_update_obligation.py — a durable publication-obligation journal (~/.autoskillit/update_obligation.json), modeled on the RETIRED_INSTALL_ARTIFACT_SHAPES registry-plus-repair idiom. Lives in workspace/ (IL-1), not cli/update/, because it must be readable from server/_lifespan.py without a server→cli import edge (REQ-ARCH-003b). Written at UPGRADE_SUBPROCESS_GATE entry (only when a registered plugin makes publication owed; a write failure aborts before the irreversible uv subprocess launches), backfilled with expected_version once the post-pivot probe succeeds, cleared only at the transaction's own RESULT_FINALIZATION success or by a verified CLI-triggered repair — the two named clear-authorities. A corrupt/unreadable journal reads as "pending, version unknown", never as "no obligation" (fail toward repair, which is idempotent). C-I2: workspace/_projected_artifact/_hook_repair.py — regenerates broken hooks.json for any plugin-cache incarnation from HOOK_REGISTRY (relocatable form) and refreshes its manifest so the tamper detector stays consistent with the rewritten file. Placement is constrained by the same architectural guard: it's an INDEPENDENT implementation from cli/_plugin_artifact.py's publish path (an upward workspace→cli import is illegal), built from the identical core-layer digest/manifest primitives so neither implementation invalidates the other's tamper detection. Lease contention or any per-incarnation error is a skip-with-diagnostic — never raises out, so one broken incarnation can't block repair of the others. C-I3: cli/update/_obligation_repair.py's attempt_obligation_repair() is the single owner of attempt/defer/clear policy for the two CLI trigger sites (cli/update/_update.py's failure handler, cli/app.py's main()). Defers under CLAUDECODE; otherwise spawns one `autoskillit install --maintenance-update` child, verifies health, and only then clears the obligation. Verification branches on expected_version: known → token-aware hook validation plus verify_installed_plugin_artifact; None (probe never succeeded) → token-aware hook validation plus a version subprocess succeeding, since InstallStateSpec.expected_version is a required field that raises on empty. server/_lifespan.py's startup hook-health check uses the lower-level repair_broken_plugin_cache_hooks primitive directly and in-process instead (the server must not shell out) and never clears the obligation itself — an in-process hook repair can't perform the full publication the obligation may demand. Doctor gains a diagnostic-only publication_obligation check (no auto-fix; repair lives at the triggers). A serious reentrancy bug was found and fixed while validating this phase via real subprocess execution (not just mocks): main()'s obligation-repair observer, called unconditionally, would recurse when invoked as the maintenance-update child itself — that child's own main() would observe the SAME still-pending obligation (written by the parent moments before launching it) and spawn another repair attempt, chaining indefinitely or deadlocking on the exclusive publication lease the outer install() already holds. main() now skips the observer specifically for `install` argv; every `install` invocation is already itself an attempt to satisfy the obligation. Regression-guarded in tests/cli/test_app_main.py. C-I4: a new cross-interpreter upgrade smoke callable (smoke_utils/_cross_interpreter_upgrade.py) wired into .autoskillit/recipes/smoke-test.yaml — the only test in the system that exercises real `uv` venv replacement under a real interpreter flip (structurally impossible to inflict from in-process pytest, since the running test interpreter's own import roots can't be destroyed by the code under test). This step requires live uv/network/two provisioned Python minors and could not be executed or verified in this environment; the YAML was validated for correct parsing and graph-edge integrity only. The in-suite approximation the plan calls for is the same fixture (fake-venv tree renamed after publication) already delivered with Phase A as test_token_aware_validation_survives_interpreter_pivot — not duplicated here. Every other piece in this phase (T-C1 obligation lifecycle across 9 fault-injection scenarios, T-C2 failure-path postcondition contract, T-C3 startup repair including lease-contention skip, T-C4 both CLI verification branches) was directly executed against the real implementation, not just statically reviewed, including the full existing test_update_transaction.py and test_install_info.py suites re-run after every change to confirm zero regressions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…gression (Phase C remediation) Remediates two confirmed audit gaps from remediation_rectify_hook_path_immunity_2026-08-05_160327.md: - C-I2: cli/_plugin_artifact.py::_publish_installed_plugin_artifact_locked and workspace/_projected_artifact/_hook_repair.py::_refresh_manifest_locked independently reimplemented the same identity-construction + write_versioned_json + log_plugin_artifact_lifecycle sequence. Extracted the shared core into a new workspace/_projected_artifact/_manifest_publication.py:: write_installed_plugin_artifact_manifest_locked(managed_path, semantic_key=, action=), and made both callers delegate to it (one implementation, two callers, per the plan's explicit C-I2 directive). cli/_plugin_artifact.py keeps its own _canonical_installed_root() pre-validation call (preserves the existing test_publication_does_not_wrap_control_flow_exceptions monkeypatch contract) before delegating. - REQ-ARCH-001: server/_lifespan.py imported autoskillit.workspace._projected_artifact._hook_repair directly (a cross-package submodule import), which test_no_cross_package_submodule_imports forbids. Re-exported repair_broken_plugin_cache_hooks and RepairOutcome through workspace/_projected_artifact/__init__.py and workspace/__init__.py (matching the existing verify_install_state precedent) and switched _lifespan.py to import from the public autoskillit.workspace surface, promoted to a top-of-file import alongside the sibling read_obligation/verify_install_state imports already there. Also: - Added a fault-injection test (test_startup_hook_health_check_survives_repair_primitive_raising) proving run_startup_hook_health_check() survives repair_broken_plugin_cache_hooks itself raising, via the pre-existing outer except Exception -> return [] contract. - Updated tests/infra/test_plugin_source_ratchets.py's STRICT_PLUGIN_WRITE_ALLOWLIST: the write_versioned_json:strict=True call site moved from cli/_plugin_artifact.py::_publish_installed_plugin_artifact_locked to the new shared _manifest_publication.py function. Verification: pre-commit run --files <touched files> passes (ruff format, ruff, mypy, secrets). task test-check scoped to every test file referencing publish_installed_plugin_artifact / _hook_repair / _plugin_artifact / test_layer_enforcement.py: 1368 passed, 1 skipped (pre-existing skip, unrelated). tests/cli/test_install_transaction.py and tests/cli/test_session_launch.py have pre-existing, environment-specific failures (real claude.exe resolution) confirmed present identically on the pre-remediation commit via git stash; untouched by this commit and outside its scope. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… (Phase B remediation)
Removes the version_reader post-pivot fallback from _resolve_fresh_version
per the plan's literal requirement ('the prober parameter is the only
sanctioned source of post-pivot version truth'). fresh_version_prober is
now the sole source of post-pivot version truth (injected or the
production out-of-process default) -- version_reader is pre-pivot-only.
Updates the 13 pre-existing test call sites in test_update_transaction.py
that relied on the removed fallback with explicit fresh_version_prober
injections, tracing each site's phase-reachability individually rather
than pattern-matching (5 sites that fail/defer strictly before
FRESH_VERSION_METADATA_GATE never needed a post-pivot read and are left
unchanged).
Also completes T-C1(b)'s fault-injection matrix: adds uv_oserror and
child_oserror parametrize cases to
test_t_c1_obligation_survives_failures_at_or_after_upgrade_subprocess
(now 6 cases, was 4).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tion, doctor obligation coverage (Phase C remediation) - test_t_c2_hooks_valid_or_repair_owed_after_any_failure: add uv_oserror, child_oserror, and verifier_raises_after_probe parametrize cases (now 6 cases, was 3). - Add test_publication_obligation_loop_survives_interpreter_pivot: C-I4's in-suite rename-approximation test, physically separate from Phase A's test_token_aware_validation_survives_interpreter_pivot per the plan's literal 'add ... beside T-C2's contract test' directive. - Add test_check_publication_obligation_ok_when_no_obligation_pending and test_check_publication_obligation_warning_when_obligation_pending for the previously-uncovered _check_publication_obligation doctor check (Check 17b). - Add test_compute_registry_hash_is_identical_for_absolute_and_relocatable_renderings pinning A-I1's stated (but previously untested) invariant that compute_registry_hash never consults the rendered command string. - tests/arch/_rules.py: exempt test_publication_obligation_loop.py from ARCH-010's StrEnum-string-compare scan -- ObligationRepairResult.outcome is a plain str field (documented values), not a StrEnum; pre-existing false positive surfaced by task test-check, fixed in the file this change already touches. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…venance bisection Confirmed via a scratch git-worktree bisection against develop (f1c9627) and the original pre-remediation Phase C tip (bdb7f92): these were baked into the original Phase A/B/C implementation itself, not introduced by remediation. - ARCH-003 (broad except without logger call), 5 sites in cli/update/_transaction.py: added a real logger.warning(exc_info=True) call to the pre-pivot obligation-write except (safe -- nothing mutated yet, parent import machinery intact). The remaining 4 sites all trace to _report_post_pivot_failure(), Phase B's B-I2-mandated single crash-proof reporting helper for every post-pivot except handler -- the AST scanner only recognizes a literal .warning()/.error() call written directly in the except body, not a call to a named function that itself logs. Duplicating the helper's try/except at each site to satisfy the scanner literally would violate the plan's explicit 'one helper, every post-pivot except handler' design and risk reintroducing the double-crash class Phase B exists to prevent; two of the four are the helper's own deliberately-terminal last-resort fallbacks (logging just failed -- logging again would risk re-triggering the same failure). Added a narrowly-scoped, clearly-commented exemption for _transaction.py to tests/arch/_rules.py's _BROAD_EXCEPT_EXEMPT. - test_doctor_check_count_is_47 -> test_doctor_check_count_is_48: Phase C added Check 17b (_check_publication_obligation) without updating the count; also corrected the stale '(47 checks)' count in cli/doctor/AGENTS.md. - test_path_like_args_registry_complete: registered _cross_interpreter_upgrade.py's incarnation_dir param in _EXPLICITLY_EXCLUDED -- it's a private-helper-internal arg, always constructed as cache_root / name (absolute), never resolved through run_python's relative-path machinery, matching the existing worktree_path/project_dir/log_dir justification pattern. - test_smoke_utils_all_exports_complete: added the already-correctly- exported run_cross_interpreter_upgrade_smoke to the test's stale expected-exports set. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ns (Phase A/B/C remediation) Confirmed via develop-provenance bisection: these 6 task-check failures pass on develop and only fail starting at bdb7f92 (Phase C's original tip) — never caught because task test-check was never run on the original 3-phase implementation (the audit's own PROCESS GAP finding). Cascade-map-guard registry gaps (tests/_test_filter.py): - MODULE_CASCADE_CORE["paths"]: add smoke_utils — _cross_interpreter_upgrade.py's _find_source_root() calls pkg_root() (core/paths.py) to locate the repo root for the live uv upgrade smoke step (C-I4). - LAYER_CASCADE_CONSERVATIVE["hook_registry"]: add workspace — _hook_repair.py calls find_broken_hook_scripts/generate_hooks_json directly. Subpackage/line-limit regressions (tests/arch/test_subpackage_isolation.py): - workspace/ EXEMPTIONS bumped 15->16: _update_obligation.py (Phase B/C's publication-obligation journal) is one cohesive 176-line read/write/clear API with no internal seam to extract. - smoke_utils/ new EXEMPTIONS entry at 11: _cross_interpreter_upgrade.py (C-I4) is one cohesive 170-line callable, same rationale. - hook_registry.py new _LINE_LIMIT_EXEMPTIONS entry (REQ-CNST-010-E21) at 1100: a stdlib-only, package-root module imported directly by hook subprocess scripts on the low-latency startup path, so it deliberately stays flat rather than splitting into a sub-package. Phase A's relocatable hook commands (${CLAUDE_PLUGIN_ROOT} token generation, resolve_codex_hooks_dir, token-aware find_broken_hook_scripts/validate_plugin_cache_hooks) add 114 net lines. Schema-convention gaps (tests/infra/test_schema_{read,version}_convention.py) — investigated each site individually per instructions, not blanket-allowlisted: - _manifest_publication.py (this remediation's own C-I2 extraction) added to _READ_SIDE_EXCEPTIONS: its write is validated by the already-registered shared read-side validator read_installed_plugin_artifact_identity, called from cli/_plugin_artifact.py and workspace/_installed_artifact.py at every launch-time binding acquisition — confirmed by grep, not assumed. - _hook_repair.py:90 added to _LEGACY_JSON_WRITES: same hooks.json format as the pre-existing _lifespan.py startup-drift-heal site (co-owned with the Claude plugin system, not autoskillit's own schema). - _lifespan.py:96->101 and cli/update/_update_checks{,_fetch}.py line renumbering: pre-existing allowlisted sites shifted by unrelated edits earlier in file, not new gaps. - workspace/_update_obligation.py: investigated as a possible REAL durability gap per instructions, and it was one. write_obligation()'s own docstring claims "Raises on write failure... upholds the invariant that the irreversible region is entered only with the breadcrumb already on disk" — but the plain atomic_write(json.dumps(...)) call it used had no strict_durability=True, so a failed parent-directory fsync was silently swallowed instead of raising, contradicting that documented contract. Migrated _write()/read_obligation() to write_versioned_json(..., strict_durability=True)/read_versioned_json() with a new _OBLIGATION_SCHEMA_VERSION=1: the write side now actually raises on a failed durability fsync (write_obligation has no try/except, so this still propagates to abort the transaction before the upgrade subprocess launches, exactly as documented; update_obligation_expected_version's existing except Exception still swallows it for the backfill's "never raises" contract). read_obligation() keeps its own path.exists() check ahead of read_versioned_json() so a missing file still returns None while every other failure mode (corrupt JSON, non-dict, schema-version mismatch) collapses into the existing degraded-but-pending record — read_versioned_json cannot express that missing/corrupt distinction on its own, so a thin wrapper was necessary. No externally-observable behavior change: verified against every test exercising this module (see below). Verification: pre-commit run --files <touched files> passes (ruff format, ruff, mypy, secrets). task test-check scoped to tests/arch/, tests/infra/, tests/workspace/, tests/contracts/, tests/cli/test_doctor.py, tests/cli/test_update_transaction.py, tests/server/: 12991 passed, 229 skipped, 13 xfailed, 0 failed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Trecek
force-pushed
the
impl-rectify_hook_path_immunity-20260805-142643
branch
from
August 6, 2026 16:48
92c0a88 to
717b195
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the rectify plan for GitHub issue #4469 ("Self-Update Crash + Total Session Lockout") in three sequential, independently-green phases, per
.autoskillit/temp/rectify/rectify_hook_path_immunity_2026-08-05_124828.md.Phase A — Relocatable Hook Commands (
f228652b6)Kills the absolute-hook-path ENOENT class: every published
hooks.jsonnow emitspython3 "${CLAUDE_PLUGIN_ROOT}/hooks/_dispatch.py" <name>instead of apkg_root()-derived absolute path, so hook validity is a property of the plugin artifact, not the venv interpreter/install path.settings.json(dev-mode, never redistributed) keeps absolute paths. Also fixes a real production bug found during implementation:validate_plugin_cache_hooks's glob was one level too shallow and matched nothing against any real installed cache (its own startup/doctor detection was a silent no-op).Phase B — Pivot-Safe Update Verification (
97bbdfc2d)Crash-proof exception rendering everywhere (stdlib-only
plain_traceback, never rich — rich's lazy imports are exactly what caused the incident's double-crash). The post-pivot version read is now an out-of-process subprocess probe by default; in-process metadata is a pre-pivot-only API.--pythoninterpreter pin on upgrade commands. Hoists 3 function-local third-party imports plus an AST guard preventing regression. Tests reproduce the incident's actual double-crash via a real subprocess withrichpurged and blocked fromsys.modules.Phase C — Detection→Repair Loop Closure (
bdb7f92bb)A durable publication-obligation journal, an in-process repair primitive, and wiring into all three lifecycle triggers (MCP server startup, update-failure handler, CLI startup) plus a diagnostic-only doctor check. A serious reentrancy bug was found and fixed while validating this phase against real subprocess execution:
main()'s obligation-repair observer would recurse when invoked as its owninstall --maintenance-updatechild.Known limitation
T-C5's real cross-interpreter upgrade smoke step (
smoke_utils/_cross_interpreter_upgrade.py, wired into.autoskillit/recipes/smoke-test.yaml) requires liveuv+ network + two provisioned Python minors unavailable in the implementation/CI environment — implemented per spec but never executed; only YAML parse/graph-edge integrity was verified.Post-implementation audit + remediation
An independent
/audit-implpass (3 parallel auditors, one per phase, run against the full plan) found Phase A and B fully covered (minor non-blocking notes only) and returned NO GO on Phase C, plus one deviation upgrade on further review. All findings were remediated in 5 follow-up commits:8c1690ad5— extracted the shared manifest-refresh core (workspace/_projected_artifact/_manifest_publication.py) socli/_plugin_artifact.pyand the new repair primitive delegate to one implementation instead of two independently-maintained copies (closes C-I2); fixed a realREQ-ARCH-001cross-package-submodule-import regression inserver/_lifespan.py; added a fault-injection test for startup-repair resilience.82bf1aa08— removed_resolve_fresh_version'sversion_reader-only post-pivot fallback (a deliberate, disclosed implementation-time deviation from the plan's "prober is the only sanctioned post-pivot source" requirement), updating the 13 pre-existing test call sites that actually needed it.a10c82715— completed the T-C2 fault-injection matrix, added the missing in-suite rename-approximation test (C-I4), added_check_publication_obligationdoctor-check coverage, plus one collateral architecture-guard exemption.621e07a23,8bcb5482a— runningtask test-checkfor real (previously only hand-driven direct Python execution was used, per implementation-phase constraints) surfaced 11 genuine regressions baked into the original Phase A/B/C implementation, confirmed via bisection against pristinedevelop(they pass there, fail starting at the Phase C tip). All 11 fixed: 5 architecture-guard registry gaps (cascade-map, path-resolution, subpackage size/line limits — resolved via justified limit bumps, no clean split seam existed), a stale doctor-check-count assertion, anARCH-003broad-except gap (real fix on the one safely-fixable site; a scoped exemption for the other 4, which route through a single shared crash-proof reporting helper the AST scanner can't see through, two of which are themselves deliberately-terminal last-resort fallbacks). One of these was a real bug, not bookkeeping:workspace/_update_obligation.py::write_obligation()'s docstring promised "raises on write failure" but the underlying write had nostrict_durability=True, so a failed fsync was silently swallowed instead of aborting the transaction — migrated towrite_versioned_json/read_versioned_json.Verification
Final full unfiltered
task test-check(AUTOSKILLIT_TEST_FILTER=conservative AUTOSKILLIT_TEST_BASE_REF=develop): 33896 passed, 4 failed, 582 skipped, 27 xfailed (364.89s). The 4 remaining failures (test_install_transaction.py::test_direct_mode_snapshots_caller_env_and_cwd, 3xtest_session_launch.py) are confirmed environment artifacts of this sandbox — a real npm-globalclaudebinary shadows the tests'shutil.which/PATH mocking — and reproduce identically on pristinedevelop, verified twice independently via scratch-worktree bisection. Zero failures are caused by this PR's diff.pre-commit run --all-files(ruff format, ruff, mypy, and all repo-specific hooks) passes clean on the final state.🤖 Generated with Claude Code