Skip to content

Integrate upstream github/spec-kit v1.0.6 - #124

Merged
satwareAG-ironMike merged 45 commits into
main-speckfrom
sync/upstream-v1.0.6
Sep 15, 2026
Merged

satwareAG-ironMike merged 45 commits into
main-speckfrom
sync/upstream-v1.0.6

Conversation

@satwareAG-ironMike

@satwareAG-ironMike satwareAG-ironMike commented Sep 15, 2026

Copy link
Copy Markdown

Summary

Integrates upstream github/spec-kit releases v1.0.5 + v1.0.6 into main-speck, following the established sync pattern (#116 / sync/upstream-v1.0.4).

  • Delta: 41 upstream commits (v1.0.4..v1.0.6), 102 files changed, +4119/-205 lines
  • Merge commit: 098ac5d2 (Merge tag 'v1.0.6' into sync/upstream-v1.0.6)
  • Metadata bump: specs/metadata.json -> version: 1.0.6, fork_version: satware-v1.0.6, project_version: 1.0.6 (af6abbbf)

Conflict resolutions

File Resolution Rationale
pyproject.toml theirs (1.0.6) Fork only diverges on version; metadata bump tracked separately
CHANGELOG.md theirs Fork has no local CHANGELOG modifications (verified: zero diff v1.0.4..HEAD on both files)

Auto-merged cleanly: tests/integrations/test_integration_cline.py, tests/test_extension_skills.py.

Notable upstream changes absorbed

Verification

Gate Result
pytest tests/ (own venv) 7847 passed, 13 skipped, 0 failed
scripts/daily-routine.sh pre-pr All 5 checks passed (ruff, integration tests, privacy, upstream sync, provenance)
Privacy leak check Clean (590 files scanned)
Upstream sync check OK: Up to date with upstream v1.0.6

Closes #122, closes #123 (rolling upstream-sync issues).


Filed by @satwareAG-ironMike (Michael Wegener); authored by Jane Alesi (GLM via satware AI harness, autonomous session).

mnriem and others added 30 commits September 2, 2026 16:08
* chore: bump version to 1.0.4

* chore: begin 1.0.5.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* Add Axi Extension to community catalog

Add axi extension submitted by @d0whc3r to:\n- extensions/catalog.community.json (alphabetical order)\n- docs/community/extensions.md community extensions table\n\nCloses github#3948\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>\nAssisted-by: GitHub Copilot (model: gpt-5.2-codex, autonomous)

* Add tools requirement to catalog community JSON

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ken Schlobohm <keschlob@microsoft.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* allow hyphen in command ref token names

the token pattern was matching only A-Z 0-9 and underscore so a command
name like speckit.agent-context.update can not be written as a token and
the token stays in the output as plain text

now the character class allows a hyphen also in both places that resolve
the token

* document that a hyphen stays inside a segment

the guide still said the token scheme does not carry hyphens, which is the
opposite of what this branch does. added the real bundled command as the
example since speckit.agent-context.update is the one that was unreachable
before this.

* replace the skills mode limitation with what skills mode actually does

the callout said a command ref token reaches codex zcode and kimi verbatim.
it does not. _resolve_command_ref_tokens inside _register_extension_skills
resolves the same token shape against the active skill style. the callout was
right that resolve_command_refs is never called there and wrong about what
follows from it.

* name the right invocation for each skills agent

the callout said kimi renders the bare slash form. it does not, kimi is in
SKILL_COLON_AGENTS only and falls through both branches to its own
build_command_invocation which returns /skill:speckit-<name>.
…owerShell twins (github#4286)

* fix(scripts): make bash branch-name sanitizing match the Python and PowerShell twins

* fix(scripts): use ASCII acronym boundaries in the Python and PowerShell twins

* test(scripts): cover the ASCII acronym boundary in the PowerShell twins

The ASCII-lookaround fix in scripts/powershell/create-new-feature.ps1 and
  extensions/git/scripts/powershell/create-new-feature-branch.ps1 had no
  PowerShell regression coverage: the accented-acronym parity cases invoked
  only bash and Python, and the existing PowerShell acronym tests used
  ASCII-separated words, so a regression there would have passed CI.

  Adds a three-way bash/Python/pwsh assertion for 'Fix eDBe sync' in the core
  parity suite and a pwsh arm to the extension parity test, both asserting
  001-fix-db-sync.

* test(scripts): cover clean_branch_name independently of generate_branch_name
* feat(workflows): add plugin slots

Assisted-by: GitHub Copilot (model: gpt-5.6-terra, autonomous)

* fix(workflows): add runtime fan-out guard to PluginStep

Mirror GateStep's inside_fan_out check so plugin slots inside fan-out
templates fail at execution time, not only at static validation. This
closes the gap when WorkflowEngine.execute() is called without prior
validation.

Assisted-by: opencode (model: qwen3.7-max, supervised)

* refactor(workflows): rename plugin slots to workflow slots

The feature reserves a no-op position replaced through a workflow
overlay; it does not register or resolve plugins. Rename per maintainer
feedback so 'plugin' stays available for a future genuine plugin
mechanism and avoid confusion with Spec Kit extensions:

- type: plugin -> type: slot
- PluginStep -> SlotStep
- 'plugin step/slot' -> 'workflow slot' in prose and error messages
- steps/plugin/ -> steps/slot/ (git mv)
- test_plugin_step.py -> test_slot_step.py (git mv)

Assisted-by: opencode (model: glm-5.3-flash, autonomous)

---------

Co-authored-by: Markus <markus@example.com>
Muse Code is Meta's terminal coding agent (binary: muse). It discovers
project skills at .agents/skills/<skill-id>/SKILL.md and invokes them
via the /speckit-<command> slash shortcut, so wire it up as a
SkillsIntegration sharing the .agents/skills layout with Codex/Zed
(multi_install_safe=False, same policy as docker-agent).

Includes registry wiring, invocation-style mapping, init next-steps,
discovery catalog, integrations doc, agent-context default (AGENTS.md),
issue templates, and a dedicated test module.
…4401)

* fix: drop unused scope input from the bundled speckit workflow

The Full SDD Cycle workflow prompted for full / backend-only /
frontend-only, but none of the steps read inputs.scope. Every command
only received inputs.spec, so the three choices behaved the same.

Remove the dead input from the shipped workflow and align the docs that
showed `specify workflow run speckit ... -i scope=...`. Keep scope as an
example in the generic input-typing docs for authors who do wire it up.

* Bump bundled speckit workflow to 1.0.1 after dropping scope.

Catalog installs only see the dead input removed when the published
version advances past what they already have.

---------

Co-authored-by: Gyanu <gyanum.ug20.cse@gmail.com>
Add evaluator extension submitted by @tbitcs to:
- extensions/catalog.community.json (alphabetical order)
- docs/community/extensions.md community extensions table

Closes github#4414

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ithub#4149)

Both loop steps type-check `steps` ("must be a list") but never require it
to be present, so an absent body silently becomes `[]`. `if` already
requires `then`, and `fan-out` already requires both `items` and `step`.

The mistype is unusually easy here because the fan-out step's own payload
key is the singular `step:` while the loops use `steps:`. Writing `step:` on
a `while` passed `specify workflow validate` with zero errors:

  A. while, body key typo'd as singular step:
     validate: []
     execute : StepStatus.COMPLETED | next_steps = []
  B. do-while, no steps at all:
     validate: []
     execute : StepStatus.COMPLETED | next_steps = []

At run time the step reports COMPLETED while returning no `next_steps`, so
the engine's `if result.next_steps:` block never fires and the loop the
workflow is built around never runs even once. `DoWhileStep`'s own docstring
promises "The first invocation always returns the nested steps for
execution".

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…thub#4397)

* fix(scripts): name setup-plan's feature directory key FEATURE_DIR

setup-plan emitted a key called SPECS_DIR holding $FEATURE_DIR -- the
per-feature subdirectory, not the specs root. The name is already taken
elsewhere with the other meaning: create-new-feature.sh sets
SPECS_DIR="$REPO_ROOT/specs" and derives FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME".

setup-plan was also the only script in the suite using it. setup-tasks and
both check-prerequisites payloads already emit FEATURE_DIR for exactly this
value, so this brings setup-plan in line rather than inventing a convention.

Renamed in all three ports so the payloads stay identical, and in
templates/commands/plan.md, which is the only consumer -- it parses the key
by name, so it has to move in the same commit.

Verified the bash, PowerShell, and Python variants all emit
['BRANCH','FEATURE_DIR','FEATURE_SPEC','IMPL_PLAN'].

Fixes github#4017

* test(scripts): pin setup-plan's FEATURE_DIR output contract

Addresses review feedback. The existing setup-plan tests compare the ports
against each other, so all three could regress to SPECS_DIR together and
still pass. This asserts the contract absolutely, in JSON and text mode and
across bash/Python/PowerShell: the key is FEATURE_DIR, it carries the
feature directory rather than the specs root, and SPECS_DIR is absent.

The value is matched by suffix rather than full path because the ports
legitimately differ in path flavour -- under MSYS bash reports /tmp/... where
the Python and PowerShell ports report C:\... . The suffix still separates
specs/001-my-feature from a bare specs, which is the regression being
guarded; verified it rejects both /tmp/proj/specs and C:\proj\specs.
…ithub#4424)

docs/reference/workflows.md introduces its YAML block as the workflow that
ships with Spec Kit, so a reader is entitled to treat it as the real
definition. It had drifted on four points:

  version           1.0.0                      -> 1.0.1
  speckit_version   >=0.7.2                    -> >=0.8.5
  integrations.any  copilot, claude, gemini    -> also alquimia, opencode
  integration       default "copilot"          -> default "auto"

The last is the most user-visible: the guide stated the default integration
was copilot, when it is auto, resolved from the project's initialized
integration. Someone reading the guide to learn what they get by default was
being told the wrong thing.

Adds a guard so this cannot drift again. It compares parsed YAML rather than
text, so the guide stays free to format lists however reads best and only the
content has to agree. Verified it fails against the pre-sync copy, reporting
all four differences, and passes after.

Follow-up to github#4384 / github#4398, at the maintainer's suggestion.
…4320)

