github/spec-kit · error · ValueError

manifest.project_root ({manifest.project_root}) does not mat

Error message

manifest.project_root ({manifest.project_root}) does not match project_root ({project_root_resolved})

What it means

Raised by GenericIntegration.setup() (src/specify_cli/integrations/generic/__init__.py:115) when the IntegrationManifest was created against a different root than the resolved project_root argument. Like the copilot/forge/hermes setups, generic setup verifies manifest.project_root == project_root.resolve() before any writes, binding each manifest to exactly one project location.

Source

Thrown at src/specify_cli/integrations/generic/__init__.py:115

        )

    def setup(
        self,
        project_root: Path,
        manifest: IntegrationManifest,
        parsed_options: dict[str, Any] | None = None,
        **opts: Any,
    ) -> list[Path]:
        """Install commands to the user-provided commands directory."""
        commands_dir = self._resolve_commands_dir(parsed_options, opts)

        templates = self.list_command_templates()
        if not templates:
            return []

        project_root_resolved = project_root.resolve()
        if manifest.project_root != project_root_resolved:
            raise ValueError(
                f"manifest.project_root ({manifest.project_root}) does not match "
                f"project_root ({project_root_resolved})"
            )

        dest = (project_root / commands_dir).resolve()
        try:
            dest.relative_to(project_root_resolved)
        except ValueError as exc:
            raise ValueError(
                f"Integration destination {dest} escapes "
                f"project root {project_root_resolved}"
            ) from exc
        dest.mkdir(parents=True, exist_ok=True)

        script_type = opts.get("script_type", "sh")
        arg_placeholder = "$ARGUMENTS"
        created: list[Path] = []

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Build the manifest with the same resolved root passed to setup: IntegrationManifest(project_root=project_root.resolve()).
  2. Resolve project_root once and reuse that Path for both the manifest and the setup call.
  3. Use the CLI path (specify init --integration generic --integration-options="--commands-dir ...") which handles manifest creation.

Example fix

# before
manifest = IntegrationManifest(project_root=Path("proj"))
GenericIntegration().setup(Path("/abs/proj"), manifest, parsed_options={"commands_dir": "x"})

# after
root = Path("/abs/proj").resolve()
manifest = IntegrationManifest(project_root=root)
GenericIntegration().setup(root, manifest, parsed_options={"commands_dir": "x"})
Defensive patterns

Strategy: validation

Validate before calling

root = project_root.resolve()
if manifest.project_root != root:
    manifest = IntegrationManifest(project_root=root)

Try / catch

try:
    integration.setup(root, manifest, parsed_options=opts)
except ValueError as e:
    if "manifest.project_root" in str(e):
        manifest = IntegrationManifest(project_root=root.resolve())
        integration.setup(root, manifest, parsed_options=opts)
    else:
        raise

Prevention

When it happens

Trigger: GenericIntegration().setup(root, manifest) with manifest built from a different directory or an unresolved path variant (symlink differences) — the strict equality on resolved paths fails.

Common situations: Manual/programmatic use of the generic integration where the manifest comes from a fixture or another project; macOS /tmp vs /private/tmp; passing a relative project_root while the manifest recorded an absolute one.

Related errors


AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14). Data as JSON: /api/errors/fba53a777b819709. Report an issue: GitHub.