Compare commits

..

11 Commits

Author SHA1 Message Date
github-actions[bot]
92fb6e8eda chore: bump version to 0.11.8 2026-06-24 22:38:40 +00:00
Rafael Sales
05cf078ea4 docs: add SpecKit Assistant npm package to Community Friends (#3142)
* docs: add SpecKit Assistant npm package to Community Friends

Adds SpecKit Assistant (https://www.npmjs.com/package/speckit-assistant)
to the Community Friends list. It is a visual interface for the specify
CLI that orchestrates Spec-Driven Development (SDD) — connecting local
specification, planning, and task checklists with AI agents (Claude,
Gemini, Copilot). No installation required; run it via npx speckit-assistant.

As the author of both the VS Code Spec Kit Assistant extension and the
SpecKit Assistant npm package, I maintain these community tools that
provide a visual interface on top of the specify CLI.

* Potential fix for pull request finding

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

* docs: clarify SpecKit Assistant requires no global installation

Address Copilot review: 'No installation required' was misleading for an
npx-run package since npx still downloads it. Clarify that no global
installation is required.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-24 17:37:28 -05:00
Manfred Riem
96039d36d2 Require preset-usage README with Spec Kit CLI syntax in preset submissions (#3104)
* Require preset-usage README with Spec Kit CLI syntax in submissions

Tighten the community preset submission workflow so it validates the
README referenced by the documentation field rather than merely checking
for a root README. The workflow now fails submissions whose linked README
lacks a valid 'specify preset add ...' command and flags monorepo
submissions that point documentation at a generic root README.

- Add a required Documentation URL field to the preset issue template
- Add validation step 2d (documentation README + CLI-syntax check) to
  .github/workflows/add-community-preset.md and recompile the lock file
- Document the stricter usage-README requirement and reviewer content
  check in presets/PUBLISHING.md

Closes #3103

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

* Align preset README docs with workflow's actual enforcement

Address PR review feedback on #3104:
- PUBLISHING.md: clarify that only README resolution + a valid
  'specify preset add ...' command are mechanically enforced; the
  preset-scoped-README and minimum-structure items are reviewer
  expectations, not automated checks.
- PUBLISHING.md: state that a missing 'specify preset add ...' command
  is a hard validation failure (check 2d), not just 'flagged for changes'.
- preset_submission.yml: require 'specify preset add ...' (not the looser
  'specify preset ...') to match the workflow validation.

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

* Tighten preset README validation and docs per PR review

Address PR review feedback on #3104:
- Workflow Step 2c: drop the generic repo-root README.md check so the
  README requirement is enforced exactly once, in Step 2d, against the
  file the documentation field points to (avoids monorepo false-positive).
- Workflow Step 2d: restrict the documentation URL to GitHub-hosted
  README URLs (github.com/.../blob/... or raw.githubusercontent.com/...)
  before fetching user-provided input.
- PUBLISHING.md: add the required 'id' field to the example catalog entry.
- preset_submission.yml: fix the Documentation URL placeholder to match
  the recommended monorepo presets/<id>/README.md pattern.
- Recompile add-community-preset.lock.yml (body hash only).

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

* Refine preset README validation rules per PR review

Address PR review feedback on #3104:
- Workflow Step 2d: broaden the documentation URL allowlist to also
  accept github.com/.../raw/... URLs; strip any fragment/query before
  fetching so the target is deterministic; clarify that a
  'specify preset add --from <url>' command only counts when its URL
  matches the submitted Download URL (a different --from URL does not
  satisfy the requirement, though other accepted forms still can).
- PUBLISHING.md: show both accepted download URL shapes (tag archive and
  release asset) in the README install example instead of implying only
  the releases/download form.
- preset_submission.yml: remove the ambiguous generic 'README.md with
  description and usage instructions' checkbox; the linked-README
  requirement is the single source of truth.
- Recompile add-community-preset.lock.yml (body hash only).

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

* Clarify install-command requirement wording per PR review

Address PR review feedback on #3104: the previous 'matching the download
URL' wording overstated the requirement. Only the 'specify preset add
--from <url>' form needs an exact download-URL match; other accepted
forms ('specify preset add <id>' / '--dev <path>') don't reference the
download URL at all.

- preset_submission.yml: reword the Documentation URL description and the
  Submission Requirements checkbox to reflect what's enforced vs preferred.
- PUBLISHING.md: clarify the reviewer note so the exact-match rule is
  scoped to the --from form.

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

* Require README.md target and fix release-ZIP wording per PR review

Address PR review feedback on #3104:
- Workflow Step 2d: add an explicit check that the documentation URL path
  ends with README.md (case-insensitive) after stripping fragment/query,
  so a non-README markdown file is rejected before fetching.
- PUBLISHING.md: reword the release-ZIP note, which conflicted with the
  earlier preset structure guidance. The real requirement is that the
  README is reachable at the documentation URL before download; it's fine
  for the same file to also ship inside the release ZIP.
- Recompile add-community-preset.lock.yml (body hash only).

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

* Use stable unnumbered anchor for Usage README Requirements

Address PR review feedback on #3104: drop the '6.' prefix from the
'Usage README Requirements' heading so its GitHub anchor isn't tied to a
section number (brittle under renumbering, and avoids confusion with the
top-level 'Best Practices' TOC item). Update the Prerequisites cross-link
to the new #usage-readme-requirements anchor.

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

* Align README requirement wording with enforced checks per PR review

Address PR review feedback on #3104:
- PUBLISHING.md: the 'mechanically enforces' summary now lists all Step 2d
  checks (GitHub-hosted URL, path ends with README.md, resolves, contains
  a valid 'specify preset add ...' command), instead of only two.
- PUBLISHING.md: reword the PR checklist item so a usage README + install
  command is the requirement, with preset-scoped README recommended for
  monorepos (matches the workflow's flag-not-fail behavior).
- preset_submission.yml: include the full 'specify preset add' prefix on
  the --dev and --from forms in the field description and checklist so
  submitters copy the exact syntax.

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

* Fix grammar in Usage README Requirements intro

Address PR review feedback on #3104: remove the incorrect colon after
'the linked README' so the sentence reads naturally.

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

* Avoid lossy raw URL rewrite for slash-containing refs per PR review

Address PR review feedback on #3104: rewriting documentation URLs into the
raw.githubusercontent.com/<owner>/<repo>/<ref>/<path> form can't reliably
represent refs that contain slashes (e.g. a feature/foo branch). Step 2d
now fetches github.com blob URLs by swapping only /blob/ -> /raw/, and
fetches github.com/.../raw/... and raw.githubusercontent.com/... URLs
as-is, instead of reconstructing the raw host form.

Recompile add-community-preset.lock.yml (body hash only).

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 17:06:51 -05:00
github-actions[bot]
d6cddd4127 [extension] Update Jira Integration (Sync Engine) extension to v0.4.0 (#3152)
* Update Jira Integration (Sync Engine) extension to v0.4.0

Update jira-sync extension submitted by @ashbrener:
- extensions/catalog.community.json (version, download_url, changelog, provides.commands, tags, requires.tools, updated_at)
- docs/community/extensions.md community extensions table (no change needed, row already current)

Closes #3149

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

* Fix review feedback: revert unrelated formatting, add bash version constraint, fix field ordering for jira-sync

- Revert unrelated em-dash/arrow encoding and tools array reformatting changes
  across the catalog (only jira-sync changes remain)
- Add version: \">=4.4\" to bash in jira-sync requires.tools
- Move category and effect fields to after license and before requires
  to match field ordering of neighboring entries

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>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-24 15:44:29 -05:00
github-actions[bot]
0cde6be41b Add Spec Roadmap extension to community catalog (#3153)
Add roadmap extension submitted by @srobroek to:
- extensions/catalog.community.json (alphabetical order)
- docs/community/extensions.md community extensions table

Closes #3150

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>
2026-06-24 15:32:04 -05:00
meymchen
dc840f07d0 feat(integration): update Kimi integration for Kimi Code CLI (#2979)
* feat(integration): update Kimi integration for Kimi Code CLI

Update the Kimi integration to target the new Kimi Code CLI
(MoonshotAI/kimi-code) layout:

- Change skills directory from .kimi/skills/ to .kimi-code/skills/
- Change context file from KIMI.md to AGENTS.md
- Extend --migrate-legacy to move old .kimi/skills/ installs and
  migrate KIMI.md user content to AGENTS.md
- Clean up leftover legacy .kimi/skills/ directories on teardown
- Update devcontainer installer to @moonshot-ai/kimi-code
- Update docs and tests

Relates to #1532

* fix(integration): align Kimi dispatch and harden legacy migration

- Override build_command_invocation to emit /skill:speckit-<stem>
  so dispatched commands match Kimi Code CLI's native slash syntax.
- Skip symlinked .kimi/skills directories during legacy migration
  and teardown to avoid operating on files outside the project.
- Remove kimi from the multi-install-safe integrations table.
- Add tests for command invocation and symlink safety.

* fix(integration): resolve custom context markers in Kimi legacy migration

Use IntegrationBase._resolve_context_markers() when migrating legacy
KIMI.md content so that projects with customized context_markers in
.specify/extensions/agent-context/agent-context-config.yml have the
managed section stripped with the correct markers instead of the
hard-coded defaults.

Adds a test verifying custom markers are respected during
--migrate-legacy.

* fix(integration): harden Kimi legacy migration against symlinked paths

* fix(kimi): guard symlinked SKILL.md during migration and teardown

* docs(kimi): mention KIMI.md→AGENTS.md migration in --migrate-legacy help

The --migrate-legacy help text listed only the skills directory move and
dotted→hyphenated renaming, but the flag also migrates KIMI.md user content
into AGENTS.md. Align the help with the actual behavior, docs, and tests.

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

* fix(kimi): validate legacy migration destination; clarify docstrings

Address Copilot review feedback on PR #2979:

- setup(): gate skills migration on _is_safe_legacy_dir(new_skills_dir)
  as well as the source. base setup() already rejects a destination that
  escapes the project root, but an in-tree symlinked .kimi-code/skills
  (e.g. -> .) could still misdirect the move; this gives the destination
  the same symlink-component protection as the source.
- _migrate_legacy_kimi_dotted_skills: rewrite docstring as a compatibility
  shim describing same-path delegation to _migrate_legacy_kimi_skills_dir.
- test_presets: clarify that the dotted-skill test exercises legacy naming
  under the current .kimi-code/ base, not the legacy .kimi/ location.

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

* fix(kimi): harden legacy KIMI.md→AGENTS.md context migration

- Skip context-file migration when the agent-context extension is
  disabled, matching upsert/remove_context_section opt-out behavior so
  an opted-out project's KIMI.md/AGENTS.md are left untouched.
- Safely skip (instead of raising) on filesystem edge cases: unreadable
  or non-UTF-8 KIMI.md, and AGENTS.md existing as a non-file/unwritable.
- Refuse to migrate a corrupted managed section (single marker, or end
  before start) so a partial managed block is never copied into
  AGENTS.md; KIMI.md is preserved for manual repair.

Add regression tests for all three cases.

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

* Potential fix for pull request finding

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

* Fix for pull request finding

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

* Approve fix for pull request finding

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

* chore(kimi): revert CHANGELOG.md edit (auto-generated)

The CHANGELOG is generated from merged PR titles, so a hand-written entry
is redundant; it was also placed under the already-released 0.10.2 section,
which would make those release notes historically inaccurate. Revert to
match main per maintainer feedback.

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

* test(kimi): skip symlink-safety tests when symlinks are unavailable

The Kimi legacy-migration safety tests create symlinks to assert that
migration/teardown never follow them out of the project. Symlink creation
fails on Windows without the create-symlink privilege and in some restricted
CI sandboxes, so these tests errored during setup instead of skipping.

Wrap every symlink_to() call in a shared _symlink_or_skip() helper that
pytest.skip()s on OSError/NotImplementedError, matching the guard pattern
already used by one of these tests. Verified on Windows: the 6 symlink tests
now skip cleanly (51 passed, 6 skipped) instead of erroring.

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

* fix(kimi): reject symlinked skills destination before install

Add a destination symlink pre-check in KimiIntegration.setup() before
super().setup() writes any SKILL.md. The base class only rejects a
destination that escapes project_root after resolve(), so an in-tree
symlinked .kimi-code/.kimi-code/skills (e.g. `-> .`) would still
misdirect writes into an unintended in-tree location (./skills/).

Extract the symlink-component walk into a shared _has_symlinked_component()
helper and reuse it from _is_safe_legacy_dir(). Add a regression test.

Also clarify that --migrate-legacy only migrates KIMI.md -> AGENTS.md when
the agent-context extension is enabled, in the CLI help text and the
integration docs.

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

* Refactor formatting and simplify logic in Kimi integration

* fix(kimi): reject symlinked target dir during legacy skills migration

When the migration destination already exists, guard against a symlinked
(or non-directory) target_dir before comparing SKILL.md bytes, so the
comparison never follows a link outside the project root. Also skip a
missing/non-file target SKILL.md explicitly.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-24 15:22:08 -05:00
github-actions[bot]
e12beda5f9 [extension] Add Golden Demo extension to community catalog (#3151)
* Add Golden Demo extension to community catalog

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

Closes #3127

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

* Remove empty changelog field from golden-demo catalog entry

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>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-24 15:17:04 -05:00
Ali jawwad
5404f7ee1c docs: run /speckit.checklist after /speckit.plan in quickstart (#3108)
* docs: run /speckit.checklist after /speckit.plan in quickstart

The quickstart workflow showed /speckit.checklist before /speckit.plan,
contradicting the CLI next-steps text (commands/init.py), which lists the
checklist as running after the plan. Per the maintainer on #2816 — "the
docs were actually wrong here ... checklists are meant for after plan" —
align the docs to the CLI: move /speckit.checklist after /speckit.plan in
the workflow diagram, the prose, and both walkthrough step sequences.

Docs-only; no behavior change.

Closes #2606

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

* docs: reword checklist as generating quality checklists, not validating directly

Address review: /speckit.checklist generates quality checklists (which then validate the requirements) rather than validating directly, matching the CLI/README phrasing. Preserves the after-plan ordering.

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

* docs: align checklist wording with CLI next-steps phrasing

Address review: state the checklist's purpose (validate requirements completeness, clarity, and consistency) and anchor it to /speckit.plan as the CLI does, use the plural 'quality checklists', and reword the Taskify step so the spec is validated using the generated checklists.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:16:36 -05:00
Ali jawwad
fdaaf18371 fix(workflows): preserve commas inside quoted list-literal elements (#3134)
* fix(workflows): preserve commas inside quoted list-literal elements

The simple-expression evaluator parsed a list literal with a naive
`inner.split(",")`, which splits on commas inside quoted strings (and
nested brackets). So `{{ ["a, b", "c"] }}` evaluated to three items
(`["a", "b", "c"]`) instead of two, silently corrupting `fan-out` `items:`
and any list expression that contains a comma inside a quoted element.

Split list-literal elements on top-level commas only, ignoring commas
inside quotes or nested brackets, via a small `_split_top_level_commas`
helper. Plain and empty lists are unchanged.

Add tests covering quoted commas, nested lists, and the existing
plain/empty cases.

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

* test(workflows): cover single-quoted and nested list literals

Address review: extend the list-literal regression test to assert single-quoted elements with commas and nested lists parse correctly, alongside the existing double-quoted cases.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 15:10:02 -05:00
Pascal THUET
e5df517ddc ci: pin actions to commit SHAs and add shellcheck (#3126)
* ci: pin actions to commit SHAs and add shellcheck

Pin actions/github-script in catalog-assign.yml to a full commit SHA; all
other workflows were already pinned. Add a repo-wide regression test that
every workflow `uses:` ref is pinned to a 40-char commit SHA.

Add a shellcheck job to lint.yml (--severity=error over scripts/bash/*.sh)
and document the local command in CONTRIBUTING.md.

* ci: use repo-standard actions/checkout v7.0.0 in shellcheck job

* ci: shellcheck all tracked shell scripts

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

* ci: address workflow hygiene review feedback

Assisted-by: Codex (model: GPT-5, autonomous)
2026-06-24 15:08:16 -05:00
Manfred Riem
b577e6c137 chore: release 0.11.7, begin 0.11.8.dev0 development (#3154)
* chore: bump version to 0.11.7

* chore: begin 0.11.8.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-24 15:04:32 -05:00
24 changed files with 1244 additions and 104 deletions

View File

@@ -88,9 +88,9 @@ fi
run_command "$kiro_binary --help > /dev/null"
echo "✅ Done"
echo -e "\n🤖 Installing Kimi CLI..."
echo -e "\n🤖 Installing Kimi Code CLI..."
# https://code.kimi.com
run_command "pipx install kimi-cli"
run_command "npm install -g @moonshot-ai/kimi-code@latest"
echo "✅ Done"
echo -e "\n🤖 Installing CodeBuddy CLI..."

View File

@@ -77,6 +77,18 @@ body:
validations:
required: true
- type: input
id: documentation
attributes:
label: Documentation URL
description: |
Link to the README that explains how to use **this preset** (not a general product/framework pitch).
Prefer the preset-scoped README (e.g. `presets/<id>/README.md` in a monorepo) over the repository root README.
It must contain at least one valid `specify preset add ...` install command — ideally `specify preset add --from <download-url>` using the exact Download URL above (other forms such as `specify preset add <preset-id>` or `specify preset add --dev <path>` are also accepted).
placeholder: "https://github.com/your-org/spec-kit-presets/blob/main/presets/your-preset/README.md"
validations:
required: true
- type: input
id: license
attributes:
@@ -175,7 +187,7 @@ body:
options:
- label: Valid `preset.yml` manifest included
required: true
- label: README.md with description and usage instructions
- label: Linked README (Documentation URL) explains how to use this preset and includes a valid `specify preset add ...` command (preferably `specify preset add --from <download-url>` using the exact download URL)
required: true
- label: LICENSE file included
required: true

View File

@@ -1,4 +1,4 @@
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b4ba1db5fdec754fa825cc3160879924118bc454a781eed70ef6c90beab83a95","body_hash":"392ace500b7cb9b0aa6b020d150841de398bcbcfe54dbad729f0d860d698bde2","compiler_version":"v0.79.8","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}}
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b4ba1db5fdec754fa825cc3160879924118bc454a781eed70ef6c90beab83a95","body_hash":"cb6c19088fa13da0a8320c174e8c14c4887d2c8a005a5cb2d2d2faa3f890de39","compiler_version":"v0.79.8","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}}
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c0338fef4749d08c21f8f975fb0e37efa17dda47","version":"v0.79.8"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]}
# This file was automatically generated by gh-aw (v0.79.8). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
#

View File

@@ -73,6 +73,7 @@ fields):
| Author | `author` | Yes |
| Repository URL | `repository` | Yes |
| Download URL | `download-url` | Yes |
| Documentation URL | `documentation` | Yes |
| License | `license` | Yes |
| Required Spec Kit Version | `speckit-version` | Yes |
| Required Extensions | `required-extensions` | No |
@@ -100,17 +101,70 @@ deciding pass/fail:
### 2c. Repository validation
- Fetch the repository URL — confirm it exists and is publicly accessible
- Confirm the repository contains a `preset.yml` file
- Confirm the repository contains a `README.md` file
- Confirm the repository contains a `LICENSE` file
### 2d. Release and download URL validation
> The README requirement is enforced once, in **Step 2d**, against the specific file the
> `documentation` field points to — not a generic repository-root `README.md`. This avoids
> the monorepo false-positive where a root README exists but isn't the preset-usage doc.
### 2d. Documentation README validation
The `documentation` field must point to the README that explains **how to use this
preset** — not just any file named `README.md`, and not a product/framework pitch.
- **Restrict the URL to GitHub before fetching.** The `documentation` value is
user-provided input. Only accept GitHub-hosted README URLs:
- `https://github.com/<owner>/<repo>/blob/<ref>/<path>`
- `https://github.com/<owner>/<repo>/raw/<ref>/<path>`
- `https://raw.githubusercontent.com/<owner>/<repo>/<ref>/<path>`
If the URL points anywhere else (or isn't a URL), **fail this check** and do not fetch it.
- **Require the URL to point at a README file.** After stripping any fragment/query (see
below), the URL path must end with `README.md` (case-insensitive). If it points at some
other Markdown file, **fail this check** and ask the submitter to link the preset's README.
- Fetch the **exact URL** in the `documentation` field. First strip any fragment (`#...`)
or query string (`?...`) — these are common when copying from the browser UI and must be
ignored so the fetch target is deterministic. Then resolve the raw content to fetch:
- For a `github.com/<owner>/<repo>/blob/<ref>/<path>` URL, fetch the equivalent
`github.com/<owner>/<repo>/raw/<ref>/<path>` URL (only swap `/blob/``/raw/`).
- Fetch `github.com/.../raw/...` and `raw.githubusercontent.com/...` URLs as-is.
Do **not** rewrite into `raw.githubusercontent.com/<owner>/<repo>/<ref>/<path>` form — that
format can't reliably represent refs containing slashes (e.g. a `feature/foo` branch).
Confirm the fetched URL resolves to a readable Markdown file.
- **Validate that the README contains a valid Spec Kit CLI install command.** The fetched
README must contain at least one `specify preset add ...` invocation. The strongest
signal is the catalog-install form whose URL matches the submitted **Download URL**:
- `specify preset add --from <download-url>` (preferred), or
- `specify preset add <preset-id>`, or
- `specify preset add --dev <path>`
A `specify preset add --from <url>` command only counts when its `<url>` **matches the
submitted Download URL exactly**. A `--from` command pointing at a *different* URL does
**not** satisfy the install-command requirement (treat it as if absent) — but the README
may still pass on one of the other accepted forms (`specify preset add <preset-id>` or
`specify preset add --dev <path>`).
If **no** accepted `specify preset add ...` command is present, the README is treated as a
generic description/pitch rather than preset-usage documentation — **fail this check** and
tell the submitter to add a valid install command (ideally
`specify preset add --from <download-url>`).
- **Prefer a preset-scoped README in monorepos.** If `documentation` resolves to a generic
repository-root README in a monorepo (the preset lives in a subdirectory such as
`presets/<id>/` and a preset-scoped README exists there), **flag it** in your comment and
recommend the submitter point `documentation` at the preset-scoped README
(e.g. `presets/<id>/README.md`) so the catalog surfaces usage instead of marketing. Treat
this as a flag rather than a hard failure **only if** the root README still contains a valid
`specify preset add ...` command for this preset; otherwise it fails check 2d above.
### 2e. Release and download URL validation
- The download URL should follow the pattern
`https://github.com/<owner>/<repo>/archive/refs/tags/v<version>.zip`
or
`https://github.com/<owner>/<repo>/releases/download/<tag>/<asset>.zip`
- Verify a GitHub release exists matching the submitted version
### 2e. Submission checklists
### 2f. Submission checklists
- Confirm that all required checkboxes in the Testing Checklist and Submission
Requirements sections are checked (`[x]`)
@@ -154,7 +208,7 @@ Insert the entry in **alphabetical order by preset ID** within the
"repository": "<repository>",
"download_url": "<download_url>",
"homepage": "<homepage or repository>",
"documentation": "<documentation or repository README>",
"documentation": "<documentation URL — the validated preset-usage README>",
"license": "<license>",
"requires": {
"speckit_version": "<speckit_version>"

View File

@@ -19,7 +19,7 @@ jobs:
permissions:
issues: write
steps:
- uses: actions/github-script@v9
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const issue = context.payload.issue;

View File

@@ -42,3 +42,15 @@ jobs:
globs: |
'**/*.md'
!extensions/**/*.md
shellcheck:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
# shellcheck is preinstalled on ubuntu-latest runners.
# Start at --severity=error to block real bugs without flagging style
# (notably SC2155). Tighten in a follow-up after cleanup.
- name: Run shellcheck on shell scripts
run: git ls-files -z -- '*.sh' | xargs -0 shellcheck --severity=error

View File

@@ -2,6 +2,21 @@
<!-- insert new changelog below this comment -->
## [0.11.8] - 2026-06-24
### Changed
- docs: add SpecKit Assistant npm package to Community Friends (#3142)
- Require preset-usage README with Spec Kit CLI syntax in preset submissions (#3104)
- [extension] Update Jira Integration (Sync Engine) extension to v0.4.0 (#3152)
- Add Spec Roadmap extension to community catalog (#3153)
- feat(integration): update Kimi integration for Kimi Code CLI (#2979)
- [extension] Add Golden Demo extension to community catalog (#3151)
- docs: run /speckit.checklist after /speckit.plan in quickstart (#3108)
- fix(workflows): preserve commas inside quoted list-literal elements (#3134)
- ci: pin actions to commit SHAs and add shellcheck (#3126)
- chore: release 0.11.7, begin 0.11.8.dev0 development (#3154)
## [0.11.7] - 2026-06-24
### Changed

View File

@@ -113,6 +113,16 @@ uv pip install -e ".[test]"
> `specify_cli` to this checkout's `src/`. This matches the gotcha documented in
> `AGENTS.md` (Common Pitfalls).
#### Shell scripts
```bash
git ls-files -z -- '*.sh' | xargs -0 shellcheck --severity=error
```
The CI `lint.yml` `shellcheck` job currently reports and blocks only
error-severity findings. Warnings such as SC2155 are intentionally outside this
job until a follow-up cleanup tightens the threshold.
### Manual testing
#### Testing setup

View File

@@ -56,6 +56,7 @@ The following community-contributed extensions are available in [`catalog.commun
| Fleet Orchestrator | Orchestrate a full feature lifecycle with human-in-the-loop gates across all SpecKit phases | `process` | Read+Write | [spec-kit-fleet](https://github.com/sharathsatish/spec-kit-fleet) |
| GitHub Issues Integration 1 | Generate spec artifacts from GitHub Issues - import issues, sync updates, and maintain bidirectional traceability | `integration` | Read+Write | [spec-kit-github-issues](https://github.com/Fatima367/spec-kit-github-issues) |
| GitHub Issues Integration 2 | Creates and syncs local specs from an existing GitHub issue | `integration` | Read+Write | [spec-kit-issue](https://github.com/aaronrsun/spec-kit-issue) |
| Golden Demo | Extracts acceptance criteria from specs, builds test vectors, and produces a behavioral drift report — complementary to Architecture Guard and CDD | `docs` | Read+Write | [spec-kit-golden-demo](https://github.com/jasstt/spec-kit-golden-demo) |
| Improve Extension | Audits any codebase as a senior advisor and writes prioritized, self-contained spec prompts under specs/ that the spec-kit lifecycle can process | `process` | Read+Write | [spec-kit-improve](https://github.com/d0whc3r/spec-kit-improve) |
| Intake | Normalize PRD, design, and test-case evidence into SDD-ready intake artifacts | `docs` | Read+Write | [spec-kit-intake](https://github.com/bigsmartben/spec-kit-intake) |
| Intelligent Agent Orchestrator | Cross-catalog agent discovery and intelligent prompt-to-command routing | `process` | Read+Write | [spec-kit-orchestrator](https://github.com/pragya247/spec-kit-orchestrator) |
@@ -117,6 +118,7 @@ The following community-contributed extensions are available in [`catalog.commun
| Spec Orchestrator | Cross-feature orchestration — track state, select tasks, and detect conflicts across parallel specs | `process` | Read-only | [spec-kit-orchestrator](https://github.com/Quratulain-bilal/spec-kit-orchestrator) |
| Spec Reference Loader | Reads the ## References section from the feature spec and loads only the listed docs into context | `docs` | Read-only | [spec-kit-spec-reference-loader](https://github.com/KevinBrown5280/spec-kit-spec-reference-loader) |
| Spec Refine | Update specs in-place, propagate changes to plan and tasks, and diff impact across artifacts | `process` | Read+Write | [spec-kit-refine](https://github.com/Quratulain-bilal/spec-kit-refine) |
| Spec Roadmap | Capture a durable spec roadmap after the constitution, then review specs against it before and after implementation so spec-specific decisions, outcomes, and constraints are never lost. | `process` | Read+Write | [speckit-roadmap](https://github.com/srobroek/speckit-roadmap) |
| Spec Scope | Effort estimation and scope tracking — estimate work, detect creep, and budget time per phase | `process` | Read-only | [spec-kit-scope-](https://github.com/Quratulain-bilal/spec-kit-scope-) |
| Spec Sync | Detect and resolve drift between specs and implementation. AI-assisted resolution with human approval | `docs` | Read+Write | [spec-kit-sync](https://github.com/bgervin/spec-kit-sync) |
| Spec Trace | Build a requirement → test traceability matrix from spec.md and the test suite — surface untested requirements and orphan tests | `code` | Read+Write | [spec-kit-trace](https://github.com/Quratulain-bilal/spec-kit-trace) |

View File

@@ -7,7 +7,9 @@ Community projects that extend, visualize, or build on Spec Kit:
- **[cc-spex](https://github.com/rhuss/cc-spex)** — A Claude Code plugin that adds composable traits on top of Spec Kit with [Superpowers](https://github.com/obra/superpowers)-based quality gates, spec/code review, git worktree isolation, and parallel implementation via agent teams.
- **[Spec Kit Assistant](https://marketplace.visualstudio.com/items?itemName=rfsales.speckit-assistant)** — A VS Code extension that provides a visual orchestrator for the full SDD workflow (constitution → specification → planning → tasks → implementation) with phase status visualization, an interactive task checklist, DAG visualization, and support for Claude, Gemini, GitHub Copilot, and OpenAI backends. Requires the `specify` CLI in your PATH.
- **[VS Code Spec Kit Assistant](https://marketplace.visualstudio.com/items?itemName=rfsales.speckit-assistant)** — A VS Code extension that provides a visual orchestrator for the full SDD workflow (constitution → specification → planning → tasks → implementation) with phase status visualization, an interactive task checklist, DAG visualization, and support for Claude, Gemini, GitHub Copilot, and OpenAI backends. Requires the `specify` CLI in your PATH.
- **[SpecKit Assistant](https://www.npmjs.com/package/speckit-assistant)** — A visual orchestrator for Spec-Driven Development (SDD). It connects your local specification, planning, and task checklists with AI agents (Claude, Gemini, GitHub Copilot). No global installation required — just run it via `npx speckit-assistant`.
- **[SpecKit Companion](https://marketplace.visualstudio.com/items?itemName=alfredoperez.speckit-companion)** — A VS Code extension that brings a visual GUI to Spec Kit. Browse specs in a rich markdown viewer with clickable file references, create specifications with image attachments, comment and refine each step inline (GitHub-style review), track your progress through the SDD workflow with a visual phase stepper, and manage steering documents like constitutions and templates.

View File

@@ -13,10 +13,10 @@ This guide will help you get started with Spec-Driven Development using Spec Kit
After installing Spec Kit and defining your project constitution, quick experiments can use the lean feature path: `/speckit.specify` -> `/speckit.plan` -> `/speckit.tasks` -> `/speckit.implement`. For production features or any work with meaningful ambiguity, treat `/speckit.clarify`, `/speckit.checklist`, and `/speckit.analyze` as regular quality gates:
```text
/speckit.constitution -> /speckit.specify -> /speckit.clarify -> /speckit.checklist -> /speckit.plan -> /speckit.tasks -> /speckit.analyze -> /speckit.implement
/speckit.constitution -> /speckit.specify -> /speckit.clarify -> /speckit.plan -> /speckit.checklist -> /speckit.tasks -> /speckit.analyze -> /speckit.implement
```
Use `/speckit.clarify` to reduce requirement ambiguity before planning, `/speckit.checklist` to validate requirements quality before planning, and `/speckit.analyze` to check spec/plan/task consistency before implementation starts. You can repeat `/speckit.analyze` after implementation as an extra review, but keep the first analysis before `/speckit.implement` so gaps are caught while the plan and tasks can still be adjusted.
Use `/speckit.clarify` to reduce requirement ambiguity before planning, `/speckit.checklist` (after `/speckit.plan`) to generate quality checklists that validate requirements completeness, clarity, and consistency, and `/speckit.analyze` to check spec/plan/task consistency before implementation starts. You can repeat `/speckit.analyze` after implementation as an extra review, but keep the first analysis before `/speckit.implement` so gaps are caught while the plan and tasks can still be adjusted.
### Step 1: Install Specify
@@ -75,12 +75,6 @@ uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME
/speckit.clarify Focus on security and performance requirements.
```
Then validate the requirements with `/speckit.checklist` before creating the technical plan:
```bash
/speckit.checklist
```
### Step 5: Create a Technical Implementation Plan
**In the chat**, use the `/speckit.plan` slash command to provide your tech stack and architecture choices.
@@ -89,6 +83,12 @@ Then validate the requirements with `/speckit.checklist` before creating the tec
/speckit.plan The application uses Vite with minimal number of libraries. Use vanilla HTML, CSS, and JavaScript as much as possible. Images are not uploaded anywhere and metadata is stored in a local SQLite database.
```
Then generate quality checklists with `/speckit.checklist` once the plan exists:
```bash
/speckit.checklist
```
### Step 6: Break Down, Analyze, and Implement
**In the chat**, use the `/speckit.tasks` slash command to create an actionable task list.
@@ -150,15 +150,7 @@ You can continue to refine the spec with more details using `/speckit.clarify`:
/speckit.clarify When you first launch Taskify, it's going to give you a list of the five users to pick from. There will be no password required. When you click on a user, you go into the main view, which displays the list of projects. When you click on a project, you open the Kanban board for that project. You're going to see the columns. You'll be able to drag and drop cards back and forth between different columns. You will see any cards that are assigned to you, the currently logged in user, in a different color from all the other ones, so you can quickly see yours. You can edit any comments that you make, but you can't edit comments that other people made. You can delete any comments that you made, but you can't delete comments anybody else made.
```
### Step 4: Validate the Spec
Validate the specification checklist using the `/speckit.checklist` command:
```bash
/speckit.checklist
```
### Step 5: Generate Technical Plan with `/speckit.plan`
### Step 4: Generate Technical Plan with `/speckit.plan`
Be specific about your tech stack and technical requirements:
@@ -166,6 +158,14 @@ Be specific about your tech stack and technical requirements:
/speckit.plan We are going to generate this using .NET Aspire, using Postgres as the database. The frontend should use Blazor server with drag-and-drop task boards, real-time updates. There should be a REST API created with a projects API, tasks API, and a notifications API.
```
### Step 5: Validate the Spec
Generate quality checklists to validate the specification using the `/speckit.checklist` command:
```bash
/speckit.checklist
```
### Step 6: Define Tasks
Generate an actionable task list using the `/speckit.tasks` command:

View File

@@ -25,7 +25,7 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify
| [iFlow CLI](https://docs.iflow.cn/en/cli/quickstart) | `iflow` | |
| [Junie](https://junie.jetbrains.com/) | `junie` | |
| [Kilo Code](https://github.com/Kilo-Org/kilocode) | `kilocode` | |
| [Kimi Code](https://code.kimi.com/) | `kimi` | Skills-based integration; supports `--migrate-legacy` for dotted→hyphenated directory migration |
| [Kimi Code](https://code.kimi.com/) | `kimi` | Skills-based integration; installs into `.kimi-code/skills/`. `--migrate-legacy` moves old `.kimi/skills/` installs to the new paths, and (when the `agent-context` extension is enabled) migrates `KIMI.md` context into `AGENTS.md` |
| [Kiro CLI](https://kiro.dev/docs/cli/) | `kiro-cli` | Kiro CLI does not substitute `$ARGUMENTS` in file-based prompts, so Spec Kit ships a prose fallback at render time (see [Manage prompts](https://kiro.dev/docs/cli/chat/manage-prompts/) and issue [#1926](https://github.com/github/spec-kit/issues/1926)). Alias: `--integration kiro` |
| [Lingma](https://lingma.aliyun.com/) | `lingma` | Skills-based integration; skills are installed automatically |
| [Mistral Vibe](https://github.com/mistralai/mistral-vibe) | `vibe` | |
@@ -158,7 +158,7 @@ Some integrations accept additional options via `--integration-options`:
| Integration | Option | Description |
| ----------- | ------------------- | -------------------------------------------------------------- |
| `generic` | `--commands-dir` | Required. Directory for command files |
| `kimi` | `--migrate-legacy` | Migrate legacy dotted skill directories to hyphenated format |
| `kimi` | `--migrate-legacy` | Migrate legacy `.kimi/skills/` installs to `.kimi-code/skills/` (including dotted→hyphenated directory names); when the `agent-context` extension is enabled, also migrates `KIMI.md` to `AGENTS.md` |
Example:
@@ -192,7 +192,6 @@ The currently declared multi-install safe integrations are:
| `iflow` | `.iflow/commands`, `IFLOW.md` |
| `junie` | `.junie/commands`, `.junie/AGENTS.md` |
| `kilocode` | `.kilocode/workflows`, `.kilocode/rules/specify-rules.md` |
| `kimi` | `.kimi/skills`, `KIMI.md` |
| `qodercli` | `.qoder/commands`, `QODER.md` |
| `qwen` | `.qwen/commands`, `QWEN.md` |
| `roo` | `.roo/commands`, `.roo/rules/specify-rules.md` |

View File

@@ -1,6 +1,6 @@
{
"schema_version": "1.0",
"updated_at": "2026-06-23T00:00:00Z",
"updated_at": "2026-06-24T00:00:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
"extensions": {
"aide": {
@@ -1327,6 +1327,39 @@
"created_at": "2026-04-12T15:30:00Z",
"updated_at": "2026-04-13T14:39:00Z"
},
"golden-demo": {
"name": "Golden Demo",
"id": "golden-demo",
"description": "Extracts acceptance criteria from specs, builds test vectors, and produces a behavioral drift report — complementary to Architecture Guard and CDD.",
"author": "jasstt",
"version": "0.1.1",
"download_url": "https://github.com/jasstt/spec-kit-golden-demo/archive/refs/tags/v0.1.1.zip",
"repository": "https://github.com/jasstt/spec-kit-golden-demo",
"homepage": "https://github.com/jasstt/spec-kit-golden-demo",
"documentation": "https://github.com/jasstt/spec-kit-golden-demo",
"license": "MIT",
"category": "docs",
"effect": "read-write",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 2,
"hooks": 2
},
"tags": [
"testing",
"drift-detection",
"behavioral-oracle",
"tdd",
"quality"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-06-24T00:00:00Z",
"updated_at": "2026-06-24T00:00:00Z"
},
"harness": {
"name": "Research Harness",
"id": "harness",
@@ -1548,25 +1581,34 @@
"id": "jira-sync",
"description": "An idempotent, drift-aware, fail-closed reconcile engine that mirrors spec-kit specs into Jira (Epic per repo, Story per spec, Subtask per phase).",
"author": "Ash Brener",
"version": "0.2.0",
"download_url": "https://github.com/ashbrener/spec-kit-jira-sync/archive/refs/tags/v0.2.0.zip",
"version": "0.4.0",
"download_url": "https://github.com/ashbrener/spec-kit-jira-sync/archive/refs/tags/v0.4.0.zip",
"repository": "https://github.com/ashbrener/spec-kit-jira-sync",
"homepage": "https://github.com/ashbrener/spec-kit-jira-sync",
"documentation": "https://github.com/ashbrener/spec-kit-jira-sync/blob/main/README.md",
"changelog": "https://github.com/ashbrener/spec-kit-jira-sync/releases",
"changelog": "https://github.com/ashbrener/spec-kit-jira-sync/blob/main/CHANGELOG.md",
"license": "MIT",
"category": "integration",
"effect": "read-write",
"requires": {
"speckit_version": ">=0.1.0"
"speckit_version": ">=0.1.0",
"tools": [
{ "name": "bash", "version": ">=4.4", "required": true },
{ "name": "git", "required": true },
{ "name": "curl", "required": true },
{ "name": "jq", "required": true },
{ "name": "gitleaks", "required": false },
{ "name": "trufflehog", "required": false }
]
},
"provides": {
"commands": 2,
"commands": 4,
"hooks": 0
},
"tags": [
"issue-tracking",
"jira",
"tasks-sync",
"lifecycle-mirror",
"reconcile",
"drift-aware"
],
@@ -1574,7 +1616,7 @@
"downloads": 0,
"stars": 0,
"created_at": "2026-06-08T00:00:00Z",
"updated_at": "2026-06-08T00:00:00Z"
"updated_at": "2026-06-24T00:00:00Z"
},
"learn": {
"name": "Learning Extension",
@@ -2962,6 +3004,40 @@
"created_at": "2026-04-20T00:00:00Z",
"updated_at": "2026-04-20T00:00:00Z"
},
"roadmap": {
"name": "Spec Roadmap",
"id": "roadmap",
"description": "Capture a durable spec roadmap after the constitution, then review specs against it before and after implementation so spec-specific decisions, outcomes, and constraints are never lost.",
"author": "srobroek",
"version": "0.1.0",
"download_url": "https://github.com/srobroek/speckit-roadmap/archive/refs/tags/v0.1.0.zip",
"repository": "https://github.com/srobroek/speckit-roadmap",
"homepage": "https://github.com/srobroek/speckit-roadmap",
"documentation": "https://github.com/srobroek/speckit-roadmap/blob/main/README.md",
"changelog": "https://github.com/srobroek/speckit-roadmap/blob/main/CHANGELOG.md",
"license": "Apache-2.0",
"category": "process",
"effect": "read-write",
"requires": {
"speckit_version": ">=0.11.6"
},
"provides": {
"commands": 4,
"hooks": 3
},
"tags": [
"roadmap",
"planning",
"governance",
"review",
"spec-alignment"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-06-24T00:00:00Z",
"updated_at": "2026-06-24T00:00:00Z"
},
"schedule": {
"name": "Spec Kit Schedule — CP-SAT Agent Orchestrator",
"id": "schedule",

View File

@@ -19,7 +19,7 @@ Before publishing a preset, ensure you have:
1. **Valid Preset**: A working preset with a valid `preset.yml` manifest
2. **Git Repository**: Preset hosted on GitHub (or other public git hosting)
3. **Documentation**: README.md with description and usage instructions
3. **Documentation**: A preset-scoped README.md that explains how to use **this preset**, including a valid `specify preset add ...` install command (see [Usage README Requirements](#usage-readme-requirements))
4. **License**: Open source license file (MIT, Apache 2.0, etc.)
5. **Versioning**: Semantic versioning (e.g., 1.0.0)
6. **Testing**: Preset tested on real projects with `specify preset add --dev`
@@ -147,6 +147,46 @@ https://github.com/your-org/spec-kit-preset-your-preset/archive/refs/tags/v1.0.0
specify preset add --from https://github.com/your-org/spec-kit-preset-your-preset/archive/refs/tags/v1.0.0.zip
```
### Usage README Requirements
The catalog `documentation` field must point at a README that explains how to use
**this preset** — not a product pitch for a broader framework or a separate CLI.
The submission workflow **mechanically enforces** that the linked README is a GitHub-hosted
URL whose path ends with `README.md`, resolves to a readable file, and contains at least one
valid `specify preset add ...` command. The remaining items (preferring a preset-scoped README
in monorepos, covering the minimum structure) are expectations a human reviewer checks —
follow them so your submission isn't sent back for changes.
- **Point `documentation` at the preset-scoped README.** In a monorepo where the preset
lives in a subdirectory (e.g. `presets/<id>/`), link the README inside that directory
(`presets/<id>/README.md`) rather than the repository-root README. The root README is
often a marketing/overview page; the catalog should surface preset usage instead. The key
requirement is that this README is reachable at the `documentation` URL so users can read
it *before* downloading the release artifact — it's fine for the same file to also ship
inside the release ZIP.
- **Include a valid Spec Kit CLI install command** *(enforced)*. The linked README must
contain at least one `specify preset add ...` invocation. Preferably use the
catalog-install form whose URL matches your Download URL:
```bash
# <download-url> is the same URL you submit as the catalog Download URL —
# either the tag archive or a release asset, e.g.:
specify preset add --from https://github.com/<org>/<repo>/archive/refs/tags/vX.Y.Z.zip
specify preset add --from https://github.com/<org>/<repo>/releases/download/vX.Y.Z/<id>-X.Y.Z.zip
```
`specify preset add <id>` and `specify preset add --dev <path>` are also accepted, but the
`--from <download-url>` form is the clearest signal that the README documents this exact
preset release.
- **Cover the minimum structure** so a reader can decide whether the preset fits:
- What the preset does / what it provides
- The install command using Spec Kit CLI syntax (above)
- When to use it / when not to use it
A submission whose linked README lacks a valid `specify preset add ...` command **fails
validation** (workflow check 2d) and will not be added until corrected.
---
## Submit to Catalog
@@ -181,12 +221,14 @@ Edit `presets/catalog.community.json` and add your preset.
"presets": {
"your-preset": {
"name": "Your Preset Name",
"id": "your-preset",
"description": "Brief description of what your preset provides",
"author": "Your Name",
"version": "1.0.0",
"download_url": "https://github.com/your-org/spec-kit-preset-your-preset/archive/refs/tags/v1.0.0.zip",
"sha256": "OPTIONAL: SHA-256 hex digest of the archive above; verified before install",
"repository": "https://github.com/your-org/spec-kit-preset-your-preset",
"documentation": "https://github.com/your-org/spec-kit-preset-your-preset/blob/main/README.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
@@ -243,7 +285,7 @@ git push origin add-your-preset
### Checklist
- [ ] Valid preset.yml manifest
- [ ] README.md with description and usage
- [ ] Usage README with a valid `specify preset add ...` command, linked from `documentation` (preset-scoped README recommended for monorepos)
- [ ] LICENSE file included
- [ ] GitHub release created
- [ ] Preset tested with `specify preset add --dev`
@@ -264,7 +306,15 @@ After submission, maintainers will review:
2. **Template quality** — templates are useful and well-structured
3. **Command coherence** — commands reference sections that exist in templates
4. **Security** — no malicious content, safe file operations
5. **Documentation**clear README explaining what the preset does
5. **Documentation** — the README linked from `documentation` explains how to use *this* preset and contains a valid `specify preset add ...` command
> **Reviewer note:** the workflow can mechanically check *structure* (the linked README
> resolves and contains a valid `specify preset add ...` snippet; when that snippet uses the
> `--from <url>` form, its URL must match the submitted download URL exactly — other accepted
> forms like `specify preset add <id>` don't reference the download URL at all). Whether the
> README genuinely documents *this* preset is partly a content judgment, so a human reviewer
> should still confirm the linked doc isn't just a funnel to a separate product or CLI before
> approving.
Once verified, `verified: true` is set and the preset appears in `specify preset search`.

View File

@@ -1,6 +1,6 @@
[project]
name = "specify-cli"
version = "0.11.7"
version = "0.11.8"
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
readme = "README.md"
requires-python = ">=3.11"

View File

@@ -1,11 +1,13 @@
"""Kimi Code integration — skills-based agent (Moonshot AI).
Kimi uses the ``.kimi/skills/speckit-<name>/SKILL.md`` layout with
Kimi uses the ``.kimi-code/skills/speckit-<name>/SKILL.md`` layout with
``/skill:speckit-<name>`` invocation syntax.
Includes legacy migration logic for projects initialised before Kimi
moved from dotted skill directories (``speckit.xxx``) to hyphenated
(``speckit-xxx``).
Legacy migration covers projects created before Kimi Code CLI moved to
this layout and handles two distinct changes: the directory move from
``.kimi/`` to ``.kimi-code/`` (including the ``KIMI.md`` → ``AGENTS.md``
context file), and the dotted-to-hyphenated skill naming
(``speckit.xxx`` → ``speckit-xxx``).
"""
from __future__ import annotations
@@ -14,7 +16,7 @@ import shutil
from pathlib import Path
from typing import Any
from ..base import IntegrationOption, SkillsIntegration
from ..base import IntegrationBase, IntegrationOption, SkillsIntegration
from ..manifest import IntegrationManifest
@@ -24,19 +26,43 @@ class KimiIntegration(SkillsIntegration):
key = "kimi"
config = {
"name": "Kimi Code",
"folder": ".kimi/",
"folder": ".kimi-code/",
"commands_subdir": "skills",
"install_url": "https://code.kimi.com/",
"requires_cli": True,
}
registrar_config = {
"dir": ".kimi/skills",
"dir": ".kimi-code/skills",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": "/SKILL.md",
}
context_file = "KIMI.md"
multi_install_safe = True
context_file = "AGENTS.md"
multi_install_safe = False
def build_command_invocation(self, command_name: str, args: str = "") -> str:
"""Build Kimi's native skill invocation: ``/skill:speckit-<stem>``.
Kimi Code CLI invokes installed skills with a ``/skill:<name>``
slash command (e.g. ``/skill:speckit-plan``), not the bare
``/speckit-<name>`` form produced by the generic skills base
class. Overriding here keeps ``dispatch_command()`` and workflow
command steps aligned with the ``/skill:`` guidance shown at init
time and in rendered hook invocations.
"""
stem = command_name
if stem.startswith("speckit."):
stem = stem[len("speckit.") :]
invocation = "/skill:speckit-" + stem.replace(".", "-")
if args:
invocation = f"{invocation} {args}"
return invocation
def post_process_skill_content(self, content: str) -> str:
"""Ensure in-skill cross-command references use Kimi's `/skill:` syntax."""
content = super().post_process_skill_content(content)
return content.replace("/speckit-", "/skill:speckit-")
@classmethod
def options(cls) -> list[IntegrationOption]:
@@ -51,7 +77,12 @@ class KimiIntegration(SkillsIntegration):
"--migrate-legacy",
is_flag=True,
default=False,
help="Migrate legacy dotted skill dirs (speckit.xxx → speckit-xxx)",
help=(
"Migrate legacy Kimi installations: "
".kimi/skills/ → .kimi-code/skills/, speckit.xxx → speckit-xxx, "
"and (when the agent-context extension is enabled) "
"KIMI.md user content → AGENTS.md"
),
),
]
@@ -62,64 +93,397 @@ class KimiIntegration(SkillsIntegration):
parsed_options: dict[str, Any] | None = None,
**opts: Any,
) -> list[Path]:
"""Install skills with optional legacy dotted-name migration."""
"""Install skills with optional legacy migration."""
parsed_options = parsed_options or {}
# Run base setup first so hyphenated targets (speckit-*) exist,
# then migrate/clean legacy dotted dirs without risking user content loss.
# Refuse a symlinked destination before any writes occur. base
# setup() only rejects a destination that *escapes* project_root
# after resolve(), so an in-tree symlinked ``.kimi-code`` /
# ``.kimi-code/skills`` (e.g. ``-> .``) would still pass that check
# and misdirect the SKILL.md writes into an unintended in-tree
# location (e.g. ``./skills/``). Reject any symlinked destination
# component up front so this never happens.
new_skills_dir = self.skills_dest(project_root)
if _has_symlinked_component(new_skills_dir, project_root):
raise ValueError(
f"Skills destination {new_skills_dir} contains a symlinked "
f"path component; refusing to install into it."
)
# Run base setup first so new-path targets (speckit-*) exist,
# then migrate/clean legacy dirs without risking user content loss.
created = super().setup(
project_root, manifest, parsed_options=parsed_options, **opts
)
if parsed_options.get("migrate_legacy", False):
skills_dir = self.skills_dest(project_root)
if skills_dir.is_dir():
_migrate_legacy_kimi_dotted_skills(skills_dir)
old_skills_dir = project_root / ".kimi" / "skills"
# Validate both endpoints. base setup() already rejects a
# destination that *escapes* the project root, but an in-tree
# symlinked ``.kimi-code``/``.kimi-code/skills`` (e.g. ``-> .``)
# would still misdirect the move; ``_is_safe_legacy_dir`` rejects
# any symlinked component, giving the destination the same
# protection as the source.
if _is_safe_legacy_dir(old_skills_dir, project_root) and (
_is_safe_legacy_dir(new_skills_dir, project_root)
):
_migrate_legacy_kimi_skills_dir(old_skills_dir, new_skills_dir)
# Mirror upsert/remove_context_section: a disabled agent-context
# extension is a full opt-out, so skip the KIMI.md → AGENTS.md
# migration entirely and leave both files untouched.
if self._agent_context_extension_enabled(project_root):
marker_start, marker_end = self._resolve_context_markers(project_root)
_migrate_legacy_kimi_context_file(
project_root, marker_start=marker_start, marker_end=marker_end
)
return created
def teardown(
self,
project_root: Path,
manifest: IntegrationManifest,
*,
force: bool = False,
) -> tuple[list[Path], list[Path]]:
"""Uninstall Kimi skills and remove leftover legacy directories."""
removed, skipped = super().teardown(project_root, manifest, force=force)
def _migrate_legacy_kimi_dotted_skills(skills_dir: Path) -> tuple[int, int]:
"""Migrate legacy Kimi dotted skill dirs (speckit.xxx) to hyphenated format.
old_skills_dir = project_root / ".kimi" / "skills"
if _is_safe_legacy_dir(old_skills_dir, project_root):
legacy_dirs = sorted(
[*old_skills_dir.glob("speckit-*"), *old_skills_dir.glob("speckit.*")]
)
for legacy_dir in legacy_dirs:
if legacy_dir.is_symlink() or not legacy_dir.is_dir():
continue
if _is_speckit_generated_skill(legacy_dir):
try:
shutil.rmtree(legacy_dir)
removed.append(legacy_dir)
except OSError:
skipped.append(legacy_dir)
try:
old_skills_dir.rmdir()
except OSError:
pass
return removed, skipped
def _has_symlinked_component(path: Path, project_root: Path) -> bool:
"""Return ``True`` when *path* escapes *project_root* or any component is a symlink.
Walks the components strictly between *project_root* and *path*
(including the final one) and reports whether any of them is a symlink.
Components that do not exist yet are not symlinks, so this safely handles
a not-yet-created destination. *project_root* itself is trusted and never
checked. A *path* outside *project_root* is treated as unsafe.
"""
try:
relative = path.relative_to(project_root)
except ValueError:
return True
current = project_root
for part in relative.parts:
current = current / part
if current.is_symlink():
return True
return False
def _is_safe_legacy_dir(path: Path, project_root: Path) -> bool:
"""Return ``True`` when *path* is a real directory safely inside *project_root*.
Legacy migration and cleanup ``shutil.move()`` and ``shutil.rmtree()``
directories, so a symlinked ``.kimi``/``.kimi/skills`` (or one reached
through a symlinked parent) must never be followed: doing so could
relocate or delete content living outside the project tree — or operate
on an unrelated in-tree directory (e.g. ``.kimi -> .`` makes
``.kimi/skills`` resolve to ``./skills``).
Checking only the fully-resolved path is insufficient, because a symlink
pointing elsewhere *inside* the project still resolves to a location under
*project_root*. We therefore reject the path when it is not a directory,
when any component between *project_root* and *path* is a symlink
(including the final component), or when the resolved path escapes the
resolved *project_root*.
"""
if not path.is_dir():
return False
# Reject if any path component below project_root is a symlink (or the
# path escapes project_root). We trust project_root itself, so only
# components strictly under it are checked.
if _has_symlinked_component(path, project_root):
return False
try:
resolved = path.resolve()
root = project_root.resolve()
except OSError:
return False
return resolved == root or root in resolved.parents
def _migrate_legacy_kimi_skills_dir(
old_skills_dir: Path, new_skills_dir: Path
) -> tuple[int, int]:
"""Migrate skills from the legacy ``.kimi/skills/`` directory to ``.kimi-code/skills/``.
Handles both hyphenated (``speckit-xxx``) and dotted (``speckit.xxx``)
legacy directory names. If a target already exists, the legacy dir is
only removed when its ``SKILL.md`` is byte-identical and no extra user
files are present.
Returns ``(migrated_count, removed_count)``.
"""
if not skills_dir.is_dir():
if not old_skills_dir.is_dir():
return (0, 0)
migrated_count = 0
removed_count = 0
for legacy_dir in sorted(skills_dir.glob("speckit.*")):
if not legacy_dir.is_dir():
# Process hyphenated dirs first, then dotted dirs.
legacy_dirs = sorted(old_skills_dir.glob("speckit-*")) + sorted(
old_skills_dir.glob("speckit.*")
)
for legacy_dir in legacy_dirs:
if legacy_dir.is_symlink() or not legacy_dir.is_dir():
continue
if not (legacy_dir / "SKILL.md").exists():
legacy_skill = legacy_dir / "SKILL.md"
# Treat a symlinked SKILL.md as invalid: later read_bytes() would
# otherwise follow it and read content from outside the project.
if legacy_skill.is_symlink() or not legacy_skill.is_file():
continue
suffix = legacy_dir.name[len("speckit."):]
if not suffix:
target_name = _legacy_to_target_name(legacy_dir.name)
if not target_name:
continue
target_dir = skills_dir / f"speckit-{suffix.replace('.', '-')}"
target_dir = new_skills_dir / target_name
# Skip if the legacy dir is already the target dir (same-directory call).
if legacy_dir.resolve() == target_dir.resolve():
continue
if not target_dir.exists():
target_dir.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(legacy_dir), str(target_dir))
migrated_count += 1
continue
# Target exists — only remove legacy if SKILL.md is identical
# Target exists — only remove legacy if SKILL.md is identical.
# Skip when the target dir or its SKILL.md is a symlink (or the dir is
# not a real directory) so the byte comparison never follows a link
# outside the project. (legacy_skill is already guaranteed to be a real
# file by the guard above.)
if target_dir.is_symlink() or not target_dir.is_dir():
continue
target_skill = target_dir / "SKILL.md"
legacy_skill = legacy_dir / "SKILL.md"
if target_skill.is_file():
try:
if target_skill.read_bytes() == legacy_skill.read_bytes():
has_extra = any(
child.name != "SKILL.md" for child in legacy_dir.iterdir()
)
if not has_extra:
shutil.rmtree(legacy_dir)
removed_count += 1
except OSError:
pass
if target_skill.is_symlink() or not target_skill.is_file():
continue
try:
if target_skill.read_bytes() == legacy_skill.read_bytes():
has_extra = any(
child.name != "SKILL.md" for child in legacy_dir.iterdir()
)
if not has_extra:
shutil.rmtree(legacy_dir)
removed_count += 1
except OSError:
pass
# Remove the legacy skills directory if it is now empty.
try:
old_skills_dir.rmdir()
except OSError:
pass
return (migrated_count, removed_count)
def _legacy_to_target_name(legacy_name: str) -> str:
"""Convert a legacy skill directory name to the modern hyphenated form."""
if legacy_name.startswith("speckit-"):
return legacy_name
if legacy_name.startswith("speckit."):
suffix = legacy_name[len("speckit.") :]
if suffix:
return f"speckit-{suffix.replace('.', '-')}"
return ""
def _is_speckit_generated_skill(skill_dir: Path) -> bool:
"""Return True when *skill_dir* contains a Speckit-generated SKILL.md.
Uses the ``metadata.author`` and ``metadata.source`` fields written by
``SkillsIntegration.setup()`` to avoid deleting user-authored skills.
"""
skill_file = skill_dir / "SKILL.md"
# A symlinked SKILL.md is never treated as Speckit-generated, so teardown
# cleanup never follows it to read frontmatter from outside the project.
if skill_file.is_symlink() or not skill_file.is_file():
return False
try:
content = skill_file.read_text(encoding="utf-8")
except OSError:
return False
if not content.startswith("---"):
return False
parts = content.split("---", 2)
if len(parts) < 3:
return False
try:
import yaml
frontmatter = yaml.safe_load(parts[1])
except Exception:
return False
if not isinstance(frontmatter, dict):
return False
metadata = frontmatter.get("metadata", {})
if not isinstance(metadata, dict):
return False
author = metadata.get("author", "")
source = metadata.get("source", "")
return (
author == "github-spec-kit"
and isinstance(source, str)
and source.startswith("templates/commands/")
)
def _migrate_legacy_kimi_context_file(
project_root: Path,
*,
marker_start: str = IntegrationBase.CONTEXT_MARKER_START,
marker_end: str = IntegrationBase.CONTEXT_MARKER_END,
) -> bool:
"""Migrate user content from legacy ``KIMI.md`` to ``AGENTS.md``.
The Speckit managed section is stripped from ``KIMI.md`` before the
remaining content is appended to ``AGENTS.md``. The legacy file is
deleted if it becomes empty. Returns ``True`` if ``KIMI.md`` was
migrated, ``False`` when the migration is skipped.
The migration is skipped (leaving ``KIMI.md`` untouched) in any of these
cases, so a best-effort legacy cleanup never aborts ``setup()`` or
corrupts ``AGENTS.md``:
- ``KIMI.md`` is a symlink, missing, or unreadable (its target could be
read from outside the project, or it may not be valid UTF-8).
- ``AGENTS.md`` is a symlink (it could redirect the write to a file
outside the project root), exists as a non-file (e.g. a directory),
or is unreadable/unwritable.
- ``KIMI.md`` has a corrupted managed section — only one marker is
present, or the end marker precedes the start. Stripping is only done
when both markers are present and well-ordered, so a partial managed
block is never copied into ``AGENTS.md``; the user repairs it manually.
"""
legacy_path = project_root / "KIMI.md"
if legacy_path.is_symlink() or not legacy_path.is_file():
return False
target_path = project_root / "AGENTS.md"
# Never follow a symlinked target, and never treat an existing non-file
# (e.g. a directory) as a writable context file.
if target_path.is_symlink() or (target_path.exists() and not target_path.is_file()):
return False
try:
content = legacy_path.read_text(encoding="utf-8-sig")
except (OSError, UnicodeDecodeError):
return False
marker_pairs = [(marker_start, marker_end)]
default_pair = (
IntegrationBase.CONTEXT_MARKER_START,
IntegrationBase.CONTEXT_MARKER_END,
)
if default_pair not in marker_pairs:
marker_pairs.append(default_pair)
start_idx = -1
end_idx = -1
has_start = False
has_end = False
for s, e in marker_pairs:
s_idx = content.find(s)
e_idx = content.find(e, s_idx if s_idx != -1 else 0)
has_s = s_idx != -1
has_e = e_idx != -1
if not has_s and not has_e:
continue
# Refuse to migrate a corrupted managed section: exactly one marker, or
# an end marker that does not follow the start.
if has_s != has_e or e_idx <= s_idx:
return False
marker_start, marker_end = s, e
start_idx, end_idx = s_idx, e_idx
has_start = True
has_end = True
break
if has_start and has_end:
removal_start = start_idx
removal_end = end_idx + len(marker_end)
if removal_end < len(content) and content[removal_end] == "\r":
removal_end += 1
if removal_end < len(content) and content[removal_end] == "\n":
removal_end += 1
if removal_start > 0 and content[removal_start - 1] == "\n":
if removal_start > 1 and content[removal_start - 2] == "\n":
removal_start -= 1
content = content[:removal_start] + content[removal_end:]
user_content = content.replace("\r\n", "\n").replace("\r", "\n").strip()
if not user_content:
legacy_path.unlink()
return True
try:
if target_path.is_file():
existing = target_path.read_text(encoding="utf-8-sig")
existing = existing.replace("\r\n", "\n").replace("\r", "\n")
if not existing.endswith("\n"):
existing += "\n"
new_content = existing + "\n" + user_content + "\n"
else:
new_content = user_content + "\n"
target_path.parent.mkdir(parents=True, exist_ok=True)
target_path.write_bytes(new_content.encode("utf-8"))
except (OSError, UnicodeDecodeError):
return False
legacy_path.unlink()
return True
def _migrate_legacy_kimi_dotted_skills(skills_dir: Path) -> tuple[int, int]:
"""Compatibility shim — migrate legacy dotted skill dirs in place.
.. deprecated::
Kept for direct callers/tests. New code should call
``_migrate_legacy_kimi_skills_dir`` directly.
Delegates to ``_migrate_legacy_kimi_skills_dir`` with *skills_dir* as both
source and destination, so it processes every ``speckit-*`` and
``speckit.*`` entry under *skills_dir*. Because the two paths are
identical, the same-path short-circuit there skips any directory whose
target resolves to itself; in practice this renames dotted
``speckit.xxx`` dirs to hyphenated ``speckit-xxx`` in place and never
moves content outside *skills_dir*.
Returns ``(migrated_count, removed_count)``.
"""
return _migrate_legacy_kimi_skills_dir(skills_dir, skills_dir)

View File

@@ -146,6 +146,40 @@ def _build_namespace(context: Any) -> dict[str, Any]:
return ns
def _split_top_level_commas(text: str) -> list[str]:
"""Split *text* on commas that are not inside quotes or nested brackets.
Used for list-literal elements so a quoted element containing a comma
(e.g. ``["a, b", "c"]``) is not split mid-string, and nested lists/calls
(e.g. ``[[1, 2], 3]``) are kept intact.
"""
parts: list[str] = []
buf: list[str] = []
quote: str | None = None
depth = 0
for ch in text:
if quote is not None:
buf.append(ch)
if ch == quote:
quote = None
elif ch in ("'", '"'):
quote = ch
buf.append(ch)
elif ch in "([{":
depth += 1
buf.append(ch)
elif ch in ")]}":
depth = max(0, depth - 1)
buf.append(ch)
elif ch == "," and depth == 0:
parts.append("".join(buf))
buf = []
else:
buf.append(ch)
parts.append("".join(buf))
return parts
def _evaluate_simple_expression(expr: str, namespace: dict[str, Any]) -> Any:
"""Evaluate a simple expression against the namespace.
@@ -291,7 +325,10 @@ def _evaluate_simple_expression(expr: str, namespace: dict[str, Any]) -> Any:
inner = expr[1:-1].strip()
if not inner:
return []
items = [_evaluate_simple_expression(i.strip(), namespace) for i in inner.split(",")]
items = [
_evaluate_simple_expression(i.strip(), namespace)
for i in _split_top_level_commas(inner)
]
return items
# Variable reference (dot-path)

View File

@@ -1,18 +1,42 @@
"""Tests for KimiIntegration — skills integration with legacy migration."""
from pathlib import Path
import pytest
from specify_cli.integrations import get_integration
from specify_cli.integrations.kimi import _migrate_legacy_kimi_dotted_skills
from specify_cli.integrations.kimi import (
_migrate_legacy_kimi_context_file,
_migrate_legacy_kimi_dotted_skills,
_migrate_legacy_kimi_skills_dir,
)
from specify_cli.integrations.manifest import IntegrationManifest
from .test_integration_base_skills import SkillsIntegrationTests
def _symlink_or_skip(
link: Path, target: Path, *, target_is_directory: bool = False
) -> None:
"""Create *link* pointing at *target*, skipping the test if unsupported.
Symlink creation fails on Windows without the create-symlink privilege and
in some restricted CI sandboxes. The symlink-safety tests below assert
behavior that only matters when symlinks exist, so skip (rather than error)
when the platform cannot create them.
"""
try:
link.symlink_to(target, target_is_directory=target_is_directory)
except (OSError, NotImplementedError) as exc:
pytest.skip(f"symlinks unavailable: {exc}")
class TestKimiIntegration(SkillsIntegrationTests):
KEY = "kimi"
FOLDER = ".kimi/"
FOLDER = ".kimi-code/"
COMMANDS_SUBDIR = "skills"
REGISTRAR_DIR = ".kimi/skills"
CONTEXT_FILE = "KIMI.md"
REGISTRAR_DIR = ".kimi-code/skills"
CONTEXT_FILE = "AGENTS.md"
class TestKimiOptions:
@@ -103,12 +127,13 @@ class TestKimiLegacyMigration:
assert migrated == 0
assert removed == 0
def test_setup_with_migrate_legacy_option(self, tmp_path):
"""KimiIntegration.setup() with --migrate-legacy migrates dotted dirs."""
def test_setup_migrate_legacy_moves_old_skills_dir(self, tmp_path):
"""--migrate-legacy moves hyphenated skills from .kimi/skills to .kimi-code/skills."""
i = get_integration("kimi")
skills_dir = tmp_path / ".kimi" / "skills"
legacy = skills_dir / "speckit.oldcmd"
old_skills_dir = tmp_path / ".kimi" / "skills"
new_skills_dir = tmp_path / ".kimi-code" / "skills"
legacy = old_skills_dir / "speckit-oldcmd"
legacy.mkdir(parents=True)
(legacy / "SKILL.md").write_text("# Legacy\n")
@@ -116,9 +141,428 @@ class TestKimiLegacyMigration:
i.setup(tmp_path, m, parsed_options={"migrate_legacy": True})
assert not legacy.exists()
assert (skills_dir / "speckit-oldcmd" / "SKILL.md").exists()
assert not old_skills_dir.exists()
assert (new_skills_dir / "speckit-oldcmd" / "SKILL.md").exists()
# New skills from templates should also exist
assert (skills_dir / "speckit-specify" / "SKILL.md").exists()
assert (new_skills_dir / "speckit-specify" / "SKILL.md").exists()
def test_setup_with_migrate_legacy_option(self, tmp_path):
"""KimiIntegration.setup() with --migrate-legacy migrates dotted dirs."""
i = get_integration("kimi")
old_skills_dir = tmp_path / ".kimi" / "skills"
new_skills_dir = tmp_path / ".kimi-code" / "skills"
legacy = old_skills_dir / "speckit.oldcmd"
legacy.mkdir(parents=True)
(legacy / "SKILL.md").write_text("# Legacy\n")
m = IntegrationManifest("kimi", tmp_path)
i.setup(tmp_path, m, parsed_options={"migrate_legacy": True})
assert not legacy.exists()
assert (new_skills_dir / "speckit-oldcmd" / "SKILL.md").exists()
# New skills from templates should also exist
assert (new_skills_dir / "speckit-specify" / "SKILL.md").exists()
class TestKimiContextFileMigration:
"""KIMI.md → AGENTS.md migration under --migrate-legacy."""
def test_setup_migrate_legacy_moves_kimi_md_user_content(self, tmp_path):
i = get_integration("kimi")
kimi_md = tmp_path / "KIMI.md"
kimi_md.write_text(
"# Project context\n\n"
"<!-- SPECKIT START -->\n"
"old managed section\n"
"<!-- SPECKIT END -->\n\n"
"Keep this user note.\n"
)
m = IntegrationManifest("kimi", tmp_path)
i.setup(tmp_path, m, parsed_options={"migrate_legacy": True})
agents_md = tmp_path / "AGENTS.md"
assert agents_md.exists()
content = agents_md.read_text(encoding="utf-8")
assert "Keep this user note." in content
assert "old managed section" not in content
assert "<!-- SPECKIT START -->" in content
assert not kimi_md.exists()
def test_setup_migrate_legacy_removes_empty_kimi_md(self, tmp_path):
i = get_integration("kimi")
kimi_md = tmp_path / "KIMI.md"
kimi_md.write_text(
"<!-- SPECKIT START -->\n"
"only managed section\n"
"<!-- SPECKIT END -->\n"
)
m = IntegrationManifest("kimi", tmp_path)
i.setup(tmp_path, m, parsed_options={"migrate_legacy": True})
assert (tmp_path / "AGENTS.md").exists()
assert not kimi_md.exists()
def test_setup_migrate_legacy_appends_to_existing_agents_md(self, tmp_path):
i = get_integration("kimi")
agents_md = tmp_path / "AGENTS.md"
agents_md.write_text("# Existing AGENTS.md\n\nExisting note.\n")
kimi_md = tmp_path / "KIMI.md"
kimi_md.write_text("# Kimi context\n\nKimi-specific note.\n")
m = IntegrationManifest("kimi", tmp_path)
i.setup(tmp_path, m, parsed_options={"migrate_legacy": True})
content = agents_md.read_text(encoding="utf-8")
assert "Existing note." in content
assert "Kimi-specific note." in content
assert "<!-- SPECKIT START -->" in content
assert not kimi_md.exists()
def test_setup_migrate_legacy_uses_custom_context_markers(self, tmp_path):
"""Migration respects context_markers from agent-context extension config."""
i = get_integration("kimi")
config_dir = tmp_path / ".specify" / "extensions" / "agent-context"
config_dir.mkdir(parents=True)
(config_dir / "agent-context-config.yml").write_text(
"context_file: AGENTS.md\n"
"context_markers:\n"
" start: '<!-- CUSTOM START -->'\n"
" end: '<!-- CUSTOM END -->'\n"
)
kimi_md = tmp_path / "KIMI.md"
kimi_md.write_text(
"# Project context\n\n"
"<!-- CUSTOM START -->\n"
"old managed section\n"
"<!-- CUSTOM END -->\n\n"
"Keep this user note.\n"
)
m = IntegrationManifest("kimi", tmp_path)
i.setup(tmp_path, m, parsed_options={"migrate_legacy": True})
agents_md = tmp_path / "AGENTS.md"
assert agents_md.exists()
content = agents_md.read_text(encoding="utf-8")
assert "Keep this user note." in content
assert "old managed section" not in content
assert "<!-- CUSTOM START -->" in content
assert "<!-- CUSTOM END -->" in content
assert "<!-- SPECKIT START -->" not in content
assert not kimi_md.exists()
def test_setup_migrate_legacy_skipped_when_agent_context_disabled(
self, tmp_path
):
"""A disabled agent-context extension opts out of KIMI.md migration."""
i = get_integration("kimi")
registry = tmp_path / ".specify" / "extensions" / ".registry"
registry.parent.mkdir(parents=True)
registry.write_text('{"extensions": {"agent-context": {"enabled": false}}}')
kimi_md = tmp_path / "KIMI.md"
kimi_md.write_text("# Kimi context\n\nKeep this user note.\n")
m = IntegrationManifest("kimi", tmp_path)
i.setup(tmp_path, m, parsed_options={"migrate_legacy": True})
# Opted-out project: KIMI.md is left untouched and AGENTS.md is not
# created/modified by the migration.
assert kimi_md.is_file()
assert kimi_md.read_text() == "# Kimi context\n\nKeep this user note.\n"
assert not (tmp_path / "AGENTS.md").exists()
def test_context_migration_skips_corrupted_single_marker(self, tmp_path):
"""A KIMI.md with only a start marker is left untouched (no leak)."""
project = tmp_path
kimi_md = project / "KIMI.md"
kimi_md.write_text(
"# Notes\n\n"
"<!-- SPECKIT START -->\n"
"dangling managed content\n"
)
result = _migrate_legacy_kimi_context_file(project)
assert result is False
# KIMI.md untouched; managed block never copied into AGENTS.md.
assert kimi_md.is_file()
assert "dangling managed content" in kimi_md.read_text()
assert not (project / "AGENTS.md").exists()
def test_context_migration_skips_unreadable_kimi_md(self, tmp_path):
"""Non-UTF-8 KIMI.md is skipped instead of raising during setup."""
project = tmp_path
kimi_md = project / "KIMI.md"
kimi_md.write_bytes(b"\xff\xfe invalid utf-8 \xa6\n")
result = _migrate_legacy_kimi_context_file(project)
assert result is False
assert kimi_md.is_file()
assert not (project / "AGENTS.md").exists()
def test_context_migration_skips_when_agents_md_is_directory(self, tmp_path):
"""An AGENTS.md that exists as a directory is skipped, not written to."""
project = tmp_path
(project / "AGENTS.md").mkdir()
kimi_md = project / "KIMI.md"
kimi_md.write_text("# Notes\n\nKeep this.\n")
result = _migrate_legacy_kimi_context_file(project)
assert result is False
# KIMI.md is preserved and the directory is untouched.
assert kimi_md.is_file()
assert (project / "AGENTS.md").is_dir()
class TestKimiTeardownLegacyCleanup:
"""teardown() removes leftover legacy .kimi/skills/ directories."""
def test_teardown_removes_legacy_speckit_skills(self, tmp_path):
i = get_integration("kimi")
legacy_skill = tmp_path / ".kimi" / "skills" / "speckit-plan" / "SKILL.md"
legacy_skill.parent.mkdir(parents=True)
legacy_skill.write_text(
"---\n"
"name: \"speckit-plan\"\n"
"description: \"Plan workflow\"\n"
"metadata:\n"
" author: \"github-spec-kit\"\n"
" source: \"templates/commands/plan.md\"\n"
"---\n"
)
m = IntegrationManifest("kimi", tmp_path)
i.teardown(tmp_path, m)
assert not legacy_skill.exists()
assert not (tmp_path / ".kimi" / "skills").exists()
def test_teardown_preserves_user_skills_in_legacy_dir(self, tmp_path):
i = get_integration("kimi")
user_skill = tmp_path / ".kimi" / "skills" / "my-custom" / "SKILL.md"
user_skill.parent.mkdir(parents=True)
user_skill.write_text("# My custom skill\n")
m = IntegrationManifest("kimi", tmp_path)
i.teardown(tmp_path, m)
assert user_skill.exists()
class TestKimiCommandInvocation:
"""Kimi dispatch must use the native ``/skill:`` slash command."""
def test_build_command_invocation_uses_skill_prefix(self):
i = get_integration("kimi")
assert i.build_command_invocation("specify") == "/skill:speckit-specify"
assert i.build_command_invocation("speckit.plan") == "/skill:speckit-plan"
def test_build_command_invocation_dotted_extension(self):
i = get_integration("kimi")
assert (
i.build_command_invocation("speckit.git.commit")
== "/skill:speckit-git-commit"
)
def test_build_command_invocation_appends_args(self):
i = get_integration("kimi")
assert (
i.build_command_invocation("specify", "my feature")
== "/skill:speckit-specify my feature"
)
class TestKimiLegacySymlinkSafety:
"""Legacy migration/cleanup must not follow symlinks out of the project."""
def test_migrate_skips_symlinked_legacy_skills_dir(self, tmp_path):
# An attacker-controlled directory outside the project root. Use a
# non-template skill name so a successful migration would be visible
# (the bundled templates never create "speckit-evillegacy").
outside = tmp_path / "outside"
(outside / "speckit-evillegacy").mkdir(parents=True)
(outside / "speckit-evillegacy" / "SKILL.md").write_text("# evil\n")
project = tmp_path / "project"
(project / ".kimi").mkdir(parents=True)
# .kimi/skills is a symlink to the outside directory.
_symlink_or_skip(
project / ".kimi" / "skills", outside, target_is_directory=True
)
i = get_integration("kimi")
m = IntegrationManifest("kimi", project)
i.setup(project, m, parsed_options={"migrate_legacy": True})
# Outside content must be untouched (not moved into .kimi-code).
assert (outside / "speckit-evillegacy" / "SKILL.md").exists()
assert not (
project / ".kimi-code" / "skills" / "speckit-evillegacy"
).exists()
def test_teardown_skips_symlinked_legacy_skills_dir(self, tmp_path):
outside = tmp_path / "outside"
outside.mkdir()
keep = outside / "keep.txt"
keep.write_text("important\n")
project = tmp_path / "project"
(project / ".kimi").mkdir(parents=True)
_symlink_or_skip(
project / ".kimi" / "skills", outside, target_is_directory=True
)
i = get_integration("kimi")
m = IntegrationManifest("kimi", project)
i.teardown(project, m)
# The symlink target and its contents must survive teardown.
assert keep.exists()
def test_migrate_skips_symlinked_legacy_parent_dir(self, tmp_path):
# `.kimi` is itself a symlink to the project root, so `.kimi/skills`
# resolves to `./skills` — an unrelated in-tree directory. Even though
# the resolved path stays inside the project, migration must not
# operate on it because a path component is a symlink.
project = tmp_path / "project"
unrelated = project / "skills" / "speckit-evillegacy"
unrelated.mkdir(parents=True)
(unrelated / "SKILL.md").write_text("# unrelated\n")
# .kimi -> project root, so .kimi/skills == ./skills.
_symlink_or_skip(project / ".kimi", project, target_is_directory=True)
i = get_integration("kimi")
m = IntegrationManifest("kimi", project)
i.setup(project, m, parsed_options={"migrate_legacy": True})
# The unrelated ./skills content must be untouched.
assert (unrelated / "SKILL.md").exists()
assert not (
project / ".kimi-code" / "skills" / "speckit-evillegacy"
).exists()
def test_teardown_skips_symlinked_legacy_parent_dir(self, tmp_path):
project = tmp_path / "project"
project.mkdir()
# Looks Speckit-generated, so only the symlink check protects it.
unrelated = project / "skills" / "speckit-evillegacy"
unrelated.mkdir(parents=True)
(unrelated / "SKILL.md").write_text(
"---\nmetadata:\n author: github-spec-kit\n---\n# x\n"
)
_symlink_or_skip(project / ".kimi", project, target_is_directory=True)
i = get_integration("kimi")
m = IntegrationManifest("kimi", project)
i.teardown(project, m)
# The unrelated ./skills content must survive teardown.
assert (unrelated / "SKILL.md").exists()
def test_setup_rejects_symlinked_destination_before_writing(self, tmp_path):
# `.kimi-code` is a symlink to the project root, so the skills
# destination `.kimi-code/skills` resolves to `./skills` — an
# unintended in-tree location. base setup() only rejects a
# destination that escapes the project root, so without the
# pre-check it would write SKILL.md files into `./skills`. setup()
# must refuse before any write occurs.
project = tmp_path / "project"
project.mkdir()
_symlink_or_skip(project / ".kimi-code", project, target_is_directory=True)
i = get_integration("kimi")
m = IntegrationManifest("kimi", project)
with pytest.raises(ValueError, match="symlinked"):
i.setup(project, m)
# Nothing was written into the unintended `./skills` location.
assert not (project / "skills").exists()
def test_migrate_skips_symlinked_target_dir(self, tmp_path):
# The destination `.kimi-code/skills/speckit-foo` already exists but is
# a symlink to a directory outside the project. Migration compares
# SKILL.md bytes to decide whether to drop the legacy copy; it must not
# follow the symlinked target dir to read SKILL.md from outside.
outside = tmp_path / "outside"
outside.mkdir()
(outside / "SKILL.md").write_text("# shared\n")
project = tmp_path / "project"
legacy = project / ".kimi" / "skills" / "speckit-foo"
legacy.mkdir(parents=True)
# Identical bytes: without the symlink guard the legacy dir would be
# removed after following the link out of the project.
(legacy / "SKILL.md").write_text("# shared\n")
target = project / ".kimi-code" / "skills" / "speckit-foo"
target.parent.mkdir(parents=True)
_symlink_or_skip(target, outside, target_is_directory=True)
_migrate_legacy_kimi_skills_dir(
project / ".kimi" / "skills", project / ".kimi-code" / "skills"
)
# Legacy copy is preserved (migration refused to follow the symlink),
# and the outside target is untouched.
assert (legacy / "SKILL.md").exists()
assert (outside / "SKILL.md").exists()
def test_context_migration_does_not_write_through_symlinked_agents_md(
self, tmp_path
):
# A sensitive file outside the project that a malicious AGENTS.md
# symlink points at. Migration must never overwrite it.
outside = tmp_path / "outside"
outside.mkdir()
secret = outside / "secret.txt"
secret.write_text("original secret\n")
project = tmp_path / "project"
project.mkdir()
_symlink_or_skip(project / "AGENTS.md", secret)
(project / "KIMI.md").write_text("# Notes\n\nKeep this.\n")
result = _migrate_legacy_kimi_context_file(project)
# The outside file must not be overwritten through the symlink.
assert secret.read_text() == "original secret\n"
# KIMI.md is preserved so the user can migrate manually.
assert (project / "KIMI.md").is_file()
assert result is False
def test_context_migration_does_not_follow_symlinked_kimi_md(self, tmp_path):
# A symlinked KIMI.md (source) must not be followed/consumed.
outside = tmp_path / "outside"
outside.mkdir()
external = outside / "external.md"
external.write_text("# external\n")
project = tmp_path / "project"
project.mkdir()
_symlink_or_skip(project / "KIMI.md", external)
result = _migrate_legacy_kimi_context_file(project)
assert result is False
# The external file and the symlink are left intact.
assert external.read_text() == "# external\n"
assert (project / "KIMI.md").is_symlink()
assert not (project / "AGENTS.md").exists()
class TestKimiNextSteps:

View File

@@ -1812,7 +1812,7 @@ class TestIntegrationSwitch:
assert result.exit_code == 0, f"extension add failed: {result.output}"
# Verify git extension skills exist for kimi
kimi_git_feature = project / ".kimi" / "skills" / "speckit-git-feature" / "SKILL.md"
kimi_git_feature = project / ".kimi-code" / "skills" / "speckit-git-feature" / "SKILL.md"
assert kimi_git_feature.exists(), "Git extension skill should exist for kimi"
result = _run_in_project(project, [

View File

@@ -226,17 +226,17 @@ class TestAgentConfigConsistency:
def test_kimi_in_agent_config(self):
"""AGENT_CONFIG should include kimi with correct folder and commands_subdir."""
assert "kimi" in AGENT_CONFIG
assert AGENT_CONFIG["kimi"]["folder"] == ".kimi/"
assert AGENT_CONFIG["kimi"]["folder"] == ".kimi-code/"
assert AGENT_CONFIG["kimi"]["commands_subdir"] == "skills"
assert AGENT_CONFIG["kimi"]["requires_cli"] is True
def test_kimi_in_extension_registrar(self):
"""Extension command registrar should include kimi using .kimi/skills and SKILL.md."""
"""Extension command registrar should include kimi using .kimi-code/skills and SKILL.md."""
cfg = CommandRegistrar.AGENT_CONFIGS
assert "kimi" in cfg
kimi_cfg = cfg["kimi"]
assert kimi_cfg["dir"] == ".kimi/skills"
assert kimi_cfg["dir"] == ".kimi-code/skills"
assert kimi_cfg["extension"] == "/SKILL.md"
def test_agent_config_includes_kimi(self):

View File

@@ -1937,7 +1937,7 @@ Agent __AGENT__
@pytest.mark.parametrize("agent_name,skills_path", [
("codex", ".agents/skills"),
("kimi", ".kimi/skills"),
("kimi", ".kimi-code/skills"),
("claude", ".claude/skills"),
("cursor-agent", ".cursor/skills"),
("trae", ".trae/skills"),

View File

@@ -0,0 +1,41 @@
"""Static checks for repository GitHub Actions workflows."""
from __future__ import annotations
import re
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
WORKFLOWS_DIR = REPO_ROOT / ".github" / "workflows"
# Match both the dedicated-step form (` uses: x@sha`) and the
# inline shorthand (` - uses: x@sha`) used in catalog-assign.yml.
USES_RE = re.compile(r"^\s*(?:-\s*)?uses:\s*(?P<ref>\S+)", re.MULTILINE)
PINNED_SHA_RE = re.compile(r"@[0-9a-f]{40}$", re.IGNORECASE)
def test_github_actions_are_pinned_to_full_commit_shas():
unpinned_refs = []
workflows = sorted(
list(WORKFLOWS_DIR.glob("*.yml")) + list(WORKFLOWS_DIR.glob("*.yaml"))
)
assert workflows
for workflow in workflows:
workflow_text = workflow.read_text(encoding="utf-8")
for match in USES_RE.finditer(workflow_text):
uses_ref = match.group("ref")
if uses_ref.startswith(("./", "../")):
continue
if PINNED_SHA_RE.search(uses_ref):
continue
unpinned_refs.append(f"{workflow.relative_to(REPO_ROOT)}: {uses_ref}")
assert unpinned_refs == []
def test_pinned_action_ref_accepts_uppercase_hex_sha():
assert PINNED_SHA_RE.search(
"actions/example@0123456789ABCDEF0123456789ABCDEF01234567"
)

View File

@@ -3763,12 +3763,16 @@ class TestPresetSkills:
assert note_file.read_text(encoding="utf-8") == "user content"
def test_kimi_legacy_dotted_skill_override_still_applies(self, project_dir, temp_dir):
"""Preset overrides should still target legacy dotted Kimi skill directories."""
"""Preset overrides should still target legacy dotted-named skill dirs.
This exercises legacy *naming* (``speckit.specify``) under the current
``.kimi-code/`` base — distinct from the legacy ``.kimi/`` *location*.
"""
self._write_init_options(project_dir, ai="kimi")
skills_dir = project_dir / ".kimi" / "skills"
skills_dir = project_dir / ".kimi-code" / "skills"
self._create_skill(skills_dir, "speckit.specify", body="untouched")
(project_dir / ".kimi" / "commands").mkdir(parents=True, exist_ok=True)
(project_dir / ".kimi-code" / "commands").mkdir(parents=True, exist_ok=True)
manager = PresetManager(project_dir)
install_self_test_preset(manager)
@@ -3785,10 +3789,10 @@ class TestPresetSkills:
def test_kimi_skill_updated_even_when_ai_skills_disabled(self, project_dir, temp_dir):
"""Kimi presets should still propagate command overrides to existing skills."""
self._write_init_options(project_dir, ai="kimi", ai_skills=False)
skills_dir = project_dir / ".kimi" / "skills"
skills_dir = project_dir / ".kimi-code" / "skills"
self._create_skill(skills_dir, "speckit-specify", body="untouched")
(project_dir / ".kimi" / "commands").mkdir(parents=True, exist_ok=True)
(project_dir / ".kimi-code" / "commands").mkdir(parents=True, exist_ok=True)
manager = PresetManager(project_dir)
install_self_test_preset(manager)
@@ -3805,7 +3809,7 @@ class TestPresetSkills:
def test_kimi_new_skill_created_even_when_ai_skills_disabled(self, project_dir, temp_dir):
"""Kimi native skills should still receive brand-new preset commands."""
self._write_init_options(project_dir, ai="kimi", ai_skills=False)
skills_dir = project_dir / ".kimi" / "skills"
skills_dir = project_dir / ".kimi-code" / "skills"
skills_dir.mkdir(parents=True, exist_ok=True)
preset_dir = temp_dir / "kimi-new-skill"
@@ -3854,9 +3858,9 @@ class TestPresetSkills:
def test_kimi_preset_skill_override_resolves_script_placeholders(self, project_dir, temp_dir):
"""Kimi preset skill overrides should resolve placeholders and rewrite project paths."""
self._write_init_options(project_dir, ai="kimi", ai_skills=False, script="sh")
skills_dir = project_dir / ".kimi" / "skills"
skills_dir = project_dir / ".kimi-code" / "skills"
self._create_skill(skills_dir, "speckit-specify", body="untouched")
(project_dir / ".kimi" / "commands").mkdir(parents=True, exist_ok=True)
(project_dir / ".kimi-code" / "commands").mkdir(parents=True, exist_ok=True)
preset_dir = temp_dir / "kimi-placeholder-override"
preset_dir.mkdir()

View File

@@ -268,6 +268,24 @@ class TestExpressions:
ctx = StepContext(inputs={"a": False, "b": True})
assert evaluate_expression("{{ inputs.a or inputs.b }}", ctx) is True
def test_list_literal_preserves_quoted_commas(self):
from specify_cli.workflows.expressions import evaluate_expression
from specify_cli.workflows.base import StepContext
ctx = StepContext()
# commas inside a double-quoted element must not split it
assert evaluate_expression('{{ ["a, b", "c"] }}', ctx) == ["a, b", "c"]
assert evaluate_expression('{{ ["x, y, z"] }}', ctx) == ["x, y, z"]
# single-quoted elements are handled the same way
assert evaluate_expression("{{ ['a, b', 'c'] }}", ctx) == ["a, b", "c"]
assert evaluate_expression("{{ ['p, q, r'] }}", ctx) == ["p, q, r"]
# plain and empty lists still parse correctly
assert evaluate_expression("{{ [1, 2, 3] }}", ctx) == [1, 2, 3]
assert evaluate_expression("{{ [] }}", ctx) == []
# nested lists (commas inside the inner brackets) stay intact
assert evaluate_expression('{{ [["a", "b"], "c"] }}', ctx) == [["a", "b"], "c"]
assert evaluate_expression("{{ [[1, 2], [3, 4]] }}", ctx) == [[1, 2], [3, 4]]
def test_filter_default(self):
from specify_cli.workflows.expressions import evaluate_expression
from specify_cli.workflows.base import StepContext