Skip to content

[bug] A file deleted from a skill is never removed from the team repo: push reports success and the skill re-flags as (modified) on every scan #650

Description

@SaulMoro

Description

teamai push reports success for a skill whose local copy has lost a file, while the team repo keeps that file. The skill is then reported (modified) on every subsequent scan, with no way to clear it.

Detection works. dirTeamSubsetEqual collects the team copy's file set recursively (src/utils/fs.ts:453-472) and compares each path against the local copy. A file present in the team copy and missing locally produces a null hash and a false result (src/utils/fs.ts:347-354, :377), so scanLocalForPush lists the skill as (modified).

The transfer does not match the comparison. SkillsHandler.pushItem calls await copyDir(item.sourcePath, dest) (src/resources/skills.ts:534), and copyDir is fse.copy(src, dest, { overwrite: true, filter: (p) => !isIgnored(path.basename(p)) }) (src/utils/fs.ts:165-168). That merges the local tree over the team tree and removes nothing. pushGroup then stages the directory path (src/push.ts:168-170, :192-198), so the commit carries the files that were written and no deletion.

compare:   team file set -> local tree    missing file seen      -> (modified)
transfer:  local tree    -> team file set merge, nothing pruned  -> file stays

The comparison is subset-directional. The transfer is additive. A deletion falls into the gap between them.

Reproduction

  1. In the team repo, place a skill at skills/front/my-skill/ containing SKILL.md and references/old.md.
  2. Run teamai pull so the local copy carries both files.
  3. Delete references/old.md from the local copy of the skill.
  4. Run teamai push. The skill is listed as (modified) and the command reports success.
  5. Open the resulting PR. It carries no deletion of references/old.md, and the file remains in the team repo.
  6. Run teamai push again. The skill is listed as (modified) again.

Step 6 repeats for every subsequent scan, because the condition that produced the flag is never resolved.

Expected behavior

teamai push removes from the team copy the files that the local copy no longer has, so the PR carries the deletion and the skill stops reporting (modified).

Where removing files is not wanted, teamai push states that the deletion was not carried, rather than reporting success.

Suggested fix

Make the skill transfer mirror the local tree rather than merge over it. collectFiles already produces both file sets, so the delete set is the difference:

 pushItem(item, dest)
   await copyDir(item.sourcePath, dest)
+  remove from dest every file in the team set that the local set does not have
   ensureSkillFrontmatter(dest)

Two exclusions the pruning must respect, or push will delete files it should keep:

  • CONTRIBUTORS, which lives only in the team copy and is passed as the ignore argument to the comparison (src/resources/skills.ts:444, appended at :546-557).
  • The entries isIgnored drops at every level: .DS_Store, node_modules, .git, *.pyc (src/utils/fs.ts:5-17).

Environment

Read from source at main (97a0277, package version 0.22.0). The installed CLI is 0.24.0, the current npm latest.

  • OS: macOS 26.5.2
  • Node.js: v22.22.2
  • teamai: 0.24.0
  • Provider: GitHub
  • AI tool(s): Claude Code

Logs

No --verbose output applies. The finding comes from reading source at the commit above.

The copy behaviour it rests on is confirmed against the fs-extra build that teamai 0.24.0 installs. The script below reproduces copyDir exactly as src/utils/fs.ts:165-168 calls it, over a team copy holding SKILL.md, references/old.md and CONTRIBUTORS, and a local copy with SKILL.md edited, references/old.md deleted and references/new.md added.

const isIgnored = (n) => ['.DS_Store','node_modules','.git'].includes(n) || n.endsWith('.pyc');
await fse.copy(local, team, { overwrite: true, filter: (p) => !isIgnored(path.basename(p)) });

Result:

team after copy: CONTRIBUTORS, SKILL.md, references/new.md, references/old.md
references/old.md still present: true
references/new.md carried: true
CONTRIBUTORS survived: true

references/old.md survives the copy, which is the defect. CONTRIBUTORS survives for the same reason, which is why pruning has to exclude it.

Related

A file added locally inside an existing skill is not detected on its own, because the comparison walks the team copy's file set. That case is largely covered in practice: a skill routes to its references, so an added file arrives with an edit to SKILL.md or another tracked file, and copyDir then carries the whole directory including the new file. The rationale for the subset direction is stated at src/utils/fs.ts:418-421. Any pruning added for the deletion case should leave that behaviour intact.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions