Skip to content

feat: spawn templates with behavioral prompts#75

Open
sf8193 wants to merge 14 commits into
mainfrom
feat/spawn-templates
Open

feat: spawn templates with behavioral prompts#75
sf8193 wants to merge 14 commits into
mainfrom
feat/spawn-templates

Conversation

@sf8193

@sf8193 sf8193 commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

  • Named spawn templates that prepend behavioral prompts to sessions
  • 4 builtins: review, fix, investigate, incident
  • Custom templates via templates.json in the state dir (hot-reloadable, validated)
  • Works with both spawn: and spawn-wt: commands
  • /templates command lists available templates
  • Hint message when a single-word spawn matches a template name
  • Confirmation message posted to thread when template is used

Usage

spawn: review wt:treasury #400       → review template + worktree
spawn: fix wt:treasury ENG-1234      → fix template + worktree
spawn: investigate email delays       → investigate template
spawn: anything else                  → generic spawn (unchanged)

Custom templates

Drop a templates.json in ~/.claude/channels/<platform>/:

{
  "deploy": { "prompt": "Deploy this change safely. Run tests first, then create a PR." }
}

Test plan

  • spawn: review wt:treasury something → template confirmation + worktree
  • spawn: fix something → fix template, no worktree
  • spawn: random topic → generic spawn, no template
  • spawn: review (no topic) → generic spawn + hint message
  • spawn-wt: treasury fix something → fix template + worktree
  • templates → lists all templates with source labels

🤖 Generated with Claude Code

sf8193 and others added 14 commits July 3, 2026 01:30
Templates prepend a behavioral prompt to sessions. Four builtins:
review, fix, investigate, incident. Custom templates via
templates.json in the state dir.

Usage: spawn: review wt:treasury #400
       spawn: fix wt:treasury ENG-1234
       spawn: investigate email delays

Templates command lists available templates. Help text updated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Templates are now top-level commands (review: topic, design: topic)
instead of spawn: prefixed. Template matching runs AFTER all hardcoded
commands in the router cascade, eliminating namespace collision risk.

- design template auto-starts the multi-persona design flow
- actions field lets templates trigger daemon-level operations on spawn
- mtime-cached template file reads (statSync vs readFileSync on hot path)
- resolveSpawnTarget helper eliminates duplicated thread-reuse logic
- isAlive guard fixes dead-session thread routing
- reserved name + colon validation on user template load

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All three multi-session protocols now launchable from main thread:
- review: topic → spawn + adversarial review (3 rounds)
- build: topic → spawn + build protocol (3 rounds)
- design: topic → spawn + design protocol

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Validate action values on load — only review, build, design are
accepted. Unknown actions log a warning and are stripped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The critic/builder/persona now sees the template prompt as part of
its topic, so custom templates like "audit" shape the protocol's
behavior, not just the owner session.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Templates now load from two repo-local files (merged, last wins):
- templates.json — checked in, shared across users
- templates.local.json — gitignored, personal overrides

Builtins reduced to review/design/build (the ones with actions).
Prompt-only templates (fix, investigate, incident) moved to
templates.json where anyone can edit them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…d code

- Extract shared spawnAndNotify helper — deduplicates handleSpawnIntercept
  and handleTemplateSpawn (~60 lines removed)
- Build dynamic regex from known template names — only matches actual
  templates, not arbitrary word: patterns
- Delete dead code: resolveTemplate and isTemplateName (replaced by
  getTemplate and getTemplateNames)
- Guard empty topic in template commands
- Chat-visible error on action failure (was only in stderr)
- Chat-visible warning when user template overrides builtin with actions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…x fix

- Change actions[] to singular action string — eliminates impossible
  multi-action combinations at the type level
- Template commands always spawn regardless of thread context
- /templates shows which templates have protocol actions
- Thread-scoped design command uses space syntax (design topic) to
  match review/build, avoiding collision with template command (design: topic)
- Chat-visible error on action failure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
services.json maps service names to repos, code paths, and runbooks.
The incident template now reads this config and relevant on-call guides
before investigating.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Incident template is specific to individual on-call workflows,
not a shared default.

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

@sf8193 sf8193 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

PR #75 Review: Spawn Templates with Behavioral Prompts

Reviewed by dual-agent process (sp-reviewer + typescript-reviewer). 9 findings: 2 blockers, 4 should-fix, 3 nits.

Recommendation: Request changes — blockers 1 and 2 need resolution before merge.

