Skip to content

fix(init): repair '(text)' config corruption and adjacent multi-agent push races (#739)#599

Merged
dollspace-gay merged 1 commit into
developfrom
fix/739-tui-string-placeholder
May 14, 2026
Merged

fix(init): repair '(text)' config corruption and adjacent multi-agent push races (#739)#599
dollspace-gay merged 1 commit into
developfrom
fix/739-tui-string-placeholder

Conversation

@dollspace-gay
Copy link
Copy Markdown

Summary

Five related fixes that together make a freshly-crosslink init'd repo actually round-trip between agents.

  • Stops the (text) placeholder corruptionWalkthroughCore::build_config no longer writes the UI placeholder "(text)" into hook-config.json for every ConfigType::String key. Pre-fix, every new repo got tracker_remote = "(text)" (and matching corruption for both sentinel.*.model keys), causing every push to fail as RemoteMisconfigured("(text)"). populate_tracker_remote and read_tracker_remote now auto-repair / gracefully degrade on the corrupt value.
  • Closes the multi-agent bootstrap racesync_cmd now calls a new SyncManager::push_hub_if_ahead() at the end. Previously, pushes happened lazily only via SharedWriter::commit_*, so agent B's init_cache could run between A's bootstrap sync and A's first write, see an empty remote, create its own orphan branch, and then never rebase onto A's eventual push.
  • Hardens the PushFailure::NonFastForward classifier — added "! [remote rejected]", "cannot lock ref", "incorrect old value provided", and "failed to push some refs" to the substring set. Pre-fix, concurrent lock claims hit a path where both agents got PushFailure::Other, both call sites returned LocalOnly, and both printed "Claimed lock on issue Support hook-config.local.json for machine-local configuration #1".
  • Tolerates cpitd 0.3.0's schema renametotal_pairstotal_groups via #[serde(default, alias = "...")]. Empty-output cpitd scan no longer fails to parse. Non-empty schema migration is a separate concern.

Test plan

  • 8 new unit tests added (1 in config_registry, 3 in init, 1 in sync::tests for the read fallback, 2 in sync::tests for the classifier patterns, plus the test counts come from build_config + populate_tracker_remote + classifier expansions)
  • 4 previously-failing smoke tests now pass:
    • smoke::cli_tooling::test_cpitd_scan_dry_run
    • smoke::concurrency::test_parallel_lock_claim_one_winner
    • smoke::coordination::test_two_agents_create_issues
    • smoke::coordination::test_adversarial_same_issue_write_conflict_convergence
  • Full suite green: 1,883 lib + 2,859 bin + 194 integ + 159 smoke pass, 0 failures
  • cargo fmt --check clean
  • cargo clippy --all-targets -- -D warnings introduces zero new errors (21 pre-existing errors on develop, 21 on this branch)

Recovery for existing affected repos

Repos initialized before this fix carry the corrupt config sticky — init --force from this build will auto-repair them. For pre-build repos, run:

crosslink config set tracker_remote origin
crosslink config set sentinel.default_agent.model <model>
crosslink config set sentinel.escalation.model <model>

Out of scope (follow-up)

  • Full cpitd 0.3.0 schema migration to actually parse non-empty clone groups (the alias only unblocks the empty path).
  • Backfilling unit tests for the new push_hub_if_ahead happy path / offline path / divergence path. The smoke tests exercise the behavior end-to-end.

🤖 Generated with Claude Code

…i-agent push fixes (#739)

The init walkthrough was writing the UI placeholder "(text)" into
hook-config.json for every ConfigType::String key, corrupting
tracker_remote (push fails with RemoteMisconfigured('(text)')) and
both sentinel.*.model keys on every new repo. populate_tracker_remote
treated the corrupt value as a manual edit and refused to repair it,
so even `crosslink init --force` left the config broken.

While tracing the resulting "new repo never works" symptom, three
adjacent multi-agent bugs surfaced that share the same blast radius
(fresh-remote bootstrap not propagating). All four are fixed together
so a freshly-inited repo actually round-trips between agents.

* config_registry.rs (build_config): add `ConfigType::String => continue`
  so the UI placeholder is never persisted; apply_tui_choices now
  preserves the template defaults.

* init/mod.rs (populate_tracker_remote): treat "(text)" as a corrupt
  template default to be auto-repaired alongside the literal "origin",
  so existing affected repos self-heal on `crosslink init --force`.
  Collapsed the new arm with the absent-key case (clippy).

* sync/mod.rs (read_tracker_remote): defensive guard — when the
  on-disk value is "(text)", warn once with remediation instructions
  and fall back to "origin", so unpatched repos see actionable text
  instead of a generic push failure.

* sync/cache.rs (push_hub_if_ahead) + commands/locks_cmd.rs (sync_cmd):
  sync now pushes the hub branch when local is ahead of remote OR the
  remote branch doesn't exist. Previously, pushes only happened lazily
  via SharedWriter::commit_*, leaving a race window where agent B's
  init_cache saw an empty remote (because A's bootstrap had never
  been pushed), created its own orphan branch, and then couldn't
  rebase onto A's eventual push.

* sync/mod.rs (classify_push_failure): expanded NonFastForward
  substring set to cover "! [remote rejected]", "cannot lock ref",
  "incorrect old value provided", and "failed to push some refs" —
  the four shapes git emits for concurrent-ref-update races.
  Pre-fix, parallel lock claims both fell through to Other, both
  call sites returned LocalOnly, and both agents printed "Claimed
  lock on issue #1".

* commands/cpitd.rs: add `#[serde(default, alias = "total_groups")]`
  to CpitdOutput::total_pairs to tolerate the cpitd 0.3.0 schema
  rename. Full schema migration for non-empty clone_reports is
  separate.

Tests added (8):
- walkthrough_core_tests::build_config_never_emits_string_typed_keys_with_text_placeholder
- 3 × test_populate_tracker_remote_repairs_text_placeholder_{with_detected, without_remotes, to_non_origin}
- test_read_tracker_remote_falls_back_when_corrupt_placeholder
- classify_push_failure_concurrent_ref_lock_is_non_ff
- classify_push_failure_remote_rejected_variant
- 4 previously-failing smoke tests now pass: cpitd_scan_dry_run,
  parallel_lock_claim_one_winner, two_agents_create_issues,
  adversarial_same_issue_write_conflict_convergence

Full suite green: 1883 lib + 2859 bin + 194 integ + 159 smoke pass,
0 failures, fmt clean, no new clippy errors introduced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dollspace-gay dollspace-gay self-assigned this May 14, 2026
@dollspace-gay dollspace-gay added the bug Something isn't working label May 14, 2026
@dollspace-gay dollspace-gay merged commit 979d220 into develop May 14, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant