Skip to content

fix(source): distinguish repositories in source and watch identities #14

Closed

Description

@showxu

Summary

Different source locations can share one identity. Hosted URLs lose their host, and local paths use only the directory basename. Two local directories named skills merge into one watch record; hosted repositories with the same owner/path also produce the same cache key.

Code evidence at 5d4987b:

  • Sources/Core/SourceParser.swift:112 derives owner/repository from the URL path; :130 uses that host-free value, and :135 uses the local basename.
  • Sources/Core/ProcessRunner.swift:78 derives the checkout directory solely from sanitized identity; :79 reuses an existing checkout without validating its origin against the requested URL.
  • Sources/Core/WatchLedger.swift:305 derives a watch ID from identity; :308 matches identity/ID and :331 replaces the source and checkout while retaining the existing record.
  • Sources/Core/InstallLock.swift:173 additionally compares location when merging pins; the lock file itself does not universally merge same-identity locations.

Reproduction

Executed at 5d4987b. Local watch behavior and host identity parsing were both executed. The parser harness compiles the repository's source files directly and needs no hosted fixtures.

Run in Bash after cd "<checkout>" && swift build, replacing <checkout> with the absolute path of the checkout at this revision. Use a fresh reproduction directory. The commands isolate HOME, agent configuration, and XDG_*, and remove their fixture root on exit.

set -eu
checkout="<checkout>"
skill_bin="$checkout/.build/debug/skill"
repro_root="$checkout/.build/issue-repro-local-identity"
mkdir -p "$repro_root/home" "$repro_root/tmp" "$repro_root/project"
trap 'rm -rf "$repro_root"' EXIT
isolated() {
  env -i PATH="$PATH" HOME="$repro_root/home" \
    CODEX_HOME="$repro_root/home/.codex" \
    CLAUDE_CONFIG_DIR="$repro_root/home/.claude" \
    XDG_CONFIG_HOME="$repro_root/home/.config" \
    XDG_STATE_HOME="$repro_root/home/.local/state" \
    XDG_CACHE_HOME="$repro_root/home/.cache" TMPDIR="$repro_root/tmp" \
    GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null \
    GIT_AUTHOR_NAME=Fixture [email protected] \
    GIT_COMMITTER_NAME=Fixture [email protected] \
    "$@"
}
skill() { isolated "$skill_bin" "$@"; }
write_skill() {
  mkdir -p "$1"
  printf -- '---\nname: %s\ndescription: Reproduction fixture\n---\n%s\n' "$2" "$3" > "$1/SKILL.md"
}
cd "$repro_root/project"

for owner in a b; do
  write_skill "$repro_root/$owner/skills/demo" demo "$owner"
  skill add "$repro_root/$owner/skills" --watch-only --skill demo --agent codex
 done
isolated python3 - <<'CODE'
import json
w=json.load(open('.agent/skills-state.json'))['watches']
print('watch count:', len(w))
for r in w:
    print('watch ID:', r['watch_id'], 'location:', r['source']['location'])
CODE

cat > "$repro_root/main.swift" <<'SWIFT'
import Foundation
for input in ["https://github.com/a/b", "https://gitlab.com/a/b"] {
  let parsed = try SourceParser.parse(input)
  print(input, "=>", SourceParser.identity(for: parsed))
}
SWIFT
isolated swiftc -module-cache-path "$repro_root/module-cache" \
  "$checkout/Sources/Core/CoreError.swift" "$checkout/Sources/Core/PathSafety.swift" \
  "$checkout/Sources/Core/SourceRequirement.swift" "$checkout/Sources/Core/SourceParser.swift" \
  "$repro_root/main.swift" -o "$repro_root/identity-test"
isolated "$repro_root/identity-test"

Observed output (relevant lines; temporary paths shortened to <temp>):

watched skills path demo
watched skills path demo
watch count: 1
watch ID: skills location: <temp>/b/skills
https://github.com/a/b => a/b
https://gitlab.com/a/b => a/b

Expected Behavior

Distinct repositories must retain separate source identities, watch state, and cache entries. Repeated references to the same repository should remain stable.

Maintainer decision: choose canonical location normalization and a migration/ambiguity policy for existing watch IDs, pins, and caches. A cache hit must not silently bind a new source location to an unrelated origin.

Completion checks:

  • Different hosts with the same owner/path remain distinct.
  • Two local paths ending in skills create independent watches and pinned-source caches.
  • Updating either watch preserves the other's location, baseline, and review cursors.
  • Re-adding the same source remains idempotent; ambiguous existing state is handled explicitly; run swift test.

Actual Behavior

Both watch adds succeed, but only the second location remains in the single skills record. The parser harness returns a/b for both hosted URLs. Cache-directory collision follows from the same identity-only cache construction; hosted repositories were not cloned in this reproduction.

Related: #10 tracks watch scope; these collisions occur within one project and remain even with a scope-correct ledger. No duplicate found.

Environment

  • Source revision: 5d4987b (5d4987b9e1f94e8b9f85124299f05d5aa4f45f07).
  • CLI version: 0.2.1; built successfully with swift build.
  • Swift: Apple Swift 6.4 (swiftlang-6.4.0.34.1, clang-2100.3.34.1).
  • OS: macOS 27.0.1 (26A434).
  • Architecture: arm64.
  • Git: 2.42.0. Validation date: 2026-10-17.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions