[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
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 4.8k
- Forks
- 342
- Avg merge
- 13h 48m
- Merged PRs (30d)
- 211
Description
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
- In the team repo, place a skill at
skills/front/my-skill/containingSKILL.mdandreferences/old.md. - Run
teamai pullso the local copy carries both files. - Delete
references/old.mdfrom the local copy of the skill. - Run
teamai push. The skill is listed as(modified)and the command reports success. - Open the resulting PR. It carries no deletion of
references/old.md, and the file remains in the team repo. - Run
teamai pushagain. 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
isIgnoreddrops 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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at SkillsHandler.pushItem in src/resources/skills.ts and copyDir in src/utils/fs.ts, then trace collectFiles, dirTeamSubsetEqual, and pushGroup in the referenced locations. Reproduce the deletion flow from the issue and verify that push removes locally absent files while preserving CONTRIBUTORS and isIgnored entries, carries new files, and no longer reports the skill as modified on later scans.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- cli, tooling
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100