Skip to content

feat(daemon): auto-spawn session for top-level design:#74

Open
kwliang1 wants to merge 1 commit into
sf8193:mainfrom
kwliang1:kevinliang/design-auto-spawn-v2
Open

feat(daemon): auto-spawn session for top-level design:#74
kwliang1 wants to merge 1 commit into
sf8193:mainfrom
kwliang1:kevinliang/design-auto-spawn-v2

Conversation

@kwliang1

@kwliang1 kwliang1 commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • When design: <topic> is typed at the top-level DM (not in a session thread), auto-spawns a session and starts the design protocol in its thread
  • Spawned session uses a custom buildDesignHostPrompt that keeps it quiet during the design and activates after the brief is posted
  • Extended promptBuilder callback signature to include threadId so the host session knows its thread
  • In-thread design: behavior unchanged — uses existing session, no spawn
  • Hides join-member sessions (design personas, build/review critics) from the Home tab and list sessions — they cluttered the view alongside real sessions

Files changed

File Change
daemon/router.ts Top-level design: match outside if (msg.isThread)
daemon/commands/design.ts New handleDesignSpawnIntercept handler
daemon/prompts/session.ts New buildDesignHostPrompt
daemon/session-lifecycle.ts Pass threadId to promptBuilder call
daemon/sessions.ts Extended promptBuilder type signature
daemon/dashboard.ts Filter isJoinMember from Home tab
daemon/commands/status.ts Filter isJoinMember from list sessions and auto-refresh
daemon/__tests__/router-commands.test.ts 6 new tests for design: regex

Test plan

  • Type design: <topic> in top-level DM → session spawns, design protocol starts in its thread
  • Type design: <topic> in a session thread → existing behavior (no spawn)
  • Verify spawned session stays quiet during design (no greeting)
  • Verify session activates after design brief is posted
  • Home tab no longer shows persona/critic sub-sessions
  • list sessions no longer shows persona/critic sub-sessions
  • bun test — 274 pass, 0 fail

🤖 Generated with Claude Code

When `design: <topic>` is typed in the top-level DM (not in a session
thread), the daemon now automatically spawns a session and starts the
design protocol in its thread — no manual `spawn:` needed first.

The spawned session uses a custom `buildDesignHostPrompt` that tells it
to stay quiet during the design and help implement the brief once
complete. Extends `promptBuilder` signature to pass `threadId` so the
host session knows its thread.

In-thread behavior is unchanged: `design:` inside a session thread
still uses that session without spawning.

Also hides join-member sessions (design personas, build/review critics)
from the Home tab and `list sessions` output — they cluttered the view
alongside real sessions.

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

@sf8193 sf8193 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Review by sp-reviewer (architecture) + typescript-reviewer (correctness) — 5 findings (1 blocker, 2 should-fix, 2 nits).

Comment thread daemon/sessions.ts
resurrectFrom?: string // tmuxName of predecessor (for lineage in respawn)
joinThread?: string // join existing thread as member (skip thread creation)
promptBuilder?: (sessionId: string, tmuxName: string) => string
promptBuilder?: (sessionId: string, tmuxName: string, threadId: string) => string

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Blocker (flagged by both reviewers): promptBuilder signature widened from 2 to 3 params, but all existing callers in design.ts, build.ts, and adversarial.ts still pass 2-arg lambdas. JS silently ignores the extra arg so no runtime crash, but the type contract is now a lie — if strict function-type checking is ever enabled, every existing caller will fail to compile.

Fix: Make threadId optional (threadId?: string) in the signature, or better yet, don't widen it at all — have handleDesignSpawnIntercept close over threadId from the doSpawnSession result (like every other caller already does) and keep the signature at two params.

Comment thread daemon/commands/design.ts

debouncedRefreshListDisplay()

await startDesign(result.threadId, topic)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Should-fix: If startDesign throws, the catch block sends an error message but never cleans up the session spawned on line 46. The user is left with an orphaned tmux session + thread that has no design running in it. The existing in-thread handleDesignIntercept doesn't have this problem because it reuses an existing session.

Fix: In the catch block, kill the spawned session before sending the error message:

} catch (err) {
  // clean up the orphaned session
  const sess = registry.get(result.name)
  if (sess) killSession(sess, 'design failed to start')
  ...
}

(Note: result needs to be declared outside the try block for this, or restructure slightly.)

test('does not match without colon', () => {
expect('design something'.match(DESIGN_RE)).toBeNull()
})

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Should-fix: Test name says "does not match bare design:" but the assertion on line 289 is not.toBeNull() — it does match, producing an empty topic after .trim(). The test name contradicts the assertion.

More importantly, this exposes a real gap: design: (trailing whitespace only) will pass through the router and spawn a session with topic "". For a top-level command that auto-spawns ~7 sessions (personas + synthesizer + auditor + brief writer), silently accepting an empty topic is risky.

Fix: Either tighten the regex to require at least one non-whitespace char after the colon (e.g. \s*([\s\S]*\S[\s\S]*)$), or add an empty-topic guard in handleDesignSpawnIntercept. Then update the test name/assertion to match.

Comment thread daemon/commands/status.ts
if (lastListMsgs.length === 0) return
const now = Date.now()
const all = [...registry.values()].filter(s => isAlive(s)).sort((a, b) => b.lastActive - a.lastActive)
const all = [...registry.values()].filter(s => isAlive(s) && !s.isJoinMember).sort((a, b) => b.lastActive - a.lastActive)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Nit: !s.isJoinMember is now added in three places (here, line 163, and dashboard.ts:39). Consider a shared isVisibleSession(s) predicate in sessions.ts to centralize this — it's load-bearing for the user-facing session list and easy to miss if a fourth display site is added.

Comment thread daemon/commands/design.ts
topic,
}),
})

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Nit: Only DM users get a confirmation message with the thread URL. Channel-initiated design: gets just the 🎨 reaction — no link to the spawned thread. Consistent with handleSpawnIntercept, but design is heavier (multi-session), so a brief channel reply with the thread URL might be worth adding.

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.

2 participants