{"record":{"id":"dc8e1825495b415c","repo":"github/spec-kit","slug":"invalid-extension-registry-registry-extensions","errorCode":null,"errorMessage":"Invalid extension registry {registry}: 'extensions' must be a mapping","messagePattern":"Invalid extension registry (.+?): 'extensions' must be a mapping","errorType":"exception","errorClass":"TemplateResolutionError","httpStatus":null,"severity":"error","filePath":"scripts/python/common.py","lineNumber":263,"sourceCode":"    extensions: dict[object, object] = {}\n    if os.path.lexists(registry):\n        if not registry.is_file():\n            raise TemplateResolutionError(\n                f\"Invalid extension registry {registry}: not a regular file\"\n            )\n        try:\n            data = json.loads(registry.read_text(encoding=\"utf-8\"))\n        except (OSError, UnicodeError, json.JSONDecodeError) as exc:\n            raise TemplateResolutionError(\n                f\"Failed to parse extension registry {registry}: {exc}\"\n            ) from exc\n        if not isinstance(data, dict):\n            raise TemplateResolutionError(\n                f\"Invalid extension registry {registry}: root must be a mapping\"\n            )\n        raw_extensions = data.get(\"extensions\", {})\n        if not isinstance(raw_extensions, dict):\n            raise TemplateResolutionError(\n                f\"Invalid extension registry {registry}: \"\n                \"'extensions' must be a mapping\"\n            )\n        extensions = raw_extensions\n        registered_ids = {\n            ext_id for ext_id in extensions if isinstance(ext_id, str)\n        }\n\n    ranked: list[tuple[int, str]] = []\n    for ext_id, metadata in extensions.items():\n        if (\n            _is_safe_component(ext_id)\n            and isinstance(metadata, dict)\n            and bool(metadata.get(\"enabled\", True))\n        ):\n            ranked.append((_normalize_priority(metadata.get(\"priority\")), ext_id))\n\n    try:","sourceCodeStart":245,"sourceCodeEnd":281,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/scripts/python/common.py#L245-L281","documentation":"_StagedWorkflowFile.verify_path() stats the staged path with follow_symlinks=False and fstats the open fd; if either stat call raises OSError, it wraps it as 'Staged workflow file changed before commit'. At this point the staged file was just written, so a failing stat means the path vanished or became inaccessible between write and commit — evidence of tampering or filesystem trouble — and the atomic commit is aborted.","triggerScenarios":"Something removes or re-permissions the staged file (typically under .specify/tmp or the staging dir) between os.write and verify: concurrent cleanup (another specify run, tmp reaper, `git clean`), antivirus/SED quarantining freshly written files, or a flaky mount dropping the inode.","commonSituations":"Parallel `specify` invocations racing in the same repo; tmp-file reapers or CI janitors deleting new files in .specify/tmp; antivirus on macOS/Windows delaying or removing just-written executables; interrupted runs leaving cleanup handlers that fire late.","solutions":["Ensure only one specify/workflow process runs against the project at a time (lock or serialize CI steps)","Exclude .specify (and the staging dir) from tmp-reaper/antivirus/cleaner tooling and `git clean -fdx`","Retry the install once after the race source is removed","If it persists, strace/fs_usage the run to find which process unlinks the staged path"],"exampleFix":"# before\n# CI: two jobs run specify simultaneously in one workspace\n\n# after\n# serialize the steps\n- run: specify workflow add my.yaml\n- run: specify build  # after, not parallel","handlingStrategy":"retry","validationCode":"import os, stat\n\ndef staged_path_still_valid(staged_path: os.PathLike, fd: int) -> bool:\n    try:\n        ps = os.stat(staged_path, follow_symlinks=False)\n        fs = os.fstat(fd)\n    except OSError:\n        return False\n    return stat.S_ISREG(ps.st_mode) and (ps.st_dev, ps.st_ino) == (fs.st_dev, fs.st_ino)","typeGuard":null,"tryCatchPattern":"try:\n    staged.verify_path()\nexcept OSError as e:\n    if 'changed before commit' in str(e):\n        restage_and_retry_once()  # reopen a fresh staged file and rewrite\n    else:\n        raise","preventionTips":["Serialize specify/workflow processes per project (locks, sequential CI steps)","Exclude .specify and staging dirs from tmp janitors, antivirus, and git clean -fdx","Retry once after removing the racing cleanup source"],"tags":["workflow","staging","race-condition","filesystem"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}