Comment thread daemon/router.ts
{
const names = getTemplateNames()
if (names.length > 0) {
const pattern = new RegExp(`^(${names.join('|')}):\\s*([\\s\\S]*)$`, 'i')

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Blocker — Regex injection via template names. Template names from user-controlled JSON files are interpolated directly into new RegExp(...) without escaping. A template name containing regex metacharacters (e.g. fix+, test(1), a|b) would produce incorrect matching or throw a runtime exception crashing the message handler.

Either regex-escape names before interpolation, or replace with a non-regex approach (split on : and check membership in a Set).

Flagged by: typescript-reviewer

Comment thread daemon/router.ts
}

const designMatch = msg.content.match(/^(?:\/design|design):\s*([\s\S]+)$/i)
const designMatch = msg.content.match(/^(?:\/design|design)\s+([\s\S]+)$/i)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Blocker — Design regex change breaks existing syntax and creates inconsistency. This changes the in-thread design command from design: topic (colon) to design topic (space). This is a breaking change to an existing command surface, buried in a template feature PR.

Additionally, the new template system now matches design: topic at top level (spawns new session + protocol), while in-thread the old command requires design topic (space, starts protocol in current session). These are fundamentally different operations with confusingly similar syntax.

More broadly, builtin templates for review, design, and build shadow the existing thread-scoped commands of the same name. When a user types review: fix the auth bug in a thread, the template match fires first (line 304) and spawns a new session, when they likely wanted the in-thread review protocol.

Recommend: exclude builtin protocol names from template command matching when msg.isThread and a session owns the thread, so thread-scoped commands retain priority.

Flagged by: both reviewers

Comment thread daemon/commands/global.ts
const action = template.template.action
const actionTopic = `${template.template.prompt} — ${topic}`
try {
switch (action) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Should-fix — Template actions bypass protocol guards. spawnAndNotify calls startReview/startBuild/startDesign directly, skipping the guards in handleReviewIntercept/handleBuildIntercept/handleDesignIntercept (session liveness, conflicting protocol checks, ownership validation). This creates a second unguarded code path into the protocol layer.

Recommend: route template actions through the existing command handlers, or extract the shared guard logic into protocol modules callable from both paths.

Flagged by: sp-reviewer

Comment thread daemon/commands/global.ts

if (template?.template.action) {
const action = template.template.action
const actionTopic = `${template.template.prompt} — ${topic}`

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Should-fix — actionTopic concatenates full prompt + user topic. This passes the entire prompt text as part of the topic string to startDesign/startReview/startBuild. Since prompts can be long (especially user-defined templates), this creates unwieldy topic strings in logs, thread names, and status displays. The user's topic alone would be more appropriate — the prompt is already injected via promptPrefix.

Flagged by: typescript-reviewer

Comment thread daemon/router.ts
if (templateCmdMatch) {
const candidateName = templateCmdMatch[1].trim()
const candidateTopic = templateCmdMatch[2].trim()
const template = getTemplate(candidateName)!

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Should-fix — Non-null assertion on potentially null value. Between getTemplateNames() and getTemplate(), the template file could be modified on disk (mtime changes, cache invalidates). If the template was removed, getTemplate returns null and the ! assertion crashes the router. The fix is trivial — add a null guard.

Flagged by: both reviewers

Comment thread daemon/router.ts

const CONTEXT_LINK_DOMAINS = /slack\.com\/archives|linear\.app|notion\.so|incident\.io|app\.datadoghq\.com|sentry\.io|pagerduty\.com/
const MAX_CONTEXT_LINKS = 5

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Should-fix — Domain logic in the router module. extractContextLinks, CONTEXT_LINK_DOMAINS, and the link storage logic are defined in router.ts — a message routing module. This is link-extraction and session-state enrichment logic that belongs in the session or sessions module where state management lives.

Flagged by: sp-reviewer

Comment thread daemon/commands/global.ts
import { getActiveBuilds, cancelBuild } from '../build.js'
import { getActiveReviews, cancelReview } from '../adversarial.js'
import { startDesign } from '../design.js'
import { startReview } from '../adversarial.js'

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Nit — Duplicate import from same module. Line 13 imports getActiveReviews, cancelReview from '../adversarial.js' and this line imports startReview from the same module. Consolidate into a single import.

Flagged by: typescript-reviewer

Comment thread daemon/commands/global.ts
): Promise<void> {
void gateway.react(msg.channelId, msg.id, '🚀').catch(() => {})
const chatId = await resolveSpawnTarget(msg)
const label = template ? `${template.name}` : null

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Nit — Redundant template literal. `${template.name}` is equivalent to template.name — no interpolation needed.

Flagged by: typescript-reviewer

Comment thread daemon/commands/global.ts
await spawnAndNotify(msg, topic)
}

export async function handleTemplateSpawn(msg: InboundMessage, templateName: string, topic: string, template: SpawnTemplate, access: Access): Promise<void> {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Nit — Unused access parameter. handleTemplateSpawn accepts access: Access but never references it. Same for the refactored handleSpawnIntercept at line 111. Either implement access gating or remove the parameter.

Flagged by: both reviewers

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.

1 participant