* fix(presets): reject falsy non-mapping catalog config shapes

`PresetCatalog._load_catalog_config` had two "shape check runs after
an emptiness check" bugs, both masking a corrupted preset-catalogs.yml
as an empty/no-op config instead of raising:

- Top level: `yaml.safe_load(...) or {}` coerced a FALSY non-mapping
  document (`[]`, `false`, `0`, `''`) to `{}` before the
  `isinstance(data, dict)` guard ran, so it was silently treated as
  "no config" — while a TRUTHY non-mapping (a bare string) already
  raised "expected a mapping at root".
- One level down: `catalogs_data = data.get("catalogs", [])` followed
  by `if not catalogs_data: return None` ran the emptiness check
  *before* the `isinstance(catalogs_data, list)` check, so a FALSY
  non-list `catalogs:` value (`{}`, `''`, `0`, `false`) was silently
  swallowed as "no catalogs" — while a TRUTHY non-list
  (`catalogs: "not-a-list"`) already raised "must be a list".

`WorkflowCatalog._load_catalog_config` and
`StepCatalog._load_catalog_config` (workflows/catalog.py) already
guard against both cases correctly, with the same explanatory
comments reused here. This preset sibling was missed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FW9fAYsCBCAgdKWovtSyqt

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Fix indentation in test for catalog config loading

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…te (github#4150)

`ClineIntegration._inject_hook_command_note` guarded idempotency with a
whole-document substring scan -- `if "replace dots" in content: return
content` -- instead of the per-instruction check the shared
`SkillsIntegration._inject_hook_command_note` helper uses (base.py:1637-1642),
which compares only the line immediately above each match.

Two consequences, both verified on main:

  A. unrelated prose -> note injected? False      (base: True)
  B. two hook sections, one already noted -> notes: 1   (want 2)

(a) Any command or extension markdown whose prose happens to contain the
phrase "replace dots" loses the note entirely, so the generated workflow
tells the agent to emit `/speckit.git.commit` -- a dotted command Cline never
registers, since Cline installs `speckit-git-commit.md`.

(b) A document with one already-noted hook section never gets a note on a
second, un-noted one -- exactly what the base helper was changed to handle.

Also aligns the capture group with the base helper: `^([ \t]*)` rather than
`^(\s*)`. Because `\s` matches newlines the captured "indent" could swallow a
preceding blank line, which was then re-emitted between the note and the
instruction:

  '## Hooks\n\n  <note>\n\n  - For each executable hook, ...'

The per-instruction check cannot line up until this is fixed.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…#4321)

* fix(workflows): reject falsy non-mapping step.yml in step add

`workflow_step_add` parses a fetched `step.yml` with
`_yaml.safe_load(...) or {}`, which coerces a FALSY non-mapping
top-level document (`[]`, `false`, `0`, `''`) to `{}` before the
`isinstance(meta, dict)` shape check runs. The command then proceeds
with `meta = {}`, derives `step_meta = {}` and `type_key = ""`, and
reports the unrelated "step.yml missing 'step.type_key' field"
instead of the real problem: "step.yml must be a YAML mapping". A
TRUTHY non-mapping document (a bare string) already reported the
correct error — this was an inconsistency.

Same falsy-or-coerce shape as the catalog-config bugs fixed elsewhere
in workflows/catalog.py, presets/__init__.py, and integrations
(github#4187) this cycle.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FW9fAYsCBCAgdKWovtSyqt

* fix(workflows): distinguish explicit YAML null from empty step.yml

An explicit null document (null/~/NULL) parses to the same None as a
genuinely empty document, so it was silently coerced to {} and
misreported as the unrelated "missing step.type_key" error instead of
the mapping-shape error, per Copilot review on PR github#4321. Use
yaml.compose to tell the two apart, matching the sibling loaders
(yamlio.py, integrations/catalog.py, overlays/layer_sources.py).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0147sii7uC56YzAu2Ep9qH94

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
… tasks (github#4430)

* fix(tasks): require field constraints from data-model.md in generated tasks

/speckit.tasks mapped data-model.md entities to user stories but never
told the agent to carry field-level constraints (max length, nullable,
enum values, validation rules) into the task text. Left to discretion,
the implementing agent can silently invent its own value instead of the
one recorded in data-model.md.

Fixes github#4383

* test(tasks): tighten constraint regression assertion to match verbatim wording

Addresses Copilot review feedback that the prior assertion only checked
for the word 'constraint', so it would still pass if the rule's meaning
were reversed (e.g. constraints permitted to be omitted).
Bumps the codeql-action group with 2 updates: [github/codeql-action/init](https://github.com/github/codeql-action) and [github/codeql-action/analyze](https://github.com/github/codeql-action).


Updates `github/codeql-action/init` from 4.37.8 to 4.37.9
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@db488dd...cdf488f)

Updates `github/codeql-action/analyze` from 4.37.8 to 4.37.9
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@db488dd...cdf488f)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.37.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-action
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.37.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-action
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Update brownkit extension submitted by @MaksimShevtsov:
- extensions/catalog.community.json (version, download_url, metadata)
- docs/community/extensions.md community extensions table

Closes github#4435

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update okf extension submitted by @alexcpn:\n- extensions/catalog.community.json (version, download_url, metadata, and tool requirements)\n- docs/community/extensions.md community extensions table\n\nCloses github#4439\n\nAssisted-by: GitHub Copilot (model: gpt-5.2-codex, autonomous)\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* chore: bump version to 1.0.5

* chore: begin 1.0.6.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…thub#4402)

* docs: resolve assess clarifications by editing artifacts in place

The assess README and decide command told people to re-run an earlier
stage when decision.md said needs-clarification. That fights the
sequential intake → research → define → shape → decide flow: each
command normally runs once and leaves an editable Markdown file.

Document direct edits and free-form agent updates as the default path,
including refining decision.md itself, and keep stage re-runs as the
exception for bad or discarded drafts.

* Clarify needs-clarification as in-place artifact refinement.

Route the assess diagram through a refine step back to decide, and list
decision.md among the artifacts that path can update.

* Narrow assess clarification docs to user-editable artifacts

Keep speckit.assess.decide's existing output template. Clarifications
are filled in the Markdown after the command finishes, not by changing
what the command writes.

---------

Co-authored-by: Gyanu <gyanum.ug20.cse@gmail.com>
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ep refresh (github#4139)

* fix(bundler): re-read the step registry when rolling back a failed refresh

`_StepKindManager.refresh` documents that it keeps a backup and restores it
"if the remove+reinstall path fails". The package half of that rollback
works; the registry half was unreachable.

`StepRegistry.__init__` snapshots the file once (`self.data = self._load()`)
and `is_installed` consults only that snapshot. Measured:

  snapshot at construction:          is_installed('my-step') = True
  after the entry is deleted on disk: same object            = True   <-- stale
  a fresh StepRegistry:                                      = False

By rollback time `self.remove()` has already deleted the entry from disk,
but `self._registry`'s snapshot still contains it — so
`not self._registry.is_installed(...)` was always False and the restore
never ran, in exactly the failure case it was written for.

The user was left with the step package back on disk but unregistered:
`workflow step list` no longer shows it, the engine cannot resolve it, and
a later `workflow step add <id>` refuses with "Step directory already
exists".

Read the registry fresh at rollback time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(bundler): restore the step registry entry verbatim on refresh rollback

Addresses review feedback: the rollback used `StepRegistry.add()`, which does
not restore the saved metadata verbatim.

The rollback deliberately constructs a *fresh* `StepRegistry` after
`self.remove()` has deleted the entry from disk (that re-read is this PR's
actual fix). `add()` therefore finds no existing record:

    metadata_to_store["installed_at"] = existing.get(
        "installed_at", datetime.now(timezone.utc).isoformat()
    )
    metadata_to_store["updated_at"] = datetime.now(timezone.utc).isoformat()

`existing` is `{}`, so `installed_at` falls through to `now`, and `updated_at`
is overwritten unconditionally. A failed refresh still mutated the
installation metadata instead of rolling it back:

    seeded      : installed_at 2020-01-01..., updated_at 2020-02-02...
    after add() : installed_at 2026-08-27..., updated_at 2026-08-27...
    verbatim via add()          -> False
    verbatim via direct restore -> True

Restore the entry directly and save, matching the existing rollback in
`workflow_step_remove`, whose comment names this very hazard: "Restore the
original registry entry verbatim (bypass add() which would overwrite
timestamps)."

The regression test now seeds distinctive past timestamps and asserts the
restored entry equals the seeded one, rather than only asserting presence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ary, review-only material (github#4431) (github#4432)

* fix(templates): stop /constitution from stacking Sync Impact Reports (github#4431)

Step 4 of the constitution command only said to prepend the Sync Impact
Report as an HTML comment, with no instruction to remove a previous one.
Each run therefore added another report block on top of the last,
growing the raw file (and the token cost of reading it) without bound.
Now the step explicitly requires removing any existing report comment
before adding the new one.

Assisted-by: Claude (model: claude-sonnet-5, autonomous)

* docs(templates): state the Sync Impact Report's temporary lifecycle

Per review on github#4432/github#4431: the growth this fixed isn't a functional
bug in the intended workflow — the report is scratch material for
human review and is expected to be removed before the constitution
file is committed. Step 4 now says so explicitly, alongside the
existing replace-not-stack instruction for runs where a prior report
was left in place.

* docs(templates): drop the Sync Impact Report de-dupe instruction

Per review, Step 4 should stay documentation-only: the report is
temporary, review-only material removed before commit, so /constitution
shouldn't auto-strip a pre-existing HTML comment (which could delete
unrelated leading content). Retarget the regression test to pin the
documented lifecycle instead of the removed de-dupe behavior.
* Add concise code review skill

Add repository-specific guidance for positive and negative tests, regression evidence, and terminology alignment while exempting community-authored catalog content.

Assisted-by: GitHub Copilot (model: gpt-5.6-luna, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 60c0e36f-daf2-4c62-87ee-4da8fb1eaf12

* Clarify community content exception

Exclude community-authored catalog content and generated documentation from terminology alignment reviews.

Assisted-by: GitHub Copilot (model: gpt-5.6-luna, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 60c0e36f-daf2-4c62-87ee-4da8fb1eaf12

* Clarify test coverage principle

Require test cases to verify intended behavior and what each code change should prevent.

Assisted-by: GitHub Copilot (model: gpt-5.6-luna, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 60c0e36f-daf2-4c62-87ee-4da8fb1eaf12

---------

Copilot-Session: 60c0e36f-daf2-4c62-87ee-4da8fb1eaf12
…ng hooks silently (github#4456)

* fix(templates): report an unreadable extensions.yml instead of skipping hooks silently

All ten core command templates told the agent: "If the YAML cannot be
parsed or is invalid, skip hook checking silently and continue normally".
A `.specify/extensions.yml` with one malformed line was therefore treated
exactly like a manifest with no hooks, and a mandatory hook
(`optional: false`, as registered by the bundled git extension) was
disabled without anything saying so.

The agent now tells the user that the manifest could not be read (with
the parser error) and that no hooks were checked, then continues as
before. Control flow is unchanged; only the silence is removed.

Adds tests/test_command_template_hooks.py, which pins the wording at
both hook sites of every template that reads extensions.yml.

* test(templates): pin every clause of the parse-failure instruction

The regression test only pinned two of the five clauses the new
instruction carries. Dropping the parser error, the mandatory-hook
warning or the continuation clause from every template would have left
it green.

It now pins all five: the manifest could not be read, the parser error
is included, no hooks were checked, mandatory (optional: false) hooks are
named, and the command then continues. Checked by mutation: removing any
one clause from all ten templates fails the ten parametrized cases.
…tension changes (github#4395)

* ci(extensions): guard bundled extension changes behind a version bump

Bundled extensions only reach existing installs through a version bump:
`specify extension update` compares the semver in extensions/catalog.json
against the installed copy and reports "Up to date" whenever they match.
Content changes shipped without a bump go silently stale on every
project that already installed the extension (github#4345). This guard turns
"please remember to bump" into a merge requirement.

- `.github/scripts/check_extension_version_bump.py` fails a PR that
  changes files under `extensions/<id>/` for a catalog-listed extension
  without increasing that extension's `extension.yml` version (PEP 440
  comparison, the same semantics `extension update` uses), and requires
  `extensions/catalog.json` to stay in sync with each manifest.
  Non-catalog extensions (the `selftest` fixture and the `template`
  scaffold) are exempt.
- `extension-version-guard.yml` runs the check on pull requests
  touching `extensions/**`, diffing the PR base against HEAD.
- Contract tests pin the working-tree half of the invariant
  (catalog/manifest version sync, bundled entries shipping a directory);
  guard-script tests pin the failure behavior against real throwaway
  git repositories so a parsing change cannot silently disable the
  guard while CI stays green.

Split out of github#4351 (part 3 of the series requested in review); refs github#4345.

Assisted-by: Claude Code (model: claude-fable-5)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): make the extension version guard usable as a required check and quoting-proof

Address Copilot review round 1 on github#4395.

Workflow: drop the `paths: extensions/**` filter. A required status check
that is skipped by path filtering stays in "Expected" state and blocks
every PR that does not touch extensions/**, which defeats the point of
making the guard a merge requirement. The job now runs on every pull
request; the script already reports success when nothing under
extensions/ changed, so unrelated PRs pass in one short job.

Script: read the changed-path list with `git diff --name-only -z`. With
git's default core.quotePath, a path containing non-ASCII or control
characters is C-quoted with the quotes included
(`"extensions/demo/caf\303\251.txt"`), so its first component was no
longer `extensions` and an unbumped change to such a file escaped the
guard. NUL-delimited output is emitted verbatim; paths are decoded with
surrogateescape so an undecodable byte cannot crash the check, and only
the ASCII `extensions/<id>/` prefix is ever interpreted.

Tests: pin both behaviors. The non-ASCII case fails against the previous
script and passes now; the no-extension-changes case backs the workflow
change. core.quotePath is pinned to true in the fixture repo so the
regression exercises the quoting path even where a developer's global
config disables it.

Refs github#4345

Assisted-by: Claude Code (model: claude-fable-5-1)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(ci): validate a new extension's version even without a base to compare

Address @mnriem's review on github#4395.

For a brand-new extension the guard returned early at "no base manifest"
before ever parsing the head version, and the catalog check only tests
string equality. A new extension shipped with e.g. `not-a-version` in
both extension.yml and catalog.json therefore passed the guard, although
ExtensionManifest rejects a version packaging cannot parse
(ValidationError "Invalid version") and `extension update` skips catalog
entries whose version is invalid - the extension would be uninstallable
and never updatable.

Parse the head version before the new-extension early return and fail
closed with a dedicated message when it is not PEP 440. The base
version is now parsed separately, so an unparseable base still fails
with the "could not compare versions" message.

Tests: add test_new_extension_with_unparseable_version_fails (fails
against the previous script, passes now) and point the existing
unparseable-version test at the new head-version message, which fires
first for that scenario.

Refs github#4345

Assisted-by: Claude Code (model: claude-fable-5-1)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(ci): diff the extension version guard against the merge commit's first parent

The guard diffed github.event.pull_request.base.sha against the checked-out
pull-request merge commit. Those two are not built from the same base: the
payload SHA is the base-branch tip from when the PR was opened and is not
refreshed when the base moves, while refs/pull/N/merge is rebuilt against
the current tip. Any base-branch drift between the two is then attributed
to the PR.

Observed on github#4395 itself: base.sha was main as of Sep 1 (3eec154), the
merge commit was built on main as of Sep 3 (db64869), and two unbumped
bundled-extension changes merged in between (github#4413 touching agent-context,
github#4286 touching git) failed the guard on a PR that never touched either.

Fetch the merge commit with depth 2 so both parents are available and
diff against HEAD^1, which is by construction the base the merge was
built on. Fail loudly if HEAD is not a merge commit. The script's CLI is
unchanged; a regression test reproduces the trap in a throwaway repo (the
stale base blames the drift on the PR, HEAD^1 does not).

Refs github#4345

Assisted-by: Claude Code (model: claude-fable-5-1)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(ci): validate every catalog version, not only those of changed extensions

Address the "needs a closer look" items from the Copilot reviews on github#4395.

Invariant 2 skipped catalog entries without an in-repo directory before
parsing their version, and never parsed the in-repo manifest version at
all - PEP 440 validation lived only in Invariant 1, which sees just the
IDs with changed files under extensions/<id>/. Two gaps followed:

- a hosted (catalog-only) entry could carry `"version": "not-a-version"`
  and pass, although `extension update` skips exactly such entries when
  packaging.Version fails;
- promoting an existing uncataloged directory by adding only its catalog
  entry never entered Invariant 1, so a matching invalid string in
  manifest and catalog passed the plain equality check.

Invariant 2 now runs over every catalog entry: the catalog version must
be a non-empty string that parses as PEP 440, and for entries with an
in-repo directory the manifest version must parse as well before the
equality check. The catalog-side parse alone closes the promotion gap
(a valid catalog string plus equality forces a valid manifest string);
the manifest parse makes the failure name the manifest precisely.

Tests: `_run_guard` now omits the head argument by default, matching the
workflow's one-argument invocation, with a separate test for the
optional HEAD_REF; new cases cover the hosted-entry and both promotion
shapes (the two invalid ones fail against the previous script) plus a
valid promotion that must keep passing. The working-tree contract test
gains the matching every-catalog-version-parses check.

Refs github#4345

Assisted-by: Claude Code (model: claude-fable-5-1)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Jakub Baranowski <cr4zybaran@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Update figma-starter extension submitted by @vibhus:\n- extensions/catalog.community.json (version, download_url, metadata)\n- docs/community/extensions.md community extensions table\n\nCloses github#4463\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>\nAssisted-by: GitHub Copilot (model: gpt-5.2-codex, autonomous)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Update pay-x402 extension submitted by @shawn93676:
- extensions/catalog.community.json (version, download_url, description, commands, tags)
- docs/community/extensions.md community extensions table

Closes github#4446

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
github-actions Bot and others added 13 commits September 9, 2026 12:44
…thub#4485)

Add pdac-product extension submitted by @juangcarmona to:
- extensions/catalog.community.json (alphabetical order)
- docs/community/extensions.md community extensions table

Closes github#4453

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: preserve extension authors in generated skills

Carry extension author metadata through skill registration, aliases, and restoration after preset removal. Keep the existing default for core skills and extensions without an author.

Assisted-by: OpenAI Codex (model: GPT-6, autonomous)

* test: accept author in extension registrar stub

Assisted-by: OpenAI Codex (model: GPT-6, autonomous)

* docs: place author parameter description on registrar

Assisted-by: OpenAI Codex (model: GPT-6, autonomous)

* fix: normalize extension skill authors

Assisted-by: OpenAI Codex (model: GPT-5, autonomous)

---------

Co-authored-by: 胥寅 <xuuyin@dingtalk.com>
* feat(workflows): add per-step integration configuration

Assisted-by: OpenAI Codex (model: GPT-5, autonomous)

* fix(workflows): address per-step integration review feedback

Assisted-by: OpenAI Codex (model: GPT-5, autonomous)

* fix(workflows): let per-step Docker Agent args override legacy config

Assisted-by: OpenAI Codex (model: GPT-6, autonomous)

---------

Co-authored-by: 胥寅 <xuuyin@dingtalk.com>
…4478)

* docs: document contribution evidence gate and label taxonomy

Add an "Evidence gate" subsection, a high-level "Review rubric" (D1-D7,
scored 0-2 each), and a "Triage and author labels" reference to
CONTRIBUTING.md so the acceptance bar is discoverable before contributing.
Cross-links the existing AI-contributions section and three-open-PR cap
rather than restating them.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* docs: expand author labels, catalog channel, and close expectations

Add author-needs-proof and author-needs-rebase to the author-actions table,
a "Community catalog submissions" subsection pointing catalog changes at the
submission-issue flow, and a note on when items are closed as
triage-out-of-scope. Fix the author-awaiting phrasing ("handed off to").

author-needs-rescope is intentionally omitted: it does not yet exist on
github/spec-kit, so it is flagged for the maintainer rather than documented
as a live label.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* docs: add author-needs-rescope to author labels table

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Fix typo in CONTRIBUTING.md regarding AI-sourced contributions

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Update speckit-inventory extension submitted by @Yash-Chindam:\n- extensions/catalog.community.json (version, download_url, sha256, etc.)\n- docs/community/extensions.md community extensions table\n\nCloses github#4486\n\nAssisted-by: GitHub Copilot (model: gpt-5.2-codex, autonomous)\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* Update Inventory Alignment preset to v0.1.1

Update inventory-alignment preset submitted by @Yash-Chindam:\n- presets/catalog.community.json (version, download_url, sha256, requires, timestamps)\n- docs/community/presets.md community presets table (no row changes required)\n\nCloses github#4487\n\nAssisted-by: GitHub Copilot (model: gpt-5.2-codex, autonomous)\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix Inventory Alignment catalog version

Assisted-by: GitHub Copilot (model: GPT-5.4, autonomous)

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

* fix: revert unrelated canon-core preset version bump

Keep canon-core at 0.1.0 so its version matches its release asset. Limit the preset update to inventory-alignment.

Assisted-by: GitHub Copilot (model: gpt-6-astra, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Update Spec Kit Schedule extension to v0.7.4

Update schedule extension submitted by @jfranc38:\n- extensions/catalog.community.json (version, download_url, metadata)\n- docs/community/extensions.md community extensions table\n\nCloses github#4457\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>\nAssisted-by: GitHub Copilot (model: gpt-5.2-codex, autonomous)

* Apply suggestion from @KSchlobohm

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ken Schlobohm <keschlob@microsoft.com>
…hub#4503)

Update the stale workflow so issues and PRs are marked stale after 60
days of inactivity and closed 30 days later (90 days total), down from
150/30. Messages updated to match.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…irectory (github#3786)

* docs(core): SPECIFY_FEATURE sets the feature label, not the feature directory

docs/reference/core.md told users to set SPECIFY_FEATURE "to the feature
directory name ... to work on a specific feature when not using Git branches".
That does not work: SPECIFY_FEATURE only feeds get_current_branch /
Get-CurrentBranch (the feature *label*). The directory comes from
SPECIFY_FEATURE_DIRECTORY or .specify/feature.json.

Verified on main with the real helper -- with ONLY SPECIFY_FEATURE set:

    $ SPECIFY_FEATURE=001-photo-albums ... get_feature_paths
    ERROR: Feature directory not found. Set SPECIFY_FEATURE_DIRECTORY or run
           the specify command to create .specify/feature.json.
    exit=1

    $ SPECIFY_FEATURE_DIRECTORY=specs/001-photo-albums ... get_feature_paths
    FEATURE_DIR    -> <resolved>
    CURRENT_BRANCH -> 001-photo-albums

The code's own error message points at the other variable, and the doc's own
"Two resolution axes" note directly below already says the feature is selected
by SPECIFY_FEATURE_DIRECTORY / .specify/feature.json -- so the table row
contradicted both the code and the paragraph under it.

Describe what the variable actually does, note that /speckit.specify and the
Git extension normally set it, and point at the directory axis. Docs only.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(core): describe SPECIFY_FEATURE as an explicit label override

The row still misstated when and how the label is applied:

* "when there is no Git branch context" — get_current_branch and
  Get-CurrentBranch never inspect Git at all. They return the variable
  verbatim when set, and otherwise fall back to the basename of the
  resolved feature directory.
* "Normally set for you by /speckit.specify" — specify.md persists
  feature_directory to .specify/feature.json and never sets this
  variable.
* The Bash and Python feature scripts can only *print* a commented
  export hint, because a child process cannot change its parent's
  environment. The PowerShell scripts do assign $env:SPECIFY_FEATURE,
  but only reach the caller when run inside the current session.

Rewrite it as an explicit user-set label override, and distinguish the
printed persistence hint from actually setting the caller's environment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(core): attribute the label fallback to get_feature_paths, not get_current_branch

The row said the label "falls back to the basename of the resolved feature
directory" when SPECIFY_FEATURE is unset, and attributed that to
get_current_branch / Get-CurrentBranch. Those helpers return an EMPTY
string when the variable is unset — scripts/bash/common.sh:87 says so
outright ("Return empty to signal 'unknown'") and scripts/python/common.py
is `return os.environ.get("SPECIFY_FEATURE", "")`.

The basename substitution happens later, in get_feature_paths /
Get-FeaturePaths, after the feature directory has been resolved
(scripts/python/common.py:168-169). Measured:

  get_current_branch (unset)       -> []
  get_current_branch (set)         -> [my-label]
  get_feature_paths CURRENT_BRANCH -> [001-photo-albums]

So a caller invoking the named helpers directly does not get the fallback.
Distinguish the two behaviours.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(core): name the PowerShell helper Get-FeaturePathsEnv

The doc cited `Get-FeaturePaths`, which does not exist. The PowerShell twin of
`get_feature_paths` is `Get-FeaturePathsEnv`
(scripts/powershell/common.ps1:152); there is no bare `Get-FeaturePaths`
anywhere in the tree.

Verified every function name the entry cites now resolves against the scripts:
get_current_branch, Get-CurrentBranch, get_feature_paths, Get-FeaturePathsEnv.
The quoted resolution error is verbatim from scripts/bash/common.sh:206.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…command (github#4337)

* fix(events): cap stdin in the generated dispatcher, not just the CLI command

The github#3857 fix capped stdin at 1 MiB in `specify event run`
(src/specify_cli/commands/event.py), but that command is not the code path
native hooks actually invoke. Every installed integration writes a
self-contained `.specify/events.py` dispatcher (the
`_EVENTS_DISPATCHER_TEMPLATE` string in src/specify_cli/events.py) that
native hook configs call directly, and its `main()` did:

    payload = sys.stdin.read() if not sys.stdin.isatty() else "{}"

with no size cap at all — the exact DoS github#3857 was meant to close, wide open
on the primary invocation path. `specify event run` is a secondary/manual
entry point; the generated dispatcher is what actually runs on every
session_start/pre_tool_use/etc. hook fire in real usage.

Fix: apply the same byte-capped read (from the binary buffer, so the cap
counts encoded bytes rather than decoded characters — matching the
just-merged fix for the CLI command) inside the dispatcher template, so
every newly-installed or refreshed dispatcher enforces the limit.

## Test plan
- Added 3 tests in tests/integrations/test_events.py::TestCommandRunner:
  an oversized payload exits 1 with the limit message instead of running
  unbounded, a multibyte payload (~300k emoji, ~1.14 MiB UTF-8 but only
  300k characters) is still rejected by the byte-based cap, and a normal
  under-the-cap payload still reaches the handler script unchanged.
- Verified both new failing-without-fix tests via test-the-test (stashed
  the src fix): the oversized-payload test failed because the dispatcher
  silently accepted the full payload and returned "not found" instead of
  exiting 1 with the limit message — reproducing the exact bug.
- Ran the full tests/integrations/test_events.py suite (124/128 pass; the
  remaining 4 are the pre-existing Windows symlink-elevation failures
  unrelated to this change).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJHJ2dHP2RVCNncHqN8Qm9

* fix(events): pin utf-8 encoding on the handler subprocess in both dispatch paths

Addresses Copilot review feedback on PR github#4337:

Both `_run_inline` (the generated dispatcher's stdlib fallback) and
`resolve_and_run_event_command` (the delegated/CLI-native path) decode
stdin explicitly as utf-8, then pass that string to the handler via
`subprocess.run(..., text=True)` with no explicit `encoding=`. Without
one, `text=True` re-encodes the payload for the child's stdin using
`locale.getpreferredencoding()` — on Windows that's commonly the ANSI
codepage, not UTF-8 — so a non-ASCII payload byte (e.g. "é") reaches
the handler as the wrong byte, corrupting JSON for handlers that
expect UTF-8. Pin `encoding="utf-8"` on both subprocess.run calls so
the decode and re-encode agree.

Also rewrote `test_dispatcher_underlimit_stdin_still_runs` (previously
skipped entirely on Windows via a POSIX-only `sh` handler) to use a
cross-platform Python handler and assert byte-for-byte fidelity of a
non-ASCII payload, and added
test_dispatcher_inline_fallback_preserves_non_ascii_payload, which
forces the `_run_inline` fallback (never reached in a dev environment
where specify_cli is importable, since the dispatcher always delegates
first) so that path's fix is independently verified too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhR6g8xT8at5pPMhkrC3e2

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Release v1.0.6

# Conflicts:
#	CHANGELOG.md
#	pyproject.toml
Aligns version, fork_version, and project_version fields with upstream
release v1.0.6 following the sync/upstream-v1.0.4 pattern (#116).

Assisted-by: Jane Alesi (GLM, autonomous)
…tream content changes

Upstream v1.0.5/v1.0.6 changed files under these bundled extensions
without version bumps (the CI guard landed mid-cycle in v1.0.6, after
those changes). The fork sync PR trips the new
check_extension_version_bump guard; bumping satisfies it and - per the
guard's own rationale - makes the changed content reach installed copies
via 'specify extension update'.

Assisted-by: Jane Alesi (GLM, autonomous)
- git extension manifest test asserted the literal 1.0.0
- bundler init/local-install fixtures pinned agent-context at 1.0.0;
  the install preflight compares bundle pins against catalog.json and
  now rejects the stale pin

Assisted-by: Jane Alesi (GLM, autonomous)
@satwareAG-ironMike
satwareAG-ironMike merged commit 144e5d8 into main-speck Sep 15, 2026
14 checks passed
@satwareAG-ironMike
satwareAG-ironMike deleted the sync/upstream-v1.0.6 branch September 15, 2026 04:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

upstream-sync: v1.0.6 available upstream-sync: v1.0.5 available