mirror of
https://github.com/github/spec-kit.git
synced 2026-07-03 12:28:06 +08:00
feat: add /speckit.converge command (#3001)
* Add /speckit.converge SDD artifacts and project scaffolding Dogfood the converge feature through Spec Kit's own workflow: - spec.md, plan.md, tasks.md, research, data-model, contracts, quickstart - requirements checklist for the feature - ratified constitution v1.0.0 (.specify/memory) - Specify project scaffolding (.specify/, .github agent + prompt files) Defines a built-in /speckit.converge command that assesses spec/plan/tasks against the codebase and appends remaining work as new tasks (no git, no change tracking, append-only). Implementation not yet started. Excludes unrelated working-tree changes to agents.py, extensions.py, test_extensions.py, catalog.community.json, and README.md. * Implement /speckit.converge command Add the built-in converge command that assesses the codebase against a feature's spec.md, plan.md, and tasks.md and appends remaining unbuilt work as new traceable tasks to tasks.md (append-only; no git, no change tracking). - templates/commands/converge.md: full command body (load artifacts, assess code, classify findings missing/partial/contradicts/unrequested, append '## Phase N — Convergence' tasks with source-ref + gap-type, read-only guardrails, converged branch, handoff, before/after_converge hooks) - Register converge as a core command across all enumeration sites (SKILL_DESCRIPTIONS, _FALLBACK_CORE_COMMAND_NAMES, ARGUMENT_HINTS, and the integration test command lists incl. copilot/generic file inventories) - init.py Next Steps panel + README Core Commands table - tasks.md: T001-T024 complete (T025 manual quickstart pending) Full suite green: 2343 passed. * Record quickstart validation results for /speckit.converge (T025) All six quickstart scenarios validated (GitHub Copilot agent, macOS/zsh): S1 gap->appended traceable task, S2 implement+re-converge, S3 converged leaves tasks.md unchanged, S4 read-only boundaries, S5 missing-prereq stop, S6 cross- integration install (copilot + windsurf). Automated suite: 2343 passed. * Record 2026-06-16 re-verification results for /speckit.converge (T025) * 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 integration upgrade deleting settings.json and dropping script +x Two upgrade-path bugs surfaced during converge E2E validation: - copilot upgrade stale-deleted .vscode/settings.json because setup() only tracks the file when it creates it; on upgrade the pre-existing file is merged and left untracked, so Phase 2 stale cleanup removed it. Add an integration-level stale_cleanup_exclusions() hook (CopilotIntegration returns {.vscode/settings.json}) and subtract it from stale_keys. - shared .specify/scripts/*.sh lost their execute bit because the managed refresh rewrites them with the bundled source mode (often 0o644) and nothing restored perms. Call ensure_executable_scripts() after the managed-refresh block (POSIX only). Add regression tests in TestIntegrationUpgrade covering both fixes (validated to fail without the fixes). * fix: resolve markdownlint errors in PR files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: clean up runtime state files from PR Remove .specify state files that are per-project runtime artifacts: - feature.json, init-options.json, integration.json - manifest files, extension registry, bug artifacts These are generated by 'specify init' and should not be committed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: fold converge artifacts from #3003 and #3005 - Add speckit.converge Copilot agent and prompt files (#3003) - Add regression test for Claude argument hints (#3005) - Remove invalid converge entry from Claude argument hints - Fix documentation removing branch-prefix fallback claims Supersedes: #3003, #3005 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: remove non-converge specify scaffolding from PR Remove .specify/ artifacts, non-converge .github/agents and prompts, and copilot-instructions.md that were generated by 'specify init' and are not part of the converge command feature. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: remove SDD spec artifacts from PR Remove specs/001-converge-command/ — the spec/plan/tasks/research SDD artifacts produced while building this feature. spec-kit does not track a specs/ directory on main (those are outputs of running the workflow on the repo, not part of the shipped tool). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: remove generated Copilot converge command files Remove .github/agents/speckit.converge.agent.md and .github/prompts/speckit.converge.prompt.md — these are generated by 'specify init --integration copilot' from templates/commands/converge.md (all __SPECKIT_COMMAND_*__/{SCRIPT} tokens are resolved). main tracks no .github/agents or .github/prompts files; the template is the source of truth. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: split out unrelated integration-upgrade fix Move the stale_cleanup_exclusions / executable-bit upgrade fix (base.py, copilot, _migrate_commands.py, test_integration_subcommand.py) out of this PR into its own change. This PR is now scoped purely to the /speckit.converge command. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: add converge to core command template ordering converge is a core command in SKILL_DESCRIPTIONS but was missing from _CORE_COMMAND_TEMPLATE_ORDER, so it sorted with the fallback rank. Add it after 'implement' to keep core-command ordering consistent across integrations. Addresses review feedback on #3001. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: make converge findings example neutral Replace the self-referential sample evidence text in the Convergence Findings table with a neutral placeholder so agents are less likely to copy nonsensical template-specific findings into real output. Co-authored-by: Copilot <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> * docs: clarify converge scope and hook outcome wording - Remove FR-specific parenthetical from code-scope rule so it doesn't imply a hard FR-001 reference exists in every feature - Replace unsupported 'pass outcome to hook context' instruction with explicit in-session outcome reporting before hook listing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: align converge task example with tasks format Use (no colon) in the convergence task example so it matches tasks-template formatting and downstream expectations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Clarification of usage 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> * docs: align converge phase/task-id format with tasks template - Use (colon) for consistency with tasks template - Clarify appended task IDs must be zero-padded ( style) - Update checklist example to a concrete zero-padded ID () Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: standardize converge phase heading format Use consistently in converge.md (including the append-only contract section) to match Step 7 and tasks template style. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -254,7 +254,7 @@ class MarkdownIntegrationTests:
|
||||
|
||||
COMMAND_STEMS = [
|
||||
"agent-context.update",
|
||||
"analyze", "clarify", "constitution", "implement",
|
||||
"analyze", "clarify", "constitution", "converge", "implement",
|
||||
"plan", "checklist", "specify", "tasks", "taskstoissues",
|
||||
]
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ class SkillsIntegrationTests:
|
||||
skill_files = [f for f in created if "scripts" not in f.parts]
|
||||
|
||||
expected_commands = {
|
||||
"analyze", "clarify", "constitution", "implement",
|
||||
"analyze", "clarify", "constitution", "converge", "implement",
|
||||
"plan", "checklist", "specify", "tasks", "taskstoissues",
|
||||
}
|
||||
|
||||
@@ -393,7 +393,7 @@ class SkillsIntegrationTests:
|
||||
# -- Complete file inventory ------------------------------------------
|
||||
|
||||
_SKILL_COMMANDS = [
|
||||
"analyze", "clarify", "constitution", "implement",
|
||||
"analyze", "clarify", "constitution", "converge", "implement",
|
||||
"plan", "checklist", "specify", "tasks", "taskstoissues",
|
||||
]
|
||||
|
||||
|
||||
@@ -486,6 +486,7 @@ class TomlIntegrationTests:
|
||||
"analyze",
|
||||
"clarify",
|
||||
"constitution",
|
||||
"converge",
|
||||
"implement",
|
||||
"plan",
|
||||
"checklist",
|
||||
|
||||
@@ -365,6 +365,7 @@ class YamlIntegrationTests:
|
||||
"analyze",
|
||||
"clarify",
|
||||
"constitution",
|
||||
"converge",
|
||||
"implement",
|
||||
"plan",
|
||||
"checklist",
|
||||
|
||||
@@ -341,18 +341,30 @@ class TestClaudeIntegration:
|
||||
class TestClaudeArgumentHints:
|
||||
"""Verify that argument-hint frontmatter is injected for Claude skills."""
|
||||
|
||||
def test_converge_has_no_argument_hint(self):
|
||||
"""Converge should not advertise unsupported feature-name arguments."""
|
||||
assert "converge" not in ARGUMENT_HINTS
|
||||
|
||||
def test_all_skills_have_hints(self, tmp_path):
|
||||
"""Every generated SKILL.md must contain an argument-hint line."""
|
||||
"""Every skill with a configured hint must contain an argument-hint line."""
|
||||
i = get_integration("claude")
|
||||
m = IntegrationManifest("claude", tmp_path)
|
||||
created = i.setup(tmp_path, m, script_type="sh")
|
||||
skill_files = [f for f in created if f.name == "SKILL.md"]
|
||||
assert len(skill_files) > 0
|
||||
for f in skill_files:
|
||||
stem = f.parent.name
|
||||
if stem.startswith("speckit-"):
|
||||
stem = stem[len("speckit-"):]
|
||||
content = f.read_text(encoding="utf-8")
|
||||
assert "argument-hint:" in content, (
|
||||
f"{f.parent.name}/SKILL.md is missing argument-hint frontmatter"
|
||||
)
|
||||
if stem in ARGUMENT_HINTS:
|
||||
assert "argument-hint:" in content, (
|
||||
f"{f.parent.name}/SKILL.md is missing argument-hint frontmatter"
|
||||
)
|
||||
else:
|
||||
assert "argument-hint:" not in content, (
|
||||
f"{f.parent.name}/SKILL.md unexpectedly has argument-hint frontmatter"
|
||||
)
|
||||
|
||||
def test_hints_match_expected_values(self, tmp_path):
|
||||
"""Each skill's argument-hint must match the expected text."""
|
||||
@@ -366,13 +378,15 @@ class TestClaudeArgumentHints:
|
||||
if stem.startswith("speckit-"):
|
||||
stem = stem[len("speckit-"):]
|
||||
expected_hint = ARGUMENT_HINTS.get(stem)
|
||||
assert expected_hint is not None, (
|
||||
f"No expected hint defined for skill '{stem}'"
|
||||
)
|
||||
content = f.read_text(encoding="utf-8")
|
||||
assert f'argument-hint: "{expected_hint}"' in content, (
|
||||
f"{f.parent.name}/SKILL.md: expected hint '{expected_hint}' not found"
|
||||
)
|
||||
if expected_hint is None:
|
||||
assert "argument-hint:" not in content, (
|
||||
f"{f.parent.name}/SKILL.md unexpectedly has argument-hint frontmatter"
|
||||
)
|
||||
else:
|
||||
assert f'argument-hint: "{expected_hint}"' in content, (
|
||||
f"{f.parent.name}/SKILL.md: expected hint '{expected_hint}' not found"
|
||||
)
|
||||
|
||||
def test_hint_is_inside_frontmatter(self, tmp_path):
|
||||
"""argument-hint must appear between the --- delimiters, not in the body."""
|
||||
@@ -386,12 +400,20 @@ class TestClaudeArgumentHints:
|
||||
assert len(parts) >= 3, f"No frontmatter in {f.parent.name}/SKILL.md"
|
||||
frontmatter = parts[1]
|
||||
body = parts[2]
|
||||
assert "argument-hint:" in frontmatter, (
|
||||
f"{f.parent.name}/SKILL.md: argument-hint not in frontmatter section"
|
||||
)
|
||||
assert "argument-hint:" not in body, (
|
||||
f"{f.parent.name}/SKILL.md: argument-hint leaked into body"
|
||||
)
|
||||
stem = f.parent.name
|
||||
if stem.startswith("speckit-"):
|
||||
stem = stem[len("speckit-"):]
|
||||
if stem in ARGUMENT_HINTS:
|
||||
assert "argument-hint:" in frontmatter, (
|
||||
f"{f.parent.name}/SKILL.md: argument-hint not in frontmatter section"
|
||||
)
|
||||
assert "argument-hint:" not in body, (
|
||||
f"{f.parent.name}/SKILL.md: argument-hint leaked into body"
|
||||
)
|
||||
else:
|
||||
assert "argument-hint:" not in content, (
|
||||
f"{f.parent.name}/SKILL.md unexpectedly has argument-hint frontmatter"
|
||||
)
|
||||
|
||||
def test_hint_appears_after_description(self, tmp_path):
|
||||
"""argument-hint must immediately follow the description line."""
|
||||
@@ -402,6 +424,14 @@ class TestClaudeArgumentHints:
|
||||
for f in skill_files:
|
||||
content = f.read_text(encoding="utf-8")
|
||||
lines = content.splitlines()
|
||||
stem = f.parent.name
|
||||
if stem.startswith("speckit-"):
|
||||
stem = stem[len("speckit-"):]
|
||||
if stem not in ARGUMENT_HINTS:
|
||||
assert "argument-hint:" not in content, (
|
||||
f"{f.parent.name}/SKILL.md unexpectedly has argument-hint frontmatter"
|
||||
)
|
||||
continue
|
||||
found_description = False
|
||||
for idx, line in enumerate(lines):
|
||||
if line.startswith("description:"):
|
||||
|
||||
@@ -125,9 +125,9 @@ class TestCopilotIntegration:
|
||||
agents_dir = tmp_path / ".github" / "agents"
|
||||
assert agents_dir.is_dir()
|
||||
agent_files = sorted(agents_dir.glob("speckit.*.agent.md"))
|
||||
assert len(agent_files) == 9
|
||||
assert len(agent_files) == 10
|
||||
expected_commands = {
|
||||
"analyze", "clarify", "constitution", "implement",
|
||||
"analyze", "clarify", "constitution", "converge", "implement",
|
||||
"plan", "checklist", "specify", "tasks", "taskstoissues",
|
||||
}
|
||||
actual_commands = {f.name.removeprefix("speckit.").removesuffix(".agent.md") for f in agent_files}
|
||||
@@ -198,6 +198,7 @@ class TestCopilotIntegration:
|
||||
".github/agents/speckit.checklist.agent.md",
|
||||
".github/agents/speckit.clarify.agent.md",
|
||||
".github/agents/speckit.constitution.agent.md",
|
||||
".github/agents/speckit.converge.agent.md",
|
||||
".github/agents/speckit.implement.agent.md",
|
||||
".github/agents/speckit.plan.agent.md",
|
||||
".github/agents/speckit.specify.agent.md",
|
||||
@@ -208,6 +209,7 @@ class TestCopilotIntegration:
|
||||
".github/prompts/speckit.checklist.prompt.md",
|
||||
".github/prompts/speckit.clarify.prompt.md",
|
||||
".github/prompts/speckit.constitution.prompt.md",
|
||||
".github/prompts/speckit.converge.prompt.md",
|
||||
".github/prompts/speckit.implement.prompt.md",
|
||||
".github/prompts/speckit.plan.prompt.md",
|
||||
".github/prompts/speckit.specify.prompt.md",
|
||||
@@ -268,6 +270,7 @@ class TestCopilotIntegration:
|
||||
".github/agents/speckit.checklist.agent.md",
|
||||
".github/agents/speckit.clarify.agent.md",
|
||||
".github/agents/speckit.constitution.agent.md",
|
||||
".github/agents/speckit.converge.agent.md",
|
||||
".github/agents/speckit.implement.agent.md",
|
||||
".github/agents/speckit.plan.agent.md",
|
||||
".github/agents/speckit.specify.agent.md",
|
||||
@@ -278,6 +281,7 @@ class TestCopilotIntegration:
|
||||
".github/prompts/speckit.checklist.prompt.md",
|
||||
".github/prompts/speckit.clarify.prompt.md",
|
||||
".github/prompts/speckit.constitution.prompt.md",
|
||||
".github/prompts/speckit.converge.prompt.md",
|
||||
".github/prompts/speckit.implement.prompt.md",
|
||||
".github/prompts/speckit.plan.prompt.md",
|
||||
".github/prompts/speckit.specify.prompt.md",
|
||||
@@ -321,7 +325,7 @@ class TestCopilotSkillsMode:
|
||||
"""Tests for Copilot integration in --skills mode."""
|
||||
|
||||
_SKILL_COMMANDS = [
|
||||
"analyze", "clarify", "constitution", "implement",
|
||||
"analyze", "clarify", "constitution", "converge", "implement",
|
||||
"plan", "checklist", "specify", "tasks", "taskstoissues",
|
||||
]
|
||||
|
||||
|
||||
@@ -214,6 +214,7 @@ class TestGenericIntegration:
|
||||
[
|
||||
"analyze",
|
||||
"clarify",
|
||||
"converge",
|
||||
"implement",
|
||||
"plan",
|
||||
"checklist",
|
||||
@@ -306,6 +307,7 @@ class TestGenericIntegration:
|
||||
".myagent/commands/speckit.checklist.md",
|
||||
".myagent/commands/speckit.clarify.md",
|
||||
".myagent/commands/speckit.constitution.md",
|
||||
".myagent/commands/speckit.converge.md",
|
||||
".myagent/commands/speckit.implement.md",
|
||||
".myagent/commands/speckit.plan.md",
|
||||
".myagent/commands/speckit.specify.md",
|
||||
@@ -370,6 +372,7 @@ class TestGenericIntegration:
|
||||
".myagent/commands/speckit.checklist.md",
|
||||
".myagent/commands/speckit.clarify.md",
|
||||
".myagent/commands/speckit.constitution.md",
|
||||
".myagent/commands/speckit.converge.md",
|
||||
".myagent/commands/speckit.implement.md",
|
||||
".myagent/commands/speckit.plan.md",
|
||||
".myagent/commands/speckit.specify.md",
|
||||
|
||||
Reference in New Issue
Block a user