Skip to content

feat(claude): run /analyze in a forked subagent#2511

Open
echarrod wants to merge 2 commits into
github:mainfrom
echarrod:ed/claude-analyze-context-fork
Open

feat(claude): run /analyze in a forked subagent#2511
echarrod wants to merge 2 commits into
github:mainfrom
echarrod:ed/claude-analyze-context-fork

Conversation

@echarrod

@echarrod echarrod commented May 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds context: fork + agent: general-purpose to the generated /analyze Claude skill so it runs in an isolated subagent context.

Refs #752 — scoped narrowly to /analyze rather than every command, because most spec-kit commands are interactive or have side effects and rely on conversation continuity.

Why /analyze specifically

/analyze is the strongest fit for context: fork in the spec-kit command set:

  • Explicitly read-only. The template states STRICTLY READ-ONLY: Do not modify any files.
  • Heavy inputs that collapse to a short report. Loads spec.md, plan.md, tasks.md, and the constitution; emits a compact findings table.
  • No conversation history needed. The task is fully described by the skill content and $ARGUMENTS.
  • Matches the docs' canonical example. Run skills in a subagent shows exactly this pattern.

Net effect: the artefact contents stay inside the subagent. The main session only sees the report — the user keeps a clean conversation context for follow-up work, which is the concern raised in #752.

Why general-purpose over Explore

Claude Code's Explore agent description warns it's unsuitable for "cross-file consistency checks, design-doc auditing, or open-ended analysis" — which is exactly what /analyze does. general-purpose has the same read-only tool surface without that caveat.

Why per-command opt-in (not a global flag)

Other spec-kit commands aren't safe to fork:

  • /specify, /plan, /tasks, /implement, /clarify, /checklist, /constitution, /taskstoissues — all interactive, accept follow-up turns, or write files based on conversation context. Forking would break them.

So this PR introduces a FORK_CONTEXT_COMMANDS mapping mirroring the existing per-command ARGUMENT_HINTS pattern, with analyze as the single entry. New commands can opt in if they fit the same criteria.

Implementation

  • FORK_CONTEXT_COMMANDS: dict[str, dict[str, str]] — stem → frontmatter overrides.
  • The per-command frontmatter injection (both argument-hint and fork context) lives in ClaudeIntegration.post_process_skill_content() — the single method every skill-generation path calls (setup(), preset overrides, and extensions). It derives the command stem from the frontmatter name: and injects each key/value via the idempotent _inject_frontmatter_flag / inject_argument_hint helpers. This means a preset or extension that regenerates speckit-analyze keeps context: fork instead of silently dropping it — previously the injection lived only in setup(), so those paths bypassed it. setup() is now just the post-process loop.
  • Non-Claude integrations are untouched — context: fork is Claude-Code-specific frontmatter.

Tests

Seven tests under TestClaudeForkContext:

  • test_analyze_skill_runs_in_forked_subagent — speckit-analyze has context: fork + agent: general-purpose
  • test_other_skills_do_not_fork — no other speckit-* skill gains context or agent
  • test_fork_flags_inside_frontmatter — keys land in frontmatter, not body
  • test_fork_injection_idempotent — re-running setup doesn't duplicate keys
  • test_fork_context_injected_via_post_process — the preset/extension path (post_process_skill_content) also injects fork context + argument-hint
  • test_post_process_no_fork_for_other_skills — non-fork skills gain nothing via that path
  • test_post_process_fork_idempotent — re-running post_process doesn't duplicate keys

Test plan for reviewers

  1. uv run --extra test python -m pytest tests/integrations/test_integration_claude.py -v — 37 tests pass (30 existing + 7 fork-context)
  2. uv run specify init test-proj --ai claude --script sh --no-git --ignore-agent-tools and inspect test-proj/.claude/skills/speckit-analyze/SKILL.md — frontmatter should contain context: fork and agent: general-purpose
  3. Run /speckit-analyze in a populated spec-kit project — output should be a single summarised report; the artefact contents should not appear in the main conversation

@echarrod echarrod requested a review from mnriem as a code owner May 11, 2026 11:41
@mnriem

mnriem commented May 11, 2026

Copy link
Copy Markdown
Collaborator

Please deliver this as a preset as per https://github.com/github/spec-kit/tree/main/presets

@echarrod

Copy link
Copy Markdown
Contributor Author

