Commit Graph

1343 Commits

Author SHA1 Message Date
Manfred Riem
a7b439174f chore: release 0.12.8, begin 0.12.9.dev0 development (#3410)
* chore: bump version to 0.12.8

* chore: begin 0.12.9.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-08 09:39:56 -05:00
github-actions[bot]
0da969df14 [extension] Add LLM Wiki extension to community catalog (#3361)
* Add LLM Wiki extension to community catalog

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

Closes #3319

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

* fix: limit catalog.community.json changes to wiki entry + timestamps only

Reverts the unintended reordering and reformatting of existing extensions
(aide, checkpoint, critique, threatmodel, etc.) and companion's tools array.
Only the new wiki entry and updated_at timestamps are now changed.

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-07-08 08:29:02 -05:00
Dyan Galih
13d2cca154 Docs: Document missing CLI flags and integrations (#3182)
* docs: document missing flags and integrations

* docs: remove invalid --refresh-shared-infra from upgrade command

* docs: address PR feedback for extension and integration flags

* docs: reorder extension add options to match CLI help
2026-07-08 07:49:42 -05:00
Dyan Galih
ba1ce366b7 Docs: Remove Cursor from CLI check list in README (#3184)
* docs: reword CLI check behavior to remove exhaustive list of tools

* docs: clarify conditional CLI tool installation check
2026-07-08 07:48:08 -05:00
Marsel Safin
295eb221e3 feat(extensions): port update-agent-context to Python (#3387)
* feat(extensions): port update-agent-context to Python

Ports the agent-context extension updater to a single Python script,
per #3281 and the check-prerequisites PoC pattern from #3302. The bash
version already ran its core logic through embedded Python heredocs, so
the port lifts that logic into a standalone script. Parity tests run
bash and Python side by side and compare output and resulting
context-file bytes.

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

* fix(extensions): match bash case-insensitivity on MSYS, test unparseable config gate

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-08 07:46:29 -05:00
Quratulain-bilal
5a901a698b fix(scripts): fall through to grep/sed when python3 is a broken stub in feature.json parser (#3312)
* fix(scripts): fall through to grep/sed when python3 is a broken stub in feature.json parser

read_feature_json_feature_directory picked its json parser by availability
(if jq / elif python3 / else grep-sed). on windows `python3` usually resolves
to the microsoft store app execution alias stub: it satisfies `command -v` but
fails at runtime (exit 49). the elif selected it, the runtime failure was
swallowed to _fd='', and the grep/sed last resort was never reached - so a
valid .specify/feature.json read as empty and every setup-plan / setup-tasks /
check-prerequisites call errored with "Feature directory not found" right after
a successful `specify init --script sh`.

change selection from availability to parse success: try jq, then python3 only
if still empty, then grep/sed only if still empty. a parser that exists but
produces nothing now falls through instead of terminating the chain.

the write path (_persist_feature_json) already uses jq-or-printf with no
python3, so it was unaffected; only the read path needed this.

add a regression test that puts a python3 stub (exit 49, like the store alias)
first on PATH and asserts setup-plan.sh still resolves the feature via the
grep/sed fallback.

fixes #3304

* test: shadow jq so the broken-python3 fallback test actually exercises it

the test claimed it dropped jq so the parser chain would reach python3 and
then grep/sed, but it only prepended the python3 stub dir to PATH. on a
runner with jq installed, read_feature_json_feature_directory parses via jq
and never reaches the fallback the test is meant to cover.

add a failing jq stub alongside the python3 stub so the chain is forced
through jq -> python3 -> grep/sed regardless of what the runner has installed.
2026-07-08 07:45:01 -05:00
Quratulain-bilal
94c7ec288f fix(toml): escape control characters so generated command files parse (#3341)
* fix(toml): escape control characters so generated command files parse

both toml renderers (TomlIntegration._render_toml_string for gemini/tabnine
and CommandRegistrar.render_toml_command for extension/preset commands) wrote
control characters raw into a multiline or basic string. toml forbids literal
control chars (U+0000-U+001F except tab/newline, and U+007F) in every string
form, and a bare CR that is not part of a CRLF pair, so a description or body
containing one produced a .toml file that fails to parse.

route any value with such a character to a fully-escaped basic string that
emits the leftover control chars as \uXXXX. added regression tests that
round-trip through tomllib.

* refactor(toml): centralize control-char escaping in one shared helper

the control-char detection and basic-string escaping added for both toml
renderers were copy-pasted into agents.py and integrations/base.py. move the
two functions into specify_cli/_toml_string.py and have both renderers
delegate to it, so the escaping rules can't drift apart later.

no behavior change; both renderers now reference the same implementation.
2026-07-08 07:42:34 -05:00
Marsel Safin
882e1e90d0 fix(cli): exit cleanly on malformed IPv6 URLs in extension/preset/workflow add (#3369)
* fix(cli): exit cleanly on malformed IPv6 URLs in extension/preset/workflow add

extension add --from, preset add --from, and workflow add <url> parsed
the user-supplied URL with a bare urlparse before their HTTPS/host
validation, so an unclosed IPv6 bracket escaped as a raw ValueError
traceback. Wrap each parse and emit the surrounding validation's clean
error style + typer.Exit(1) instead.

Fixes #3368

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

* fix(cli): convert malformed redirect URLs to URLError in shared redirect handler

Parse the redirect target once in _StripAuthOnRedirect.redirect_request
before the validator and stdlib handler run, converting ValueError into
URLError which every download path already catches. Also escape from_url
in the preset install message so IPv6 brackets don't break Rich markup.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-07 15:20:13 -05:00
Ali jawwad
a307894709 fix(github-http): return None on malformed GHES port instead of raising (#3379)
resolve_github_release_asset_api_url's is_ghes branch built the authority
with 'parsed.port', which raises ValueError on a malformed port (e.g.
host:notaport). The function's contract is to resolve or return None,
never raise — every other unresolvable case returns None. An allowlisted
GHES host with a bad port therefore crashed the caller. Read parsed.port
defensively and return None on ValueError.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 15:14:25 -05:00
Ali jawwad
10d4bca64c fix(integrations): guard _sha256 against unreadable managed files (#3376)
manifest.py::_sha256 does an unguarded open(). check_modified() and
uninstall() both call it on a readable-but-unopenable regular file
(e.g. permission denied) without catching OSError, so
'specify integration upgrade/uninstall/switch' surface a raw
PermissionError traceback. Guard both call sites: in check_modified()
treat an unreadable file as modified (consistent with the adjacent
symlink / non-regular-file handling); in uninstall() treat it as skipped
and preserve it (mirroring the existing path.unlink() OSError guard just
below). The force short-circuit is unchanged.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 15:11:18 -05:00
Manfred Riem
f1a8d8f95b chore: release 0.12.7, begin 0.12.8.dev0 development (#3398)
* chore: bump version to 0.12.7

* chore: begin 0.12.8.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-07 15:01:43 -05:00
Ali jawwad
d5ba062eab fix(bundler): bundle update uninstalls components dropped by new version (#3353)
On refresh (bundle update), install_bundle iterated only the new plan's
components, so a component the previous version owned but the new one no
longer ships was left installed on disk while being dropped from the
rewritten record (contributed only holds plan.components). With no
record referencing it, remove_bundle could never clean it up —
permanently orphaned, violating the provenance invariant (FR-022). After
the component loop, when refresh and a prior record exists, uninstall
each previously-owned component absent from the new plan — unless another
bundle still needs it (components_still_needed refcount, mirroring
remove_bundle), in which case it stays installed and is simply
de-attributed. Runs inside the existing try so a failed removal takes the
same no-record-written rollback path.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:59:27 -05:00
Ali jawwad
12faf7b5b5 fix(workflows): route run/resume errors to stderr under --json (#3352)
workflow run/resume --json is contracted to emit a single JSON object on
stdout, but every error path (workflow-not-found, invalid workflow,
validation failure, execute/resume failure, and the shared
_parse_input_values invalid-input error) used console.print, landing the
human error text on stdout and corrupting the machine-readable stream.
Route those messages through err_console when --json is set (a no-op for
normal text mode), mirroring the stderr-only error routing already used
by 'specify bundle' (_fail) and err_console elsewhere in this module.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:52:36 -05:00
Ali jawwad
220e6fcc4e fix(workflows): fan-in validate() rejects non-mapping output (#3349)
FanInStep.validate() only checked wait_for, so a non-mapping 'output'
(a list or scalar) validated clean; execute() then silently coerces it
to {}, so the author's declared aggregation keys vanish with COMPLETED
status and no diagnostic. Reject a non-mapping output at validation,
mirroring the command-step (#3262) non-mapping fix. execute()'s
defensive coercion is left in place for unvalidated callers.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:49:29 -05:00
Ali jawwad
fb796c2a39 fix(workflows): shell step validate() rejects non-string run (#3348)
ShellStep.validate() only checked that 'run' was present, so run: (null)
or a GitHub-Actions-style list validated clean; execute() then
str()-coerces the value and invokes it under shell=True, literally
running 'None' or "['echo', 'hi']" as a command. Add a type check after
the presence check, mirroring the command-step (#3262) and gate options
validation. Expression strings ('{{ ... }}') are strings, so they stay
valid.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:48:26 -05:00
Ali jawwad
0151d239b5 fix(integrations): agy honors SPECKIT_INTEGRATION_AGY_EXTRA_ARGS (#3347)
AgyIntegration.build_exec_args returned [exe, '--print', prompt] without
calling _apply_extra_args_env_var(), so the documented per-integration
extra-args env hook was silently dropped for agy — same class as the
cursor-agent fix #3265. Append the hook after the positional prompt,
matching the devin integration's shape. agy still ignores model/output
as before.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:38:36 -05:00
github-actions[bot]
f764270d06 Add Orchestration Task Context Management extension to community catalog (#3372)
Add orchestration-task-context-management extension submitted by @benizzio to:
- extensions/catalog.community.json (alphabetical order)
- docs/community/extensions.md community extensions table

Closes #3356

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-07-07 12:56:34 -05:00
github-actions[bot]
7839acce86 Update DocGuard — CDD Enforcement extension to v0.30.0 (#3371)
Update docguard extension submitted by @raccioly:
- extensions/catalog.community.json (version, download_url, description, updated_at)
- docs/community/extensions.md community extensions table

Closes #3355

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-07-07 12:30:29 -05:00
github-actions[bot]
1935cf7e48 Update Ripple extension to v1.1.0 (#3370)
Update ripple extension submitted by @chordpli:
- extensions/catalog.community.json (version, download_url, description, requires.tools)
- docs/community/extensions.md community extensions table

Closes #3354

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-07-07 12:25:54 -05:00
Roland Huss
d4e7d2b888 feat(integrations): generalize post-processing to all format types (#3311)
* feat(integrations): add post_process_command_content() hook for all format types

Add post_process_command_content(self, content: str) -> str to IntegrationBase
with a no-op default. Wire it into register_commands() for non-skills format
types (Markdown, TOML, YAML) after format rendering, before writing to disk.
Also applies to aliases rendered via the inject_name path (cline, forge).

Skills-format agents are excluded to preserve the existing
post_process_skill_content() path and avoid double-processing.

This gives extension authors a clean per-agent content transformation seam
for all 21 non-skills integrations that previously had no post-processing hook.

Ref: #3303

Assisted-By: 🤖 Claude Code

* fix: initialize _integration before conditional branch

Prevents potential UnboundLocalError if the non-skills guard is
refactored without updating the alias path reference.

Assisted-By: 🤖 Claude Code
2026-07-07 11:35:43 -05:00
Manfred Riem
abaed10d00 chore: release 0.12.6, begin 0.12.7.dev0 development (#3393)
* chore: bump version to 0.12.6

* chore: begin 0.12.7.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-07 11:05:13 -05:00
Marsel Safin
2b5e175440 fix(bundler): validate catalog URLs in catalog add (HTTPS-only, require host) (#3367)
* fix(bundler): validate catalog URLs in `catalog add` (HTTPS-only, require host)

add_source persisted remote catalog URLs without the HTTPS/host checks
that specify_cli.catalogs (#3210) and the bundler adapters (#3333)
enforce, and an unclosed IPv6 bracket escaped as a raw ValueError.
Mirror the catalogs.py validation for http(s) schemes and wrap urlparse
so malformed input raises BundlerError.

Fixes #3366

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

* docs: correct config filename and validation reference in comment

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-07 11:01:46 -05:00
github-actions[bot]
0e40438903 Update Ralph Loop extension to v1.2.1 (#3365)
Update ralph extension submitted by @Rubiss:
- extensions/catalog.community.json (version, download_url, speckit_version, tools, tags, updated_at)

Closes #3337

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-07-07 10:55:18 -05:00
Zhiyao Wen
1930f89d17 fix extension-local script path rewriting (#3364)
Co-authored-by: Zhiyao <zhiyao@ZhiyaodeMacBook-Air.local>
2026-07-07 10:51:50 -05:00
github-actions[bot]
4bb5166445 Add Charter extension to community catalog (#3363)
Add charter extension submitted by @Huljo to:
- extensions/catalog.community.json (alphabetical order)
- docs/community/extensions.md community extensions table

Closes #3322

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-07-07 09:53:05 -05:00
WOLIKIMCHENG
73f77c200f feat(scripts): add Python check-prerequisites PoC (#3302)
* feat(scripts): add Python check-prerequisites PoC

* fix(scripts): address check-prerequisites parity feedback

* test(scripts): label PowerShell prerequisite parity cases

---------

Co-authored-by: root <kinsonnee@gmail.com>
2026-07-06 17:54:31 -05:00
Pascal THUET
b8d27e472f test: reduce registry manifest test repetition (#3146)
* test: isolate integration test home

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

* test: reduce registry manifest test repetition

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

* test: clarify disjoint-manifest order rationale and guard safe set

Add a >=2 precondition, explain why two install orders are tested
(manifests are order-independent; the orders only vary the init path),
and build the manifest map with a comprehension.

* test: rotate init coverage for manifest isolation

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

* test: assert integration home isolation

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

* test: guard multi-install manifest rotations

Assisted-by: Codex (model: GPT-5, autonomous)
2026-07-06 17:50:33 -05:00
Ali jawwad
587b1859fa fix(integrations): hermes honors SPECKIT_INTEGRATION_HERMES_EXTRA_ARGS (#3346)
HermesIntegration.build_exec_args routed argv[0] through
_resolve_executable() but never called _apply_extra_args_env_var(), so
the documented per-integration extra-args env hook was silently dropped
for hermes — the same class of bug fixed for cursor-agent in #3265.
Insert the hook after the base 'chat -Q' command and before Spec Kit's
canonical -m/--json/-s/-q flags (mirrors opencode), so operator args
can't displace or clobber the canonical flags.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 17:38:39 -05:00
Ali jawwad
52480ee50f fix(extensions): coerce non-mapping YAML config roots to {} in ConfigManager (#3345)
ConfigManager._load_yaml_config returned yaml.safe_load(...) or {}, which
only guards falsy roots — a truthy non-mapping root (a YAML list or
scalar) flows straight into _merge_configs, whose .items() raises
AttributeError. get_config()/has_value()/get_value() then crash, and via
should_execute_hook's blanket 'except Exception: return False' every
config-based hook condition for that extension is silently disabled.
Coerce a non-dict root to {}, mirroring the existing non-dict-root guard
in get_project_config(). Hardens all three call sites in one place.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 17:36:35 -05:00
Quratulain-bilal
d3e7b06fa7 fix(yaml): pin goose recipe prompt block-scalar indentation (#3343)
the goose recipe renderer emitted the prompt body under a bare '|' block
scalar. yaml infers a plain block scalar's indentation from its first
non-empty line, so a command body whose first line is itself indented (a
markdown code block, a nested list item) made the parser expect that deeper
indent for the whole block and reject the later, shallower lines - the
generated .goose recipe then failed to parse.

use an explicit '|2' indentation indicator so the block is always read at
2 spaces regardless of the body. added a regression test that round-trips
an indented-first-line body through the yaml parser.
2026-07-06 17:34:16 -05:00
Manfred Riem
de6a04eaad chore: release 0.12.5, begin 0.12.6.dev0 development (#3381)
* chore: bump version to 0.12.5

* chore: begin 0.12.6.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-06 16:51:18 -05:00
Noor ul ain
5217206fdf fix(workflows): match gate reject option case-insensitively (#3335)
`validate` accepts a reject option case-insensitively
(`o.lower() in {"reject", "abort"}`), so a gate authored as
`options: [Approve, Reject]` passes validation. But `execute`
compared the echoed choice case-sensitively, so picking `Reject`
fell through to the approval path and silently ran downstream
steps instead of aborting.

Lower-case `choice` before the reject comparison so the runtime
agrees with the validation that let the option through.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 16:48:33 -05:00
Quratulain-bilal
c978faac57 fix(bundler): reject host-less catalog URLs in adapters (use hostname, not netloc) (#3333)
_validate_remote_url in bundler/services/adapters.py guarded on parsed.netloc,
which is truthy for host-less URLs like "https://:8080" or "https://user@" even
though they carry no host. so those passed the "must be a valid URL with a host"
check. its docstring says it mirrors specify_cli.catalogs validation, but that
site was already fixed to use hostname in #3210/#3227 and this twin was missed.

switch to parsed.hostname (None for host-less URLs), matching catalogs.py. this
guard runs before any network call, so it is a pre-flight safety check.

add parametrized regression tests for the host-less forms plus a valid
host+port sanity case.
2026-07-06 16:44:08 -05:00
Quratulain-bilal
b5f1194168 fix(bundler): resolve catalog search at highest-precedence source before filtering (#3331)
CatalogStack.search() claimed a bundle id in `seen` only when the entry matched
the query. so when the highest-precedence entry for an id did NOT match, a
lower-precedence entry with the same id could match and be returned instead --
even though resolve()/install always use the highest-precedence entry. search
advertised a bundle (name, version, source) the user could never actually get,
contradicting the method's own docstring ("resolved at its highest-precedence
source").

resolve every id to its highest-precedence entry first, then filter the
resolved set by the query. search now agrees with resolve(): a query that only
a shadowed lower-precedence copy matches returns nothing.

add a regression test covering the shadowed-entry case.
2026-07-06 16:42:45 -05:00
Quratulain-bilal
44c112c807 fix(workflows): compare non-numeric strings lexicographically instead of returning False (#3323)
_safe_compare coerced both operands to int/float unconditionally for <, >, <=,
>=. any non-numeric string (an iso date, a version tag, a name) failed that
coercion and the whole comparison silently returned False -- so
`{{ inputs.d < '2026-02-01' }}` was False even when the date was earlier.

only coerce when both operands look numeric; otherwise compare the original
values, so two strings order lexicographically the way python does and two
numeric strings still compare as numbers ("10" > "9"). a number vs a
non-numeric string stays incomparable and yields False.

add a regression test covering dates, plain strings, numeric strings, and the
number-vs-string case.
2026-07-06 16:20:45 -05:00
Quratulain-bilal
3b4e7f3cb6 fix(workflows): quote-aware interpolation so a literal }} in a filter arg doesn't break multi-expression templates (#3307)
* fix(workflows): quote-aware interpolation so a literal }} in a filter arg doesn't break multi-expression templates

#3208/#3228 hardened the single-expression fast path (_is_single_expression)
so a literal {{ or }} inside a string argument like `| default('}}')` stays on
the typed path. the multi-expression interpolation path was left on the old
_EXPR_PATTERN regex, whose non-greedy `(.+?)}}` body stops at the first }}
regardless of quoting. so a multi-expression template with a literal }} in any
block captured a truncated body, hit the filter parser malformed, and raised
ValueError.

e.g. `{{ inputs.name }}: {{ inputs.missing | default('}}') }}` raised instead
of interpolating.

replace _EXPR_PATTERN.sub with _interpolate_expressions, which scans each block
for a }} outside string literals - the same quote handling _is_single_expression
already uses. plain-value passthrough (a literal }} in a resolved value, not an
expression) is unchanged.

add regression tests for a literal }} in the second block and in the first
block, plus a literal {{ guard.

* fix(workflows): surface malformed templates in interpolation instead of emitting verbatim

address copilot review on #3307: when the quote-aware scan finds no block-closing
`}}` (e.g. an unbalanced quote in a filter arg swallowed the delimiter), fall back
to the first raw `}}` in the tail and evaluate it, so the parser raises ValueError
just as the old _EXPR_PATTERN.sub path did. only when there is no `}}` at all is
the tail left verbatim (a genuinely unterminated `{{`, which the regex also could
not match). keeps a typo failing loudly rather than being silently hidden.

add a regression test for an unbalanced quote in a multi-expression template.
2026-07-06 15:46:35 -05:00
Pascal THUET
f494a8e33e Support namespaced git feature branch templates (#3293)
* test: cover namespaced git branch templates

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

* feat: support namespaced git branch templates

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

* test: cover git branch template edge cases

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

* fix: harden git branch template parsing

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

* fix: address git branch template review feedback

Address Copilot review feedback for branch_prefix help text, namespaced GIT_BRANCH_NAME fallback behavior, final-segment validation docs, and Bash UTF-8 byte reporting.

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

* fix: reject slug-scoped branch templates

Reject branch templates that place {slug} before {number}, because that makes namespace scanning depend on the generated feature slug and can reset numbering per feature name.

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

* fix: ignore malformed timestamp refs when numbering

Align branch-number scanning with feature-branch validation so malformed timestamp-looking refs do not inflate sequential numbering. Also updates the stale git-common comment called out in review.

Assisted-by: Codex (model: GPT-5, autonomous)
2026-07-06 15:41:58 -05:00
dependabot[bot]
92b7cf7658 chore(deps): bump actions/setup-dotnet from 5.3.0 to 5.4.0 (#3315)
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 5.3.0 to 5.4.0.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](9a946fdbd5...26b0ec14cb)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-version: 5.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-06 08:41:12 -05:00
Ali jawwad
bba473c223 fix(integrations): cursor-agent honors executable/extra-args env overrides (#3265)
* fix(integrations): cursor-agent ignores executable/extra-args env overrides

cursor-agent's build_exec_args() hardcoded self.key as argv[0] and never
called _apply_extra_args_env_var(), so the documented
SPECKIT_INTEGRATION_CURSOR_AGENT_EXECUTABLE (issue #2596) and
SPECKIT_INTEGRATION_CURSOR_AGENT_EXTRA_ARGS (issue #2595) hooks were
silently dropped — unlike every other CLI-dispatch integration (codex,
devin). Route argv[0] through _resolve_executable() and apply the
extra-args hook after the mandatory headless flags, mirroring the twins.

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

* test(integrations): pin extra-args insertion order for cursor-agent

Per Copilot feedback: the extra-args override test only asserted the
injected tokens were present, not that they land before Spec Kit's
canonical --model / --output-format flags. Exercise build_exec_args with
both a model and JSON output and assert the extra args are inserted
before --model / --output-format (and the canonical flags stay intact and
paired). Verified this fails if the _apply_extra_args_env_var call is
moved after the flag extends.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 08:49:53 -05:00
Quratulain-bilal
288bd679f3 docs: drop stale kimi KIMI.md->AGENTS.md migration note (#3291)
* docs: drop stale kimi KIMI.md->AGENTS.md migration note

#3097 made the agent-context extension a full opt-in and removed the
KIMI.md -> AGENTS.md context migration from the kimi integration
(_migrate_legacy_kimi_context_file and the context_file handling are
gone). kimi's --migrate-legacy now only moves the skills directory. two
lines in the integrations reference still promised the removed context
migration; drop that clause so the docs match the code.

* docs: clarify kimi legacy migration is skill naming, not directory names

address review: the parenthetical said 'dotted->hyphenated directory
names', but the migration is about skill naming (speckit.xxx ->
speckit-xxx), matching the module docstring. reword to match.
2026-07-02 08:40:30 -05:00
Manfred Riem
9bd3512025 chore: release 0.12.4, begin 0.12.5.dev0 development (#3305)
* chore: bump version to 0.12.4

* chore: begin 0.12.5.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-02 05:57:50 -05:00
Manfred Riem
bbe86310ca feat(cli): add py script type & Python interpreter resolution (#3278) (#3285)
* feat(cli): add `py` script type & Python interpreter resolution (#3278)

Introduce a third script variant alongside `sh`/`ps` as the foundation
for unifying workflow scripts under a single Python implementation.

- Add `"py": "Python"` to `SCRIPT_TYPE_CHOICES`; `VALID_SCRIPT_TYPES`
  consumers (init workflow step, init command, _helpers) pick it up
  automatically since they derive from that mapping.
- Add `IntegrationBase.resolve_python_interpreter()` (project venv →
  `python3` → `python`, falling back to `python3`).
- Prefix the resolved interpreter when `process_template()` expands
  `{SCRIPT}` for the `py` script type so `.py` scripts run portably
  (notably on Windows); thread `project_root` through callers so venv
  preference works.
- Make `install_scripts()` mark copied `.py` files executable too.

Includes positive and negative unit tests for interpreter resolution,
`py` template processing, the new choice, and script installation.

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

* fix(cli): return repo-relative venv interpreter & correct docstring

Address PR review feedback on #3285:

- `resolve_python_interpreter()` now returns the venv interpreter as a
  path relative to the project root (`.venv/bin/python` /
  `.venv/Scripts/python.exe`) instead of an absolute/joined path, so the
  generated `{SCRIPT}` invocation stays portable and runnable from the
  repo root regardless of where the project lives.
- Update `install_scripts()` docstring to note `.py` scripts are now
  made executable alongside `.sh`.
- Update tests to assert the repo-relative interpreter path.

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

* fix(cli): fall back to sys.executable for interpreter resolution

When neither python3 nor python is discoverable on PATH (and no project
venv is found), resolve_python_interpreter() now returns the running
interpreter (sys.executable) so the generated {SCRIPT} invocation works
in the current environment, falling back to "python3" only if that is
also unavailable. Update unit tests accordingly.

* fix(cli): quote py interpreter path when it contains whitespace

For the `py` script type, the resolved interpreter may be an absolute
path containing spaces (notably `sys.executable` under Windows
`Program Files`). Quote it when it contains whitespace so the `{SCRIPT}`
invocation isn't split into multiple arguments. Add positive/negative
tests for the quoting behavior.

* test: guard executable-bit assertions from Windows chmod semantics

The Windows CI job failed because `os.chmod` does not set POSIX
executable bits on Windows, so `install_scripts()` cannot make `.py`/
`.sh` files executable there (nor is it needed — the interpreter is
invoked explicitly). Split the install_scripts test so file-copy
behavior is still verified cross-platform, and skip the executable-bit
assertions on win32 (matching the repo's existing pattern).

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-01 16:34:46 -05:00
lselvar
3b30e40aaa fix: resolve GitHub release asset API URL for private repo bundle downloads (#3136)
* fix: resolve GitHub release asset API URL for private repo bundle downloads

For private/SSO-protected GitHub repos, browser release download URLs
(https://github.com/<owner>/<repo>/releases/download/<tag>/<asset>)
redirect to an HTML/SSO page instead of delivering the asset, causing
bundle manifest downloads to fail.

Extends the pattern from #2855 (presets/workflows) to cover the bundle
manifest download path in _download_remote_manifest:

- Resolves browser release URLs to GitHub REST API asset URLs via
  resolve_github_release_asset_api_url before downloading
- Direct REST API asset URLs (api.github.com/repos/.../releases/assets/<id>)
  are passed through directly
- Both cases use Accept: application/octet-stream so the API returns the
  binary payload rather than JSON metadata
- The original catalog URL is used to determine artifact format (.zip vs
  YAML) since the resolved API URL does not carry the file extension

Adds two CLI-level contract tests:
- bundle info resolves browser release URL via GitHub tags API
- bundle info passes direct API asset URL through with octet-stream

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: detect ZIP payload by magic bytes; add zip and API-asset tests

Address Copilot review feedback on PR #3136:

1. Detect ZIP payloads by magic bytes (PK\x03\x04) in addition to the
   '.zip' URL suffix so that direct GitHub REST asset URLs — which carry
   no file extension — are correctly routed through the ZIP extraction
   path when the asset is a ZIP bundle artifact.

2. Add two new contract tests:
   - test_bundle_info_resolves_github_browser_release_url_zip: exercises
     the '.zip' browser release URL path end-to-end, verifying the tags
     API lookup fires, octet-stream header is used, and bundle.yml is
     successfully extracted from the ZIP payload.
   - test_bundle_info_api_asset_url_zip_detected_by_magic_bytes: verifies
     that a direct REST asset URL returning ZIP bytes is detected by magic
     and parsed correctly without a tags API call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: improve error message, broaden ZIP magic, drop unused tmp_path

Address second-round Copilot review feedback on PR #3136:

- Error message: when the download fails, report the original catalog
  download_url so the user knows which entry to fix; include the resolved
  REST API URL when it differs for easier debugging.
- ZIP detection: broaden the magic-bytes check from PK\x03\x04 to raw[:2]
  == b"PK", covering all valid ZIP variants (local-file header PK\x03\x04,
  empty-archive PK\x05\x06, spanned/split PK\x07\x08).
- Tests: remove the unused tmp_path parameter from
  test_bundle_info_resolves_github_browser_release_url_zip.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: use full 4-byte ZIP signatures instead of 2-byte PK prefix

Address Copilot feedback: raw[:2] == b"PK" is too broad and could
misclassify any payload starting with ASCII "PK" as a ZIP, producing
a confusing "not a valid bundle" error.

Use the three specific 4-byte ZIP magic signatures instead:
  PK\x03\x04 — local file header (standard ZIP)
  PK\x05\x06 — end-of-central-directory (empty archive)
  PK\x07\x08 — data descriptor / spanning marker

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: harden _download_remote_manifest parsing and tighten tests

- Promote _ZIP_SIGNATURES to module-level constant (was redefined per call)
- Use PurePosixPath for URL path suffix extraction so query strings and
  fragments are ignored and URL paths are treated as POSIX on all OSes
- Move yaml/BundleManifest imports to function top to flatten the
  previously nested try/except into a single handler with explicit
  except _yaml.YAMLError and except Exception clauses
- Re-add None guard on _local_manifest_source return: the function is
  typed Optional[BundleManifest] and without the guard a None return
  propagates silently to callers that degrade gracefully rather than
  raising an actionable error; comment explains it is defensive not dead
- Assert exact resolved asset URL in browser-URL download tests, not
  just the Accept header, so a regression where download uses the
  original URL instead of the resolved one would be caught
- Add resolution-failure test: when tags API finds no matching asset the
  code falls back to the original URL and exits non-zero with Error:

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(bundle): pass github_provider_hosts() for GHES private release downloads

Extends the GHES support pattern from extensions and presets (#2855, #3157)
to the bundle manifest download path: resolve_github_release_asset_api_url
now receives github_hosts=github_provider_hosts() so browser release URLs
from GitHub Enterprise Server instances are resolved via /api/v3 rather
than falling back to the unauthenticated download path.

Also adds a contract test covering the GHES resolution path for
_download_remote_manifest (analogous to the existing github.com tests).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(bundle): remove unused ghes_entry variable from GHES contract test

The dict was defined but never consumed — the test drives GHES host
recognition entirely through the github_provider_hosts() patch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(bundle): include source URL in remote manifest parse errors

Thread the catalog URL (and resolved API URL when it differs) into the
YAML parse, generic parse, and ZIP-extraction error paths of
_download_remote_manifest so failures point at the offending source
instead of an opaque temp path. Addresses PR review feedback.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-01 16:30:20 -05:00
github-actions[bot]
6288dea6ae [extension] Add Analytics extension to community catalog (#3296)
* Add Analytics extension to community catalog

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

Closes #3288

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

* Fix empty changelog field for analytics extension

Set the analytics extension changelog to the GitHub releases page instead of
an empty string, which the catalog treats as a URI when present and can fail
schema validation and downstream tooling.

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

---------

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: Manfred Riem <15701806+mnriem@users.noreply.github.com>
2026-07-01 16:16:21 -05:00
Noor ul ain
5b682b2cb3 fix: interpolate multi-expression templates instead of returning None (#3208) (#3228)
* fix: interpolate multi-expression templates instead of returning None (#3208)

`evaluate_expression` returned None for templates containing two or more
`{{ }}` blocks with no surrounding literal text, e.g.
`"{{ context.run_id }} {{ inputs.issue }}"`.

The single-expression fast path used `_EXPR_PATTERN.fullmatch()`, but
`fullmatch` defeats the pattern's non-greedy `(.+?)` body: for two adjacent
expressions it still matches, capturing everything between the first `{{`
and the last `}}` (`"context.run_id }} {{ inputs.issue"`) as the body. That
garbage failed dot-path resolution and returned None directly, bypassing the
`sub()` interpolation path that would have resolved each expression. Downstream
this surfaced as the literal string "None" reaching commands.

Guard the fast path on `stripped.count("{{") == 1` so only genuine
single-expression templates take the typed return; multi-expression templates
fall through to `sub()` and interpolate correctly.

Add regression tests for two expressions separated by a space and for adjacent
expressions with no separator.

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

* Potential fix for pull request finding

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

* Potential fix for pull request finding

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

* fix(expressions): use match-span guard so single expressions with literal {{ keep their type

The previous `stripped.count("{{") == 1` guard misclassified a genuine
single expression whose string argument contains a literal `{{` (e.g.
`{{ inputs.text | contains('{{') }}`) as multi-expression, routing it
through `sub()` interpolation and coercing the typed (bool/int/list)
return value to a string -- breaking the type-preservation the docstring
promises (Copilot review on #3228).

Anchor a single match at the start and require it to consume the whole
stripped string instead. The non-greedy body stops at the first `}}`, so
a two-block template fails the span check (falls through to interpolation,
fixing #3208) while a lone expression -- including one with a `{{` inside
a string literal -- matches to the end and keeps its typed value.

Add a regression test for the literal-brace single-expression case.

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

* Potential fix for pull request finding

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

* fix(expressions): detect single expression with quote-aware scan

The match-span guard using the non-greedy _EXPR_PATTERN stopped at the
first `}}`, so a lone expression whose string argument contains a literal
`}}` (e.g. `{{ inputs.text | contains('}}') }}`) was misclassified as
multi-expression and mis-parsed by the interpolation path, raising
ValueError and turning CI red (Copilot review on #3228).

Replace the span check with `_is_single_expression`, which scans the
`{{ ... }}` body for a block-closing `}}` outside string literals (mirrors
the quote handling already in `_split_top_level_commas`). A genuine
two-block template closes early and falls through to interpolation
(fixing #3208); a lone expression with a literal `{{` or `}}` inside a
string argument keeps its typed return value.

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

* Potential fix for pull request finding

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-01 16:05:50 -05:00
Pascal THUET
490566847c feat(cli): honor SPECIFY_INIT_DIR in the specify CLI project resolver (#3186)
* feat(cli): honor SPECIFY_INIT_DIR in the specify CLI project resolver

The shell resolver honors SPECIFY_INIT_DIR (#2892), but the Python CLI did
not: it resolved the project as Path.cwd() + a .specify/ check and never read
the override. So setup-plan.sh respected it while `specify integration install`
ignored it, and you still had to cd into the member project.

Route project resolution through a shared _resolve_init_dir_override() that
applies the shell resolver's validation rules (relative to cwd, must exist and
contain .specify/, hard error, no fallback, same error strings). It's wired into
_require_specify_project() — the chokepoint for every project-scoped subcommand
(integration/extension/workflow/preset/...) — and the `workflow run <file>`
standalone path, which re-applies its symlinked-.specify guard on the override
branch too. init is unchanged: it creates .specify/, so the must-pre-exist rule
doesn't apply.

The resolver canonicalizes symlinks via Path.resolve() while the shell keeps the
logical path; they agree for non-symlinked paths (documented in the resolver).

Tests in tests/test_init_dir_cli.py mirror the strict cases from test_init_dir.py
through the CLI; conftest now strips SPECIFY_* for the whole suite so a stray
export can't perturb the now-env-reading resolver. Docs note the CLI applies the
same rules.

Discussion: github/spec-kit#2834

(Disclosure: I used an AI coding agent to audit the call sites and resolver,
draft the change, and run an adversarial code review; reviewed by me.)

* fix(cli): honor SPECIFY_INIT_DIR for bundle commands

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

* fix(bundler): refuse symlinked .specify on the SPECIFY_INIT_DIR override path

find_project_root refuses a symlinked .specify (following it could read/write
outside the tree, and a test pins that), but the SPECIFY_INIT_DIR override added
for bundle commands returned early and skipped that guard:
_resolve_init_dir_override validates .specify with is_dir(), which follows
symlinks. So `specify bundle` accepted via the override a layout the cwd path
rejects. Re-check the override result with the same guard, plus a regression test.

(Disclosure: found via an AI code review and fixed with an AI coding agent;
reviewed by me.)

* fix(cli): keep SPECIFY_INIT_DIR strict for bundles

Treat an explicit symlinked SPECIFY_INIT_DIR project as a hard bundle error instead of returning no project, which could initialize the current directory. Align the docs with the actual unset resolver behavior.

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

* docs(core): note symlinked .specify handling differs across CLI surfaces

A symlinked .specify is followed by integration/extension/workflow (matching the
shell resolver) but refused by bundle and workflow run <file> (write
confinement). Document the asymmetry so it reads as intentional.

(Disclosure: AI-assisted; reviewed by me.)

* docs(core): reframe symlinked .specify note around the override invariant

Per maintainer feedback on #3186: SPECIFY_INIT_DIR relocates where the project
is, not how a surface treats symlinks. Each surface keeps its cwd-path stance
(write surfaces refuse a symlinked .specify, read/config surfaces follow it),
so the split is one policy relocated, not an inconsistency.

* docs: address Copilot review on resolver docstrings

- _project.py: the error messages "mirror" the shell wording rather than
  "match" it (the CLI renders a Rich `Error:` line, the shell a plain `ERROR:`).
- find_project_root: document that honoring SPECIFY_INIT_DIR when start is None
  can raise typer.Exit / BundlerError, so the Path | None signature isn't
  surprising to direct callers.

* docs(bundler): note require_project_root inherits the override raise behavior

find_project_root can raise typer.Exit / BundlerError under the SPECIFY_INIT_DIR
override (start=None); require_project_root inherits that, so document it
alongside its own BundlerError-on-missing-project.

* docs: clarify symlinked project root behavior

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

* Address SPECIFY_INIT_DIR review feedback

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

* Route workflow JSON errors to stderr

Assisted-by: OpenAI Codex (model: GPT-5, autonomous)
2026-07-01 15:55:18 -05:00
Noor ul ain
f59fd81608 fix(extensions): resolve core-command dirs via _assets helpers (#3274) (#3287)
`_load_core_command_names()` computed its candidate command dirs with
bespoke `Path(__file__)` arithmetic. The #3014 move of this module from
`specify_cli/extensions.py` to `specify_cli/extensions/__init__.py`
pushed the file one directory deeper but left the `.parent` counts
unchanged, so both candidates resolved to non-existent paths:

  wheel  -> specify_cli/extensions/core_pack/commands (real: specify_cli/core_pack/commands)
  source -> src/templates/commands                    (real: repo-root templates/commands)

Neither exists, so every call silently fell through to
`_FALLBACK_CORE_COMMAND_NAMES`. Discovery is latent-dead: the fallback
happens to equal the real stems today, but the shadowing guard (#1994)
that depends on it now relies on someone hand-editing the fallback on
every core-command add/remove (as already happened for `converge`, #3001).

Delegate path resolution to the canonical `_locate_core_pack` /
`_repo_root` resolvers in `_assets` — the same ones the presets and
bundle loaders use. They are anchored to the package root, so discovery
survives future module moves.

Add regression tests that point the resolvers at a temp tree with
*different* command names, proving discovery reads from disk rather than
returning the fallback (they fail on the pre-fix code).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 13:53:39 -05:00
Noor ul ain
1849543611 fix: fall back to feature dir basename for empty CURRENT_BRANCH (#3026) (#3229)
* fix: fall back to feature dir basename for empty CURRENT_BRANCH (#3026)

When a feature is resolved via SPECIFY_FEATURE_DIRECTORY or .specify/feature.json
without SPECIFY_FEATURE set, get_current_branch() returns empty, so
get_feature_paths / Get-FeaturePathsEnv emitted CURRENT_BRANCH= (empty) even
though the feature directory was resolvable. Downstream scripts and agents that
expect a non-empty identifier got misleading output.

Fall back to the basename of the resolved feature directory when the branch is
empty, in both the bash (`${feature_dir##*/}`) and PowerShell
(`Split-Path -Leaf`) resolvers. An explicit SPECIFY_FEATURE still takes
precedence, so this only fills the previously-empty case.

Add bash + PowerShell regression tests: the basename fallback fires when
SPECIFY_FEATURE is unset, and an explicit SPECIFY_FEATURE still overrides it.

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

* Potential fix for pull request finding

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

* Potential fix for pull request finding

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

* Potential fix for pull request finding

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

* Potential fix for pull request finding

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

* fix: address Copilot feedback — PS 5.1 compat + parametrize bash test

- common.ps1: replace [System.IO.Path]::TrimEndingDirectorySeparator
  (a .NET Core-only method that throws MethodNotFound on Windows
  PowerShell 5.1 / .NET Framework) with a portable String.TrimEnd,
  so the trailing-slash trim actually works on 5.1.
- tests: parametrize the bash fallback test to cover feature.json,
  SPECIFY_FEATURE_DIRECTORY, and the explicit SPECIFY_FEATURE override
  (mirrors the PowerShell test), folding in the old explicit-override
  test; add the missing blank line before the next test (PEP 8).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-01 13:50:53 -05:00
Ben Buttigieg
c34a505d1c feat(bug-fix): add label-driven bug-fix agentic workflow (#3258)
* feat(bug-fix): add label-driven bug-fix agentic workflow

Add a `bug-fix` gh-aw workflow as stage 2 of the assess -> fix -> test
bug pipeline, mirroring the existing `bug-assess` stage. It triggers when
a maintainer applies the `bug-fix` label, recovers the slug and remediation
contract from the prior bug-assess assessment comment, applies the fix, and
opens a draft pull request plus a summary comment for human review.

The workflow is intentionally decoupled from Spec Kit specifics: it consumes
the assessment from the issue comment rather than any `.specify/` files, so it
is portable to other repositories running the matching bug-assess stage.

- .github/workflows/bug-fix.md authored and compiled to bug-fix.lock.yml
- Label-gated trigger (github.event.label.name == 'bug-fix')
- Draft PR via create-pull-request safe-output; scoped permissions
- Untrusted-input / URL-safety guardrails consistent with bug-assess
- Maintainer remains the gatekeeper; no unattended automation

Refs #3238

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

* fix(bug-fix): tighten bash allowlist and block protected files

Address Copilot review feedback on PR #3258:

- Trim tools.bash to the inspect set plus a small test-runner set
  (pytest, npm, go, cargo, dotnet), dropping package-manager/build
  tools (pip, npx, pnpm, yarn, mvn, gradle, make, bundle, rake, ruby,
  node) to reduce blast radius under prompt injection.
- Set create-pull-request.protected-files.policy: blocked so edits to
  sensitive files (dependency manifests, README/CHANGELOG/SECURITY,
  etc.) block PR creation, matching the stronger contract used by the
  other PR-creating workflows in this repo.

Refs #3238

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

* Potential fix for pull request finding

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

* Potential fix for pull request finding

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

* Potential fix for pull request finding

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

* fix(bug-fix): resync lock body_hash after review edits

The Copilot autofix commits edited bug-fix.md (verdict phrasing, Assisted-by
trailer) but did not recompile the lock, leaving body_hash stale. Since the
workflow runs with strict integrity, the runtime-imported bug-fix.md must match
the lock's recorded body_hash. Recompiled with gh-aw v0.79.8 (checkout pin kept
at v7.0.0 to match sibling locks); the only change is the body_hash.

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

* Potential fix for pull request finding

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

* Potential fix for pull request finding

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

* fix(bug-fix): align add-labels max to 1 and soften next-stage label reference

Address two Copilot review findings:

- add-labels.max: the authored frontmatter said max:1 but the committed lock
  enforced max:2 (stale from an earlier frontmatter), and Step 8 said 'max 2
  labels total'. The workflow only ever applies ONE status label per run
  (fix-proposed | needs-reproduction | fix-blocked | needs-assessment), so 1 is
  the correct, tightest contract. Recompiled so the lock now enforces max:1, and
  reworded Step 8 to 'exactly one status label per run'.
- bug-test label: Step 7 hard-coded applying a 'bug-test' label that does not
  exist in this repo. Since the workflow is portable, reworded to present the
  stage-3 bug-test workflow as the planned next stage 'if the repository has it
  configured' rather than assuming it exists.

Recompiled with gh-aw v0.79.8; checkout pins kept at v7.0.0 to match sibling
locks. No compile drift.

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

* Potential fix for pull request finding

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

* fix(bug-fix): set add-labels max to 1 consistently across source and lock

A prior autofix flipped the authored frontmatter add-labels.max back to 2,
re-introducing the mismatch: source said 2, the compiled lock enforced 1, and
Step 8 prose says 'exactly one status label per run'. The workflow only ever
applies a single status label per run (needs-assessment | needs-reproduction |
fix-proposed | fix-blocked), so 1 is the correct, tightest contract and matches
the compiled lock. Set the frontmatter to max:1 so source, lock, and prose all
agree (also avoids the lock staleness guard failing on a frontmatter mismatch).

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

* fix(bug-fix): relax protected files and number bug-fix branches

Address the two new Copilot review findings:

-  was still covering
  README.md and CHANGELOG.md, which can legitimately need updates as part of a
  prior bug remediation. Add them to the exclude list so the workflow can still
  open a PR when the assessment calls for documentation changes, matching the
  pattern used by add-community-extension.
- The generated branch name used , but the repo
  convention for bug fixes requires  so branches are
  traceable and aligned with AGENTS.md. Update the branch naming guidance to use
  .

Recompiled with gh-aw v0.79.8; lock reflects the protected-files exclusion and
keeps the v7.0.0 checkout pin fixups.

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

* fix(bug-fix): accept workflow-authored assessment comments from bot/service accounts

Address the open Copilot finding on assessment-author matching.

The workflow previously required the prior assessment comment to be authored by
`github-actions[bot]`. That is too strict for portable repos where bug-assess
may post through a different bot/service account token.

Updated Step 1 to select the most recent assessment comment that appears
workflow-authored by combining:
- bot/service-account authorship, and
- expected bug-assess structure (assessment header plus remediation/files/tests sections).

This keeps the spoof-resistance intent while removing dependence on one fixed
login.

Recompiled with gh-aw v0.79.8 and kept checkout v7.0.0 pin fixups.

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

* fix(bug-fix): clarify local-check guardrails for dependency fetching

Address Copilot feedback on Step 5 consistency around network-dependent checks.

The workflow previously listed `go test ./...` and `cargo test` as examples
while also forbidding network-dependent commands, which could be ambiguous on
clean runners.

Updated Step 5 to:
- keep those commands as examples only when dependencies are already present
- explicitly disallow dependency-fetch/install commands during verification
  (go mod download/go get/cargo fetch/npm|pnpm|yarn install)

Recompiled with gh-aw v0.79.8 and kept checkout v7.0.0 pin fixups.

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

* fix(bug-fix): make status label application conditional on label existence

Address Copilot feedback about missing status labels causing runtime failures.

The workflow previously instructed unconditional application of
`needs-assessment`, `fix-blocked`, and `fix-proposed`. In repositories where
those labels are not pre-created, `add_labels` fails and can break the run.

Updated Steps 1/3/4/8 to require existence checks before adding those labels:
- add the label only if it exists
- otherwise skip labeling and explicitly note that in the comment

This preserves the status-label UX when labels exist while keeping execution
robust in repos that have not created every optional status label yet.

Recompiled with gh-aw v0.79.8 and kept checkout v7.0.0 pin fixups.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
2026-07-01 18:52:35 +01:00
Ben Buttigieg
ac6eef4520 feat(workflows): add label-driven bug-test workflow (#3239) (#3257)
* feat(workflows): add label-driven bug-test workflow (#3239)

Add the third stage (assess → fix → test) of the semi-automated, human-gated
bug pipeline. The `bug-test` agentic workflow triggers when a maintainer applies
the `bug-test` label, runs the relevant tests in isolation against the fix,
compiles a readable pass/fail report, and posts it back as a single issue
comment.

- Locates the fix under test: linked PR → named fix branch → current checkout
  fallback, only ever from origin.
- Stack-agnostic test detection (uv+pytest, npm/pnpm/yarn, go, make) so it is
  decoupled from Spec Kit specifics and reusable by other projects.
- Runs tests under a timeout as untrusted code; scoped read-only permissions;
  same URL-safety / untrusted-input guardrails as bug-assess.
- Verification mode compares a generated fix against the historical fix for
  old/closed bugs to surface discrepancies.
- Optional single result label (tests-passing / tests-failing /
  tests-inconclusive).

Compiled bug-test.lock.yml with `gh aw compile`.

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

* fix(workflows): bump actions/checkout from 6.0.3 to 7.0.0 in bug-test workflow

Align with repo standards (e.g. dependabot PR #3064, other workflows).
Manually pinned in the compiled lock file for consistency.

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

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-01 12:13:09 -05:00