[Bug]: specify init writes speckit.manifest.json without the speckit-converge skill it just installed #4273
Description
Version
spec-kit v1.0.1, specify init --here --integration claude --force
Description
After an in-place upgrade, .specify/integrations/speckit.manifest.json lists 9 speckit-* SKILL.md entries under files — but init also installed .claude/skills/speckit-converge/SKILL.md, which is absent from the manifest.
Any tooling that uses the manifest to classify vendored files (pristine / modified / missing / untracked — e.g. deciding whether the next specify init will clobber local work) sees converge as untracked: it cannot be classified at all, which is the worst of the four states for a file the installer itself owns.
Observed identically on two independent upgrades (a template repo and a workflows repo, both 0.5.0 → v1.0.1 in-place).
Expected behavior
The generated manifest includes every skill file the init run installs, converge included.
Activity
Gambit-Checkmate commentedon Aug 22, 2026
Hi, I’d like to investigate the missing speckit-converge entry in the manifest generated by init. I can reproduce it with a clean installation, add a regression test for the generated manifest, and make the smallest fix that keeps existing manifests compatible. Is this issue open for an external PR?
mnriem commentedon Sep 1, 2026
@Gambit-Checkmate Absolutely, go for it. I have triggered our bug assess pipeline so you will get some additional background
github-actions commentedon Sep 1, 2026
Bug assessment — converge-missing-manifest: Valid · severity medium
Bug Assessment: converge-missing-manifest
converge-missing-manifestReport (summarized)
After running
specify init --here --integration claude --forcewith spec-kit v1.0.1 (upgrading from v0.5.0), the file.specify/integrations/claude.manifest.json(referred to asspeckit.manifest.jsonin the report — see note below) contains 9speckit-*SKILL.md entries but is missingspeckit-converge/SKILL.md, even though that file was successfully installed at.claude/skills/speckit-converge/SKILL.md. Observed on two independent in-place upgrades.Note on filename:
.specify/integrations/speckit.manifest.jsonis the shared infrastructure manifest (tracks scripts/templates). The per-integration manifest for claude lives at.specify/integrations/claude.manifest.json. Based on the description ("9 speckit-* SKILL.md entries"), the reporter is looking atclaude.manifest.jsonand using "speckit.manifest.json" generically.Symptom
specify init --here --integration claude --forceinstalls all 10speckit-*SKILL.md files to.claude/skills/(includingspeckit-converge/SKILL.md) but the resultingclaude.manifest.jsonrecords only 9 of them —speckit-converge/SKILL.mdis absent. Any tooling that consults the manifest to classify files (pristine / modified / missing / untracked) will treatspeckit-converge/SKILL.mdas untracked, preventing clean uninstall, upgrade diff-detection, and hash-based freshness checks for that skill.Reproduction
specify init myproject --integration claudemyproject/, runspecify init --here --integration claude --force..specify/integrations/claude.manifest.json— checkfileskeys forspeckit-*/SKILL.mdentries..claude/skills/speckit-converge/SKILL.mdexists on disk but has no corresponding entry in the manifest.[NEEDS CLARIFICATION: Whether the same omission occurs on a fresh
specify init(no prior install) with v1.0.1 or only on--forcere-inits of existing projects.]Suspected Code Paths
src/specify_cli/integrations/base.py:1725–1818(SkillsIntegration.setup()loop) — the main loop that callswrite_file_and_record()for each skill. Converge is index 4 of_CORE_COMMAND_TEMPLATE_ORDER(line 49); the loop should process all 10 templates without exclusion.src/specify_cli/integrations/base.py:558–576(write_file_and_record()) — writes the file, then callsmanifest.record_existing(rel). Ifrecord_existingraises (e.g. for a symlinked path component) and this exception is swallowed somewhere, the file is on disk but not in the manifest.src/specify_cli/integrations/manifest.py:168–233(record_existing()) — raisesValueErrorif any path component traversed fromself.project_rootis a symlink, or if the file is not a regular file. This is a plausible silent-failure site if an outer context swallows the exception for one file.src/specify_cli/commands/init.py:665–698— the init entry point for integration setup. Creates a freshIntegrationManifest, callssetup(), thenmanifest.save(). Aftermanifest.save()(line 698),_register_extensions_for_agentand_register_presets_for_agentare called when--forceis set (lines 700–722). These post-save calls do not affect the already-saved integration manifest but they do run after the file is saved, creating a potential window.src/specify_cli/integrations/base.py:466–479(list_command_templates()) — lists all.mdfiles fromshared_commands_dir(). Converge is present intemplates/commands/converge.mdand would be returned; the sort key is distinct (_CORE_COMMAND_TEMPLATE_RANK["converge"] == 4). No exclusion here.Root Cause Hypothesis
Converge was introduced in commit
0c29d890(feat: add /speckit.converge command, June 2026), making it the most recently added core command when v1.0.1 shipped. TheSkillsIntegration.setup()loop has no try/except around the per-skill write; any exception would abort setup entirely, not silently skip one file. This rules out most exception-swallowing theories.The most credible scenario is an environmental interaction specific to the upgrade path: on a v0.5.0 project, the
.claude/skills/directory existed with 9 subdirectories pre-installed. Whenspecify init --forcerunssetup()over an already-populated tree,dest.parent.mkdir(parents=True, exist_ok=True)silently succeeds for all 10 skill directories. For converge specifically, if the newly createdspeckit-converge/directory resolves differently (e.g., a stale inode, platform-specific casing, or a symlink left by a prior install) andmanifest.record_existing()subsequently raisesValueErrorfor that path after the file is written, then...However,
setup()does not have a per-iteration try/except — aValueErrorfromrecord_existingwould propagate toinit.py's outertry/except Exceptionhandler (line 936), which would print an error and exit. The user would not see a "success with 9 files" result. This means the exception-path theory requires a currently-invisible swallowing site.Most likely: The bug is a missing coverage gap —
write_file_and_recordcallsmanifest.record_existing(rel)whererelis computed asdest.resolve().relative_to(project_root.resolve())(aPathobject).record_existingre-walks frommanifest.project_rootto verify no symlinks. Ifproject_rootpassed towrite_file_and_recorddiffers frommanifest.project_root(even by trailing slash or symlink resolution), the reconstructed absolute path for converge'sSKILL.mdmay not match what was written, so_sha256(abs_path)operates on a slightly different path. Thesave()then writes only the keys that were successfully recorded.Confidence: medium. The code path is clear, but reproducing it requires the specific v0.5.0→v1.0.1 upgrade environment. A targeted regression test would either confirm or rule out this theory.
Proposed Remediation
Preferred: Add an explicit assertion / test that verifies the count of manifest entries equals the count of installed SKILL.md files after
setup()in both fresh-install and re-init (--force) scenarios. Then add the same check insideSkillsIntegration.setup()as a postcondition assertion (or a warning log) that countslen([f for f in created if f.name == "SKILL.md"])againstlen(manifest.files filtered to SKILL.md paths). If they diverge, raise so the bug surfaces loudly rather than producing a silently-partial manifest.Additionally, in
write_file_and_record, replace themanifest.record_existing(rel)call with the more directmanifest.record_file(rel, ...)pattern (which writes content and records in one step, computing the hash from the in-memory bytes rather than re-reading from disk). This eliminates the window between write and record and avoids any potential path-resolution discrepancy.Alternative: Add a post-setup reconciliation step in
SkillsIntegration.setup()after the loop: iterateskills_dir.rglob("SKILL.md"), and for any SKILL.md found under aspeckit-*directory that is NOT yet in the manifest, callmanifest.record_existing()on it. This is a belt-and-suspenders backstop that self-heals silently omitted entries.Files likely to change:
src/specify_cli/integrations/base.py(SkillsIntegration.setup loop — add postcondition or switch torecord_file)tests/integrations/test_integration_base_skills.py(add a test that verifies exactly N SKILL.md entries appear in manifest.files, exercising re-init over a pre-populated skills directory)tests/integrations/test_integration_claude.py(add the same assertion for the Claude-specific subclass)Tests to add or update:
tmp_pathwith 9 SKILL.md stubs under.claude/skills/speckit-*/(simulating an existing v0.5.0 install without converge) and then callssetup()with a fresh manifest — verifies all 10 skills including converge appear inmanifest.filesaftersetup().setup()twice with a fresh manifest each time (simulating--forcereinit) and asserts both manifest objects record the same 10 entries.test_all_files_tracked_in_manifestto also check the inverse: thatlen([k for k in m.files if k.endswith("SKILL.md")])equalslen(templates)(currently the test only checks files increatedare tracked, not that ALL templates produced entries).Risks & Considerations
specify integration uninstall claudewill not removespeckit-converge/SKILL.mdbecause it is not in the manifest. The directory.claude/skills/speckit-converge/is also not removed. This is already occurring for any project that hit this bug.integration upgradestale-file pass (Phase 2 in_migrate_commands.py:844–867) computesstale_keys = old_files - new_files. If the old manifest is missing converge, the stale set will never include converge — so a subsequent upgrade won't try to remove a stale converge skill. This is benign but means the broken state persists silently across upgrades.specify integration upgrade claudeorspecify init --here --forceonce the bug is fixed.specify init --forceover a pre-existing project after converge was added in v1.0.1.Open Questions
specify init(no prior.claude/skills/directory) on v1.0.1 also omit converge, or is it strictly a--force/upgrade-path issue?]Posted on behalf of
@mnriemby GitHub Copilot (model: claude-sonnet-4.6)Fix converge-missing-manifest: track generated skill files
github-actions commentedon Oct 15, 2026
Pull request created: #4990
github-actions commentedon Oct 15, 2026
Draft fix PR created on
fix/4273-converge-missing-manifest: converge-missing-manifest now records generated skill files atomically and verifies everySKILL.mdis present in the manifest. Please review the draft and validate it in the stage-3bug-testworkflow, if configured.Assisted-by: GitHub Copilot (model: gpt-5.2-codex, autonomous)
github-actions commentedon Oct 19, 2026
Bug test — converge-missing-manifest: ✅ passing · 58 passed, 0 failed, 0 skipped · fix from linked PR #4990 (head
bb3f14322fcd9fb7618d63e281557caa7d4a3e48)Bug Test Report: specify init omits speckit-converge from the manifest
bb3f14322fcd9fb7618d63e281557caa7d4a3e48)timeout 600 python3 -m pytest tests/integrations/test_integration_base_skills.py tests/integrations/test_integration_claude.pySummary
The proposed fix's focused integration tests all passed. The regression coverage confirms that generated skill files, including
speckit-converge/SKILL.md, are recorded in the manifest both for a fresh setup and when setup runs over an existing nine-skill installation.Test Results
Failures (if any)
None.
Verification vs. Historical Fix
Partially matches. Historical PR #4405 overlaps with the generated fix on the core remediation: routing generated integration files through
IntegrationManifest.record_file, plus Claude manifest coverage. The generated PR #4990 additionally includes aSkillsIntegration.setup()postcondition and base integration tests for the generated-to-manifest inverse relationship and re-init over a pre-existing nine-skill installation. PR #4405 is closed and was not merged; no historical implementation was used as the test revision.Notes & Caveats
pyproject.tomlwith pytest configuration). Pytest and runtime dependencies were already available; no dependency installation was needed.