Skip to content

[Bug]: specify init writes speckit.manifest.json without the speckit-converge skill it just installed #4273

Open

Description

@jabelk

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

  1. Gambit-Checkmate commented on Aug 22, 2026

    @Gambit-Checkmate

    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?

  2. mnriem commented on Sep 1, 2026

    @mnriem
    Collaborator

    @Gambit-Checkmate Absolutely, go for it. I have triggered our bug assess pipeline so you will get some additional background

  3. github-actions commented on Sep 1, 2026

    @github-actions
    Contributor

    Bug assessment — converge-missing-manifest: Valid · severity medium


    Bug Assessment: converge-missing-manifest

    Report (summarized)

    After running specify init --here --integration claude --force with spec-kit v1.0.1 (upgrading from v0.5.0), the file .specify/integrations/claude.manifest.json (referred to as speckit.manifest.json in the report — see note below) contains 9 speckit-* SKILL.md entries but is missing speckit-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.json is 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 at claude.manifest.json and using "speckit.manifest.json" generically.

    Symptom

    specify init --here --integration claude --force installs all 10 speckit-* SKILL.md files to .claude/skills/ (including speckit-converge/SKILL.md) but the resulting claude.manifest.json records only 9 of them — speckit-converge/SKILL.md is absent. Any tooling that consults the manifest to classify files (pristine / modified / missing / untracked) will treat speckit-converge/SKILL.md as untracked, preventing clean uninstall, upgrade diff-detection, and hash-based freshness checks for that skill.

    Reproduction

    1. Create a project with spec-kit v0.5.0: specify init myproject --integration claude
    2. Upgrade the CLI to v1.0.1.
    3. From inside myproject/, run specify init --here --integration claude --force.
    4. Inspect .specify/integrations/claude.manifest.json — check files keys for speckit-*/SKILL.md entries.
    5. Observe that .claude/skills/speckit-converge/SKILL.md exists 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 --force re-inits of existing projects.]

    Suspected Code Paths

    • src/specify_cli/integrations/base.py:1725–1818 (SkillsIntegration.setup() loop) — the main loop that calls write_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 calls manifest.record_existing(rel). If record_existing raises (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()) — raises ValueError if any path component traversed from self.project_root is 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 fresh IntegrationManifest, calls setup(), then manifest.save(). After manifest.save() (line 698), _register_extensions_for_agent and _register_presets_for_agent are called when --force is 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 .md files from shared_commands_dir(). Converge is present in templates/commands/converge.md and 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. The SkillsIntegration.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. When specify init --force runs setup() 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 created speckit-converge/ directory resolves differently (e.g., a stale inode, platform-specific casing, or a symlink left by a prior install) and manifest.record_existing() subsequently raises ValueError for that path after the file is written, then...

    However, setup() does not have a per-iteration try/except — a ValueError from record_existing would propagate to init.py's outer try/except Exception handler (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_record calls manifest.record_existing(rel) where rel is computed as dest.resolve().relative_to(project_root.resolve()) (a Path object). record_existing re-walks from manifest.project_root to verify no symlinks. If project_root passed to write_file_and_record differs from manifest.project_root (even by trailing slash or symlink resolution), the reconstructed absolute path for converge's SKILL.md may not match what was written, so _sha256(abs_path) operates on a slightly different path. The save() 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 inside SkillsIntegration.setup() as a postcondition assertion (or a warning log) that counts len([f for f in created if f.name == "SKILL.md"]) against len(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 the manifest.record_existing(rel) call with the more direct manifest.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: iterate skills_dir.rglob("SKILL.md"), and for any SKILL.md found under a speckit-* directory that is NOT yet in the manifest, call manifest.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 to record_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:

    • A test that pre-populates a tmp_path with 9 SKILL.md stubs under .claude/skills/speckit-*/ (simulating an existing v0.5.0 install without converge) and then calls setup() with a fresh manifest — verifies all 10 skills including converge appear in manifest.files after setup().
    • A test that calls setup() twice with a fresh manifest each time (simulating --force reinit) and asserts both manifest objects record the same 10 entries.
    • Extend test_all_files_tracked_in_manifest to also check the inverse: that len([k for k in m.files if k.endswith("SKILL.md")]) equals len(templates) (currently the test only checks files in created are tracked, not that ALL templates produced entries).

    Risks & Considerations

    • Uninstall breakage: specify integration uninstall claude will not remove speckit-converge/SKILL.md because 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.
    • Upgrade stale-file pass: The integration upgrade stale-file pass (Phase 2 in _migrate_commands.py:844–867) computes stale_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.
    • No data loss: The converge skill file is fully intact on disk; only the manifest entry is missing. Recovery is manual: re-run specify integration upgrade claude or specify init --here --force once the bug is fixed.
    • Blast radius: Affects every skills-based integration (not just claude) that ran specify init --force over a pre-existing project after converge was added in v1.0.1.

    Open Questions

    • [NEEDS CLARIFICATION: Does a fresh specify init (no prior .claude/skills/ directory) on v1.0.1 also omit converge, or is it strictly a --force/upgrade-path issue?]
    • [NEEDS CLARIFICATION: Is the missing entry consistently the converge skill, or could it be whichever skill happens to fall at position 4 in the template order? If the issue is positional (e.g., an off-by-one in a loop boundary), earlier or later commands would also be affected.]

    Posted on behalf of @mnriem by GitHub Copilot (model: claude-sonnet-4.6)

    Generated by 🐛 Assess Bug from Labeled Issue for issue #4273 · 2.8K AIC · ⌖ 23.4 AIC · ⊞ 34.2K · ◷

  4. added
    triage-must-haveVerdict: high-value, important work for Spec Kit — do first
    bug-fixTrigger the bug-fix agentic workflow
    on Oct 15, 2026
  5. github-actions commented on Oct 15, 2026

    @github-actions
    Contributor

    Pull request created: #4990

    Generated by Fix Bug from Labeled Issue · copilot · gpt52codex · 3.34 AIC · ⌖ 0.696 AIC · ⊞ 24.6K

  6. github-actions commented on Oct 15, 2026

    @github-actions
    Contributor

    Draft fix PR created on fix/4273-converge-missing-manifest: converge-missing-manifest now records generated skill files atomically and verifies every SKILL.md is present in the manifest. Please review the draft and validate it in the stage-3 bug-test workflow, if configured.

    Assisted-by: GitHub Copilot (model: gpt-5.2-codex, autonomous)

    Generated by 🛠️ Fix Bug from Labeled Issue for #4273 · copilot · gpt52codex · 3.34 AIC · ⌖ 0.696 AIC · ⊞ 24.6K · ◷

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions