Skip to content

feat: update Bob integration to skills-based layout for Bob 2.0#3415

Open
davidebibm wants to merge 28 commits into
github:mainfrom
davidebibm:feat/update-bob-skills-integration
Open

feat: update Bob integration to skills-based layout for Bob 2.0#3415
davidebibm wants to merge 28 commits into
github:mainfrom
davidebibm:feat/update-bob-skills-integration

Conversation

@davidebibm

@davidebibm davidebibm commented Jul 8, 2026

Copy link
Copy Markdown

Bob 2.0 replaces the command-based workflow with a skills-based layout.

https://bob.ibm.com/blog/bob-v2-release-announcement

Description

Testing

  • Tested locally with uv run specify --help
  • Ran existing tests with uv sync && uv run pytest
  • Tested with a sample project (if applicable)

AI Disclosure

  • I did not use AI assistance for this contribution
  • I did use AI assistance (describe below)

Very simple code generated by Bob but verified by me

Bob 2.0 replaces the command-based workflow (.bob/commands/*.md) with
a skills-based layout (.bob/skills/speckit-<name>/SKILL.md), matching
the pattern used by Claude Code, Codex, and other skills-first agents.

- Switch BobIntegration from MarkdownIntegration to SkillsIntegration
- Update folder/dir from .bob/commands to .bob/skills
- Change extension from .md to /SKILL.md (skills layout)
- Add --skills option (default: True) consistent with Codex pattern
- Update tests to inherit from SkillsIntegrationTests (28 tests pass)
- Bump catalog entry to version 2.0.0 with updated description

Assisted-by: IBM Bob (model: claude-sonnet-4-5, autonomous)
@davidebibm davidebibm requested a review from mnriem as a code owner July 8, 2026 18:26
@davidebibm

Copy link
Copy Markdown
Author

@mnriem please review, we need to make this work with new Bob... Thankyou so much

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the built-in IBM Bob integration to align with Bob 2.0’s skills-based layout, switching installation output from command files to speckit-<name>/SKILL.md skills directories and bumping the integration’s catalog version accordingly.

Changes:

  • Migrate BobIntegration from MarkdownIntegration to SkillsIntegration and update output paths to .bob/skills/.../SKILL.md.
  • Update Bob integration tests to use the shared SkillsIntegrationTests mixin.
  • Bump the Bob entry in integrations/catalog.json to 2.0.0 with an updated description.
Show a summary per file
File Description
src/specify_cli/integrations/bob/__init__.py Switch Bob to SkillsIntegration and update registrar/config output to .bob/skills + /SKILL.md.
tests/integrations/test_integration_bob.py Update base test mixin and expected output directories for the skills layout.
integrations/catalog.json Bump Bob integration version/description to reflect the 2.0.0 skills-based update.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Low

Comment thread src/specify_cli/integrations/bob/__init__.py Outdated
Comment thread src/specify_cli/integrations/bob/__init__.py

@mnriem mnriem left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this fundamentally changes the layout for any Bob users what is the migration strategy? This will break them once they adopt a new version of Spec Kit so you need to make sure this goes through a deprecation cycle so they can migrate to the new Bob version. E.g make it an opt-in to the new version of Bob first and then in 2 minor releases (X.Y.Z where Y is minor) you can then remove the non-skill variant

@davidebibm

Copy link
Copy Markdown
Author

As this fundamentally changes the layout for any Bob users what is the migration strategy? This will break them once they adopt a new version of Spec Kit so you need to make sure this goes through a deprecation cycle so they can migrate to the new Bob version. E.g make it an opt-in to the new version of Bob first and then in 2 minor releases (X.Y.Z where Y is minor) you can then remove the non-skill variant

Hi @mnriem
this should not break for Bob 1 user cause it was already compatible with skill mode.
Still needed this?

@mnriem

mnriem commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Thanks @davidebibm — but I think we're talking about two different layers, and this still needs to change before it can land.

Your point is about Bob the tool — that Bob 1.x can already read the skills layout. My concern is about Spec Kit's generated output: this PR changes what specify writes for the bob integration from .bob/commands/*.md to .bob/skills/speckit-<name>/SKILL.md. Any project someone already initialized has .bob/commands/*.md on disk. When they upgrade Spec Kit and re-init or switch, that layout is orphaned/replaced. That's a breaking change in our contract regardless of whether the Bob app happens to read both directories.

A hard cutover isn't acceptable here — we shouldn't catch existing users off guard. This needs to go through a proper deprecation cycle:

  1. This release: make skills an opt-in and keep the existing .bob/commands markdown layout as the default. That's real dual-mode — setup() branching between the two layouts, the way Copilot does it — not the --skills flag as written now.
  2. Next cycle: flip the default to skills, with the legacy layout still available.
  3. Cycle after that: remove the legacy markdown mode.

As it stands the --skills flag is a no-op: BobIntegration is hardwired to SkillsIntegration and the MarkdownIntegration variant is deleted, so --skills=False still emits skills. There's no path that produces the old layout, so it doesn't actually provide the opt-in it implies.

Can you rework it along those lines — genuine dual-mode with skills opt-in first — so we phase this in without breaking anyone?

@davidebibm

Copy link
Copy Markdown
Author

Ok, thankyou @mnriem i'll do that

@davidebibm davidebibm requested a review from mnriem July 13, 2026 15:01
@davidebibm

Copy link
Copy Markdown
Author

@mnriem Done, hope this is what you are expecting

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 3
  • Review effort level: Medium

Comment thread src/specify_cli/integrations/bob/__init__.py
Comment thread src/specify_cli/integrations/bob/__init__.py Outdated
Comment thread src/specify_cli/integrations/bob/__init__.py Outdated
@mnriem

mnriem commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback. You will need to update the description to reflect the reality

davidebibm and others added 12 commits July 14, 2026 08:40
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* chore: bump version to 0.12.12

* chore: begin 0.12.13.dev0 development

---------

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

* fix(workflows): if-step validate accepts falsy non-list else

IfThenStep.validate() guarded the 'else' branch with
'if else_branch and not isinstance(else_branch, list)'. The leading
truthiness check short-circuits for falsy non-list values (False, 0,
'', {}), so a malformed else-branch passes validation and is then
silently skipped at runtime. The sibling 'then' branch is validated
strictly; 'else' now matches by switching to an 'is not None' guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(workflows): cover explicit else:None and missing-else separately

Per Copilot feedback: the parametrized valid-else test omitted the
'else' key when the value was None, so it covered only the missing-else
case, not an explicit 'else: None'. Set 'else' explicitly (including
None) in the parametrized test and add a dedicated missing-else test, so
both accepted shapes are pinned.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…github#3448)

* fix(workflows): don't crash on membership test against a non-iterable

the `in` / `not in` operators in _evaluate_simple_expression only guarded
`right is not None`, so `left in right` still raised a raw TypeError when the
right operand was any other non-iterable (int, bool, float). a condition like
`{{ inputs.tag in inputs.count }}` where count is a number crashed the whole
workflow run instead of evaluating.

nothing is contained in a non-iterable, so treat membership as False (`not in`
as True) via a new _safe_membership helper that swallows TypeError. this
generalizes the old None guard and mirrors _safe_compare, which already
catches TypeError for the ordering operators.

added a regression test; confirmed it fails on the pre-fix code (raw
TypeError) and that genuine list/substring membership still works.

* address review: float membership case + broaden _safe_membership docstring

- add a float right-operand assertion so the test matches its comment (was
  claiming float coverage while only exercising int/bool/None).
- reword the _safe_membership docstring to describe TypeError generally
  (non-iterable right is the common case, but also e.g. an unhashable left
  against a set) rather than implying only the right operand matters.
Readers were replacing vX.Y.Z with bare versions like 0.12.11,
which fails because git tags are named v0.12.11.

Assisted-by: Cursor Grok 4.5 (supervised)

Co-authored-by: Cursor <cursoragent@cursor.com>
…ithub#3328)

* feat(workflows): make shell step timeout configurable (github#3327)

The `shell` step hardcoded a 300s subprocess timeout, so any command
that legitimately runs longer than five minutes (a full build, a linter
aggregator, an integration-test target) was killed with TimeoutExpired
and failed the whole run, with no YAML knob to raise the limit.

Add an optional `timeout` field (seconds) that defaults to 300 for
backward compatibility and is threaded through to `subprocess.run`. The
timeout failure message now reports the configured value instead of a
hardcoded 300. `validate` rejects a `timeout` that is not a positive
number (bool is rejected explicitly, since it is an int subclass but a
config error rather than a duration).

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

* Potential fix for pull request finding

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

* test(workflows): cover non-finite timeout rejection in shell step

The isfinite guard added in 955d46a rejects YAML .inf/.nan timeouts, but no test asserted it. inf and nan are floats that pass a plain > 0 check (nan <= 0 is False), so without an explicit case a regression could silently reaccept them and crash subprocess.run(timeout=...) at runtime. Addresses the remaining Copilot review comment on PR github#3328.

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

* docs(workflows): document configurable shell step timeout

Address Copilot review feedback on github#3328: the per-step `timeout`
option was not reflected in the public workflow docs. The Shell Steps
section only showed `run:`, so readers couldn't discover `timeout:`,
its unit (seconds), or its default (300).

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

* Potential fix for pull request finding

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

* refactor(workflows): consolidate shell-step timeout validation into one path

Address Copilot review feedback on github#3328:

- Remove the dead "fall back to default" timeout block in execute(): it
  re-read `timeout` from config immediately after, so the fallback was
  discarded and its comment contradicted the new fail-on-invalid behavior.
- Extract a single `_timeout_error()` helper shared by execute() and
  validate() so both reject the same values with the same message, instead
  of two drifting copies of the check.
- Hoist the duplicated inline `import math` to module scope.
- Add test_execute_fails_cleanly_on_invalid_timeout: asserts execute()
  fails the step (rather than raising) on an unvalidated string/bool/inf/0
  timeout, covering the engine-skips-validate path Copilot flagged.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…d files (github#3418)

* fix(templates): point constitution sync checklist at installed command files

The consistency-propagation checklist told the agent to read
.specify/templates/commands/*.md, but specify init never creates that
directory — command templates are rendered straight into the
agent-specific directory (.github/prompts/, .claude/commands/, ...).
The checklist step could therefore never run against real files.

Point it at the installed speckit.* command files for the active agent
instead.

Fixes github#660

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

* fix(templates): cover hyphenated and skills-mode command filenames

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

* fix(templates): use actual integration output directories in examples

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

* docs(templates): cover skills-based command layouts in sync checklist

Copilot skills mode installs speckit-<name>/SKILL.md under .github/skills/,
not .github/agents/. Mention both directories and the SKILL.md layout.

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

* docs(templates): restore hyphenated speckit-* naming in sync checklist

The previous commit dropped the speckit-* flat-file variant used by
Cline and others while adding the SKILL.md layout. Name all three:
speckit.*, speckit-*, and speckit-<name>/SKILL.md.

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

* docs: clarify agent-specific reference phrasing in constitution template

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

---------

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

* fix: rewrite extension-relative subdir paths in generated command bodies

Extension command bodies reference bundled files relative to the
extension root (agents/, knowledge-base/, templates/, ...). Generated
SKILL.md and command files emitted those paths verbatim, so agents
resolved them against the workspace root where they do not exist.

Add CommandRegistrar.rewrite_extension_paths, which rewrites references
to subdirectories that actually exist in the installed extension to
.specify/extensions/<id>/..., and call it once in register_commands so
every output format and alias gets the fix. commands/, specs/ and
dot-directories are never rewritten.

Fixes github#2101

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

* fix: only rewrite relative extension path references

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

* fix: use callable re.sub replacement for extension subdir rewrite

subdir and extension_id come from filesystem directory names and were
interpolated into a re.sub string replacement template. A directory name
containing a backslash (e.g. assets\q) would raise re.error: bad escape,
aborting command registration even when the body didn't reference it.
Use a callable replacement so these values are treated literally.

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

* fix: make subdir rewrite regression test cross-platform

Renamed the test's subdir fixture from "assets\\q" to "assets[q]":
on Windows, backslash is a path separator, so mkdir would create
nested "assets/q" dirs instead of one literally-named directory,
and iterdir() would only discover "assets", never exercising the
rewrite. extension_id keeps a real backslash/"\\1" since it isn't
used to create a directory, still verifying the callable replacement
handles it literally. Added a sanity assertion for this assumption.

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

* fix: apply extension subdir path rewrite in skills-mode renderer

register_commands() rewrote extension-relative subdir references
(agents/, knowledge-base/, etc.) via rewrite_extension_paths(), but
_register_extension_skills() - the separate renderer used for active
non-native skills agents (e.g. Claude with ai_skills: true) - never
called it. Generated SKILL.md files left agents/... and
knowledge-base/... unresolved, and mapped the extension's own
templates/ through the generic project-level rewrite instead of its
installed .specify/extensions/<id>/templates/ location.

Reuse the existing rewrite_extension_paths() helper in
_register_extension_skills() at the same point register_commands()
applies it (before resolve_skill_placeholders' generic rewrite), and
add a skills-mode regression test.

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

* fix: apply extension subdir path rewrite on preset restore/reconcile paths

_unregister_skills() restored extension-backed SKILL.md content via
resolve_skill_placeholders() without first calling
rewrite_extension_paths(), so removing a preset override that shadowed
an extension command restored the bare, unresolvable agents/... and
knowledge-base/... references. Carried extension_id/extension_dir
through _build_extension_skill_restore_index() and applied the same
rewrite used at initial registration before restoring.

Found the identical gap in _reconcile_composed_commands()'s non-skill
agent path: when a removed preset's command reverts to an extension
winner, register_commands_for_non_skill_agents() was called without
extension_id, so the rewrite never ran for plain command-file agents
either. Passed extension_id through there too.

Added regression tests for both restore paths (skills-mode and
non-skill-agent command files) in tests/test_presets.py.

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

* fix: apply extension subdir path rewrite when composing over extension base

PresetResolver.resolve_content() read the effective base layer's raw
content directly via path.read_text() before composing append/prepend/
wrap overlays on top of it, and its outright-replace shortcut did the
same. When that base layer was extension-provided, neither read path
applied rewrite_extension_paths(), so composing a preset over an
extension command (or an extension winning outright through
resolve_content) left bare, unresolvable agents/... and
knowledge-base/... references in the composed output.

All three call sites (PresetManager._register_commands()'s composed
path, _reconcile_composed_commands()'s composed path, and skills-mode
reading the .composed file written by either) consume resolve_content's
return value, so fixing the read at its source covers command output,
skill output, and both initial-install and reconcile flows without
threading extension identity through each caller.

Tagged extension layers in collect_all_layers() with extension_id/
extension_dir, and added a _read_layer_content() helper in
resolve_content() that applies rewrite_extension_paths() whenever a
layer carries that extension identity — used at both raw-read sites
(outright-replace shortcut and composition base). Composing
(append/prepend/wrap) layers are never extension-provided (extensions
are always inserted with strategy "replace"), so no other read site
needs the rewrite.

Added regression tests: a parametrized resolve_content() test covering
append/prepend/wrap composing over an extension base, a skills-mode
test asserting the composed SKILL.md resolves the extension's subdir
references, and a non-skill-agent (Gemini) install-time test matching
the reported live repro.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assisted-by: Codex (model: GPT-5, autonomous)
* follow  fc3d124

agent-file-template.md is removed at  fc3d124

* Fix ruled line for constitution-template.md

* fix test
…hing (github#3481)

`SwitchStep.validate()` already rejects a non-mapping `cases`, but the
engine's `execute()` path does not auto-validate (see
`WorkflowEngine.load_workflow`, whose docstring notes the definition is
"not yet validated"). On an unvalidated run, `execute` called
`cases.items()` on the raw value, so a list or scalar `cases` authoring
mistake raised `AttributeError` and took down the whole run — the engine
invokes `step_impl.execute()` with no surrounding try/except.

Guard `execute` to return a FAILED StepResult naming the type error
instead, mirroring the fan-out step's non-list `items` handling. The
expression is still evaluated first, so its value is surfaced in the
step output for downstream context.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: bump version to 0.12.13

* chore: begin 0.12.14.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Noor-ul-ain001 and others added 13 commits July 14, 2026 08:48
…hing (github#3482)

`FanInStep.validate()` and the engine's fan-in checks both reject a
non-list `wait_for`, but the engine's `execute()` path does not
auto-validate (see `WorkflowEngine.load_workflow`, whose docstring notes
the definition is "not yet validated"). On an unvalidated run, `execute`
iterated the raw value with `for step_id in wait_for`, with two bad
outcomes:

  * a scalar (`wait_for: 5`, `wait_for: null`) raised `TypeError` and
    took down the whole run — the engine invokes `step_impl.execute()`
    with no surrounding try/except; and
  * a string (`wait_for: stepA`) silently iterated its characters and
    returned a join of empty results with a COMPLETED status — the exact
    "silent empty result + COMPLETED" wiring bug the engine's own fan-in
    validation comment warns against.

Guard `execute` to return a FAILED StepResult naming the type error
instead, mirroring the fan-out step's non-list `items` handling. A
missing `wait_for` key still defaults to an empty list (COMPLETED),
unchanged; the guard fires only on an explicit non-list value.

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

kiro-cli confines all of its managed files to an isolated agent root
(`.kiro/`, with commands in `.kiro/prompts`) that no other integration
writes to, so it meets every documented criterion for multi-install
safety — but `KiroCliIntegration` never set `multi_install_safe = True`.

As a result, co-installing kiro-cli alongside any other integration left
`specify integration status` permanently in ERROR:

    error unsafe-multi-install: Installed integrations are not all
    declared multi-install safe: kiro-cli

`--force` bypasses the install-time gate but does not clear the status
error, and there is no flag or config to acknowledge it, so the error is
permanent while both integrations remain installed.

Set `multi_install_safe = True`. The registry's parametrized
multi-install-safe contract tests (static isolated root, distinct agent
roots / command dirs, disjoint manifests) now cover kiro-cli
automatically, and a focused regression test pins the declaration so a
future edit cannot silently drop it and reintroduce the error.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…options (github#3457) (github#3466)

* fix(integrations): exit cleanly on unbalanced quote in --integration-options (github#3457)

`_parse_integration_options` called `shlex.split(raw_options)` unguarded, so an
unbalanced quote in the flag value (e.g. `--integration-options='--commands-dir
"foo'`) made shlex raise `ValueError: No closing quotation` and a raw traceback
escaped — unlike every other bad-input path in this function (unknown option,
missing value, unexpected value), which print a message and exit 1.

Reachable from `specify init --integration-options=...` and every `specify
integration install/switch/upgrade/migrate --integration-options=...`.

Wrap the split in a try/except ValueError that prints a one-line error and
raises `typer.Exit(1)`, matching the existing loud-fail UX. Add a test asserting
the unbalanced-quote input raises `typer.Exit` with exit code 1.

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

* Potential fix for pull request finding

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…ity catalog (github#3431)

* Add Quality Gates (Enforcement Layer) extension to community catalog

Add gates extension submitted by @schwichtgit to:
- extensions/catalog.community.json (alphabetical order, between fx-to-dotnet and github-issues)
- docs/community/extensions.md community extensions table

Closes github#3414

Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: revert unrelated catalog reformatting and remove empty changelog field from gates entry

- Restore original ordering/formatting of aide, checkpoint, critique,
  threatmodel entries and inline requires.tools objects that were
  inadvertently reordered in the previous commit
- Remove `"changelog": ""` from the gates entry (empty URL is
  inconsistent with catalog conventions; field should be omitted when
  no changelog URL exists)

Addresses review comments:
- github#3431 (comment) — unrelated reformatting/reordering
- github#3431 (comment) — empty changelog field

Assisted-by: GitHub Copilot (model: claude-sonnet-4.5, autonomous)

* Fix gates entry tool requirements: git required, add node and shellcheck optional

- Mark git as required (per v0.1.0 README: \"jq and git — the hooks and verify.sh require them\" and release notes: \"Requires Spec Kit >=0.12.0, jq, and git\")
- Add node as optional tool (per issue github#3414 submission)
- Add shellcheck as optional tool (per issue github#3414 submission)
- Update gates entry updated_at and top-level updated_at to 2026-07-13

Assisted-by: GitHub Copilot (model: claude-sonnet-4.5, 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>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
github#3236)

* fix(init): don't block on confirmation for 'init --here' without a TTY

When 'specify init --here' targets a non-empty directory without --force, it called typer.confirm() unconditionally. In a non-interactive session (no TTY -- CI, piped, agent) there is no input, so the prompt reads EOF and aborts unhelpfully (or blocks), with no actionable message. The named-project path already fails fast and points to --force; --here was the inconsistent outlier.

Guard the confirmation with the existing _stdin_is_interactive() helper: when non-interactive, print a clear 'directory not empty; re-run with --force' error and exit 1 instead of prompting.

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

* fix(init): honor piped confirmation for 'init --here'; only fail-fast on empty stdin

The first version of this fix short-circuited on '_stdin_is_interactive()' (isatty) before typer.confirm, which broke 'init --here' when confirmation is piped (e.g. 'echo y | specify init --here' / CliRunner input='y\n') -- a non-TTY pipe with valid input was wrongly rejected, regressing test_init_here_without_force_preserves_shared_infra. Instead, call typer.confirm normally (piped 'y'/'n' is honored) and catch the Abort/EOFError it raises only when stdin is empty, converting that to the actionable '--force' guidance. This keeps the UX win for the no-input case without rejecting piped input.

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

* fix(init): distinguish interactive cancel from no-input; defer merge warning

Address Copilot review on the --here non-empty path: (1) treat typer.Abort during an interactive confirm (e.g. Ctrl+C) as a normal cancellation (exit 0), and only emit the '--force' guidance + exit 1 when there is no TTY (empty stdin / EOF) -- no longer conflating the two; (2) move the 'will be merged / may overwrite' warning so it only shows when actually proceeding (force) or folded into the confirmation prompt, not on the fail-fast path where nothing is merged.

Piped confirmation (e.g. 'echo y | specify init --here') is still honored, which is why the prompt is attempted rather than refused outright when non-interactive -- the existing test_init_here_without_force_preserves_shared_infra pipes 'y' and must succeed.

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

* fix(init): fail fast on non-interactive --here instead of prompting

Per Copilot review: do not call typer.confirm when stdin is not a TTY -- an open-but-idle non-TTY stdin (CI/agent) could block on the prompt. When the directory is non-empty and --force is not given, fail fast with '--force' guidance unless an interactive terminal is present. Interactive confirm still offers the merge-but-preserve path (distinct from --force, which overwrites); a Ctrl+C there is treated as a normal cancellation (exit 0). The merge/overwrite warning is only printed when actually proceeding, not on the fail-fast path.

Updated the preserve-merge E2E test to simulate an interactive terminal so it exercises the confirm path (non-interactive sessions now require --force).

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

* fix(init): honor piped y/n for 'init --here', error only on no-input

Per maintainer review: restore the second-revision shape. Calling
typer.confirm normally keeps 'echo y | specify init --here' reaching the
non-destructive preserve-merge path (and piped 'n' cancels with exit 0).
Only when no confirmation input is available at all (closed/empty stdin
-> typer.Abort/EOFError) is it converted into the actionable error that
points at --force. This drops the _stdin_is_interactive fail-fast that
broke the common piped-confirm idiom and made preserve-merge
interactive-only. The preserve test no longer needs to monkeypatch
_stdin_is_interactive - it passes on the real contract.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(init): preserve interactive-cancel semantics; fold merge risk into the prompt

Two review-driven refinements to the 'init --here' non-empty confirm, keeping
the maintainer-endorsed control flow (piped y/n honored; non-interactive EOF →
actionable --force error):

1. typer.confirm raises typer.Abort for BOTH an interactive Ctrl+C and an EOF on
   closed/empty stdin. Catching it unconditionally reported 'no confirmation
   input available, use --force' and exited 1 even when the user cancelled at a
   real TTY. Branch on _stdin_is_interactive(): a TTY cancel is a normal exit 0
   ('Operation cancelled'); only non-interactive EOF becomes the --force error.

2. Fold the merge-risk warning into the confirmation question instead of printing
   it unconditionally beforehand, so the EOF/no-input path (which exits without
   changing anything) no longer prints a misleading 'will be merged' line first.

Adds test_init_here_interactive_cancel_exits_zero (fails before: exit 1 with
--force; passes after: exit 0, 'cancelled', pre-existing file untouched). The
non-interactive EOF and piped-y preserve-merge tests are unchanged and still pass.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…presets (github#3351)

* fix(presets): resolve() honors manifest-declared file: for installed presets

PresetResolver.resolve()'s tier-2 (installed presets) loop was
convention-only: it looked for templates/<name>.md and <name>.md,
ignoring a preset manifest that declares the template with an explicit,
non-convention file: path. So resolve() returned the core template (and
resolve_with_source() misattributed source='core') while
collect_all_layers()/resolve_content() correctly used the preset's
declared file — a divergence inside the same class. It could also return
a stray convention-path file the manifest deliberately points away from.
Mirror collect_all_layers()'s manifest-first logic: use the declared
file: when present (skip convention fallback if it's missing, to avoid
masking typos), and fall back to the convention walk only when the
manifest is absent or doesn't list the template.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(presets): clarify the empty/falsey manifest-file branch comment

Per review: 'file' is a required key for every template entry
(PresetManifest._validate()), so the manifest-found branch is reached
for an empty/falsey/non-usable 'file' value, not a truly absent one.
Reword the comment to say so. Comment-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(presets): resolve() returns only real files; test missing-file skip

Per review:
- Use is_file() (not exists()) when honoring a manifest-declared file: so a
  manifest pointing at a directory is treated as missing rather than
  returned to a caller that will read_text() it. Applied in both resolve()
  and collect_all_layers() so the two stay consistent.
- Add a regression test for the skip-convention-fallback-when-declared-file-
  missing behavior: manifest declares a missing custom/spec.md while the pack
  has a convention templates/spec-template.md; resolve() must skip the pack
  and fall through to core, not pick up the stray convention file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(presets): resolve()/collect_all_layers() require a regular file for manifest file:

A manifest-declared file: path is honored via exists(), which also accepts
a directory. If a preset points file: at a directory, resolve() returned it
and downstream read_text() crashes. Use is_file() in both resolve() and
collect_all_layers() so a non-file (directory) is treated as missing and the
convention fallback is skipped (pack yields to core), matching the existing
missing-file behavior.

Adds a directory-at-file: test (fails on exists(), passes on is_file()) that
also asserts collect_all_layers() never returns the directory as a layer.

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

* refactor(presets): extract shared _manifest_declared_template for resolve()/collect_all_layers()

Both methods reimplemented the manifest-entry lookup + authoritative-fallback
rules independently — the exact duplication that let them diverge and caused the
bug this PR fixes. Extract a single _manifest_declared_template(pack_dir, name,
type) -> (entry, candidate) helper (candidate is the declared file only when it
is_file(); a declared-but-unusable file returns (entry, None) so callers skip the
convention fallback). resolve() and collect_all_layers() now both call it, so
their manifest-first resolution cannot silently diverge again.

Pure refactor, behavior-preserving: full test_presets.py (331) still passes,
including the directory-at-file:, missing-file, and manifest-file-wins cases.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…hub#3262)

* fix(workflows): validate command step input/options are mappings

CommandStep.validate() only checked for 'command'; execute() then does input.items() and options.update(step_options). A non-mapping input:/options: (e.g. a YAML list or scalar) raised AttributeError at run time, bypassing the per-step FAILED/continue-on-error contract -- unlike the sibling steps (switch 'cases', fan-out 'step') which type-check their config fields in validate(). Add the same checks, plus a defense-in-depth coercion in execute() since the engine does not auto-validate before running a step.

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

* docs: fix code-comment typo in CommandStep.validate

The explanatory comment said options.update(options) but execute() does
options.update(step_options). Comment-only change; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(workflows): command step FAILS on malformed input/options instead of coercing

execute() previously coerced a non-mapping 'input' to {} and silently ignored a
non-mapping 'options', then dispatched the command anyway. For a workflow that
skipped validation (the engine does not auto-validate before execute()), that
let an explicitly malformed step run with empty args and report COMPLETED —
masking the config error and defeating the per-step FAILED / continue_on_error
semantics this change is meant to provide.

Both now return a FAILED StepResult with the same contract error validate()
reports (never crashing on .items()/.update()). Valid mapping configs are
unaffected. Strengthened the execute() test to assert FAILED + the exact
'must be a mapping' error for input and options (fails before: the result
carried the downstream dispatch error, not the shape error).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add autonomous-run-governance preset submitted by @hindermath to:
- presets/catalog.community.json (alphabetical order)
- docs/community/presets.md community presets table

Closes github#3499


Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, 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>
Add test-first-governance preset submitted by @mnriem to:
- presets/catalog.community.json (alphabetical order)
- docs/community/presets.md community presets table

Closes github#3502

Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, 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>
…b#3455)

* Add Spec Kit Memory extension to community catalog

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

Closes github#3446

Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: resolve merge conflicts with main branch

- extensions/catalog.community.json: keep updated_at 2026-07-10 (more recent)
- docs/community/extensions.md: include both Spec Kit Figma (main) and
  Spec Kit Memory (this PR) in alphabetical order

Assisted-by: GitHub Copilot (model: claude-sonnet-4.5, autonomous)

* fix: add memsearch optional tool dependency to memory extension catalog entry

Assisted-by: GitHub Copilot (model: claude-sonnet-4.5, 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>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
* chore: bump version to 0.12.14

* chore: begin 0.12.15.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@davidebibm

Copy link
Copy Markdown
Author

@mnriem Addressed Copilot comments and aligned with main

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 38/38 changed files
  • Comments generated: 4
  • Review effort level: Medium

Comment on lines +117 to +120
if parsed_options.get("legacy_commands"):
_warn_legacy_commands_deprecated()
return self._setup_legacy(project_root, manifest, parsed_options, **opts)
return SkillsIntegration.setup(self, project_root, manifest, parsed_options, **opts)
Comment on lines +67 to +70
Inheriting ``SkillsIntegration`` ensures ``invoke_separator = "-"`` is
set at the class level so ``CommandRegistrar.AGENT_CONFIGS`` (which reads
the class attribute directly) generates correct hyphenated
``/speckit-<name>`` references for skills.
Comment on lines 14 to 17
class KiroCliIntegration(MarkdownIntegration):
key = "kiro-cli"
multi_install_safe = True
config = {
Comment thread CHANGELOG.md

<!-- insert new changelog below this comment -->

## [0.12.14] - 2026-07-13
@mnriem

mnriem commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback and resolve conflicts

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.

10 participants