Please deliver this as a preset as per https://github.com/github/spec-kit/tree/main/presets

Hi @mnriem, my thinking was that this would be useful behaviour to all, regardless of whether you've opted in to the preset or not. Are you concerned with it becoming default behaviour?

@mnriem

mnriem commented May 12, 2026

Copy link
Copy Markdown
Collaborator

It is a matter of traceability. Not every one would want sub agents to get triggered automatically. Hence why it would be better as a preset so one can opt into it. And also making sure that the core stays agnostic as much as possible

@echarrod echarrod changed the title claude: run /analyze in a forked subagent feat(claude): run /analyze in a forked subagent Jun 11, 2026
@echarrod

Copy link
Copy Markdown
Contributor Author

Pushed ebf98c1 to close a gap in this change: the fork-context (and argument-hint) frontmatter was injected only in ClaudeIntegration.setup(), so skills generated through post_process_skill_content() directly - presets and extensions - bypassed it. A preset overriding speckit-analyze, for example, would silently drop context: fork.

The per-command injection now lives in post_process_skill_content() - the one method every generation path calls - keyed off the frontmatter name:, so setup(), presets, and extensions stay consistent. setup() is now just the post-process loop. Added 3 regression tests for the post_process path (injection applied, non-fork skills untouched, idempotency).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the Claude integration’s generated /analyze skill to run in an isolated Claude Code subagent by injecting context: fork and agent: general-purpose into the SKILL.md frontmatter for speckit-analyze only. It also centralizes per-command frontmatter injection in ClaudeIntegration.post_process_skill_content() so the behavior applies consistently across all skill-generation paths (setup, presets, extensions).

Changes:

  • Add FORK_CONTEXT_COMMANDS mapping (currently analyze only) to define per-command frontmatter injected into Claude skills.
  • Enhance ClaudeIntegration.post_process_skill_content() to derive the skill stem from frontmatter name: and inject both argument-hint and fork-context frontmatter where applicable.
  • Add a dedicated test suite validating fork-context injection scope, placement (frontmatter only), and idempotency.
Show a summary per file
File Description
tests/integrations/test_integration_claude.py Adds coverage for fork-context injection behavior and idempotency for speckit-analyze, and ensures other skills remain unaffected.
src/specify_cli/integrations/claude/init.py Introduces FORK_CONTEXT_COMMANDS and injects per-command frontmatter (argument-hint + fork context) via post_process_skill_content().

Copilot's findings

Tip

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

  • Files reviewed: 2/2 changed files
  • Comments generated: 0

@mnriem

mnriem commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Please resolve conflicts

echarrod and others added 2 commits June 16, 2026 09:52
/analyze is explicitly read-only and produces a compact analysis
report from heavy artefact reads (spec.md, plan.md, tasks.md). It
matches the canonical use case for context: fork — bulk inputs that
collapse to a short summary, no need for conversation history.

Forking keeps the artefact contents out of the main conversation
context, which is the concern raised in github#752.

Done as a per-command opt-in via FORK_CONTEXT_COMMANDS so other
spec-kit commands (which are interactive or have side effects) are
unaffected.

Refs github#752
argument-hint and fork context were injected only in setup(), so skills
produced via post_process_skill_content() directly (presets, extensions)
lost them - e.g. a preset overriding speckit-analyze dropped context: fork.

Move the per-command injection into post_process_skill_content(), deriving
the command stem from the frontmatter name, so all generation paths stay
consistent. setup() now just calls post_process_skill_content().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@echarrod echarrod force-pushed the ed/claude-analyze-context-fork branch from ebf98c1 to 276e5da Compare June 16, 2026 09:00
@echarrod

Copy link
Copy Markdown
Contributor Author

Please resolve conflicts

Resolved now 👍

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 1

Comment on lines 227 to 233
content_bytes = path.read_bytes()
content = content_bytes.decode("utf-8")

updated = content

# Inject argument-hint if available for this skill
skill_dir_name = path.parent.name # e.g. "speckit-plan"
stem = skill_dir_name
if stem.startswith("speckit-"):
stem = stem[len("speckit-"):]
hint = ARGUMENT_HINTS.get(stem, "")
if hint:
updated = self.inject_argument_hint(updated, hint)
updated = self.post_process_skill_content(content)

if updated != content:
path.write_bytes(updated.encode("utf-8"))
@mnriem

mnriem commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants