feat: spawn templates with behavioral prompts#75
Conversation
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
left a comment
There was a problem hiding this comment.
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.
| { | ||
| const names = getTemplateNames() | ||
| if (names.length > 0) { | ||
| const pattern = new RegExp(`^(${names.join('|')}):\\s*([\\s\\S]*)$`, 'i') |
There was a problem hiding this comment.
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
| } | ||
|
|
||
| const designMatch = msg.content.match(/^(?:\/design|design):\s*([\s\S]+)$/i) | ||
| const designMatch = msg.content.match(/^(?:\/design|design)\s+([\s\S]+)$/i) |
There was a problem hiding this comment.
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
| const action = template.template.action | ||
| const actionTopic = `${template.template.prompt} — ${topic}` | ||
| try { | ||
| switch (action) { |
There was a problem hiding this comment.
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
|
|
||
| if (template?.template.action) { | ||
| const action = template.template.action | ||
| const actionTopic = `${template.template.prompt} — ${topic}` |
There was a problem hiding this comment.
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
| if (templateCmdMatch) { | ||
| const candidateName = templateCmdMatch[1].trim() | ||
| const candidateTopic = templateCmdMatch[2].trim() | ||
| const template = getTemplate(candidateName)! |
There was a problem hiding this comment.
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
|
|
||
| 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 | ||
|
|
There was a problem hiding this comment.
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
| import { getActiveBuilds, cancelBuild } from '../build.js' | ||
| import { getActiveReviews, cancelReview } from '../adversarial.js' | ||
| import { startDesign } from '../design.js' | ||
| import { startReview } from '../adversarial.js' |
There was a problem hiding this comment.
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
| ): Promise<void> { | ||
| void gateway.react(msg.channelId, msg.id, '🚀').catch(() => {}) | ||
| const chatId = await resolveSpawnTarget(msg) | ||
| const label = template ? `${template.name}` : null |
There was a problem hiding this comment.
Nit — Redundant template literal. `${template.name}` is equivalent to template.name — no interpolation needed.
Flagged by: typescript-reviewer
| await spawnAndNotify(msg, topic) | ||
| } | ||
|
|
||
| export async function handleTemplateSpawn(msg: InboundMessage, templateName: string, topic: string, template: SpawnTemplate, access: Access): Promise<void> { |
There was a problem hiding this comment.
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
Summary
review,fix,investigate,incidenttemplates.jsonin the state dir (hot-reloadable, validated)spawn:andspawn-wt:commands/templatescommand lists available templatesUsage
Custom templates
Drop a
templates.jsonin~/.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 + worktreespawn: fix something→ fix template, no worktreespawn: random topic→ generic spawn, no templatespawn: review(no topic) → generic spawn + hint messagespawn-wt: treasury fix something→ fix template + worktreetemplates→ lists all templates with source labels🤖 Generated with Claude Code