chore: initialize recovered claude workspace
This commit is contained in:
204
src/tools/BriefTool/BriefTool.ts
Normal file
204
src/tools/BriefTool/BriefTool.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { feature } from 'bun:bundle'
|
||||
import { z } from 'zod/v4'
|
||||
import { getKairosActive, getUserMsgOptIn } from '../../bootstrap/state.js'
|
||||
import { getFeatureValue_CACHED_WITH_REFRESH } from '../../services/analytics/growthbook.js'
|
||||
import { logEvent } from '../../services/analytics/index.js'
|
||||
import type { ValidationResult } from '../../Tool.js'
|
||||
import { buildTool, type ToolDef } from '../../Tool.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
import { plural } from '../../utils/stringUtils.js'
|
||||
import { resolveAttachments, validateAttachmentPaths } from './attachments.js'
|
||||
import {
|
||||
BRIEF_TOOL_NAME,
|
||||
BRIEF_TOOL_PROMPT,
|
||||
DESCRIPTION,
|
||||
LEGACY_BRIEF_TOOL_NAME,
|
||||
} from './prompt.js'
|
||||
import { renderToolResultMessage, renderToolUseMessage } from './UI.js'
|
||||
|
||||
const inputSchema = lazySchema(() =>
|
||||
z.strictObject({
|
||||
message: z
|
||||
.string()
|
||||
.describe('The message for the user. Supports markdown formatting.'),
|
||||
attachments: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
'Optional file paths (absolute or relative to cwd) to attach. Use for photos, screenshots, diffs, logs, or any file the user should see alongside your message.',
|
||||
),
|
||||
status: z
|
||||
.enum(['normal', 'proactive'])
|
||||
.describe(
|
||||
"Use 'proactive' when you're surfacing something the user hasn't asked for and needs to see now — task completion while they're away, a blocker you hit, an unsolicited status update. Use 'normal' when replying to something the user just said.",
|
||||
),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
|
||||
// attachments MUST remain optional — resumed sessions replay pre-attachment
|
||||
// outputs verbatim and a required field would crash the UI renderer on resume.
|
||||
const outputSchema = lazySchema(() =>
|
||||
z.object({
|
||||
message: z.string().describe('The message'),
|
||||
attachments: z
|
||||
.array(
|
||||
z.object({
|
||||
path: z.string(),
|
||||
size: z.number(),
|
||||
isImage: z.boolean(),
|
||||
file_uuid: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.optional()
|
||||
.describe('Resolved attachment metadata'),
|
||||
sentAt: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'ISO timestamp captured at tool execution on the emitting process. Optional — resumed sessions replay pre-sentAt outputs verbatim.',
|
||||
),
|
||||
}),
|
||||
)
|
||||
type OutputSchema = ReturnType<typeof outputSchema>
|
||||
export type Output = z.infer<OutputSchema>
|
||||
|
||||
const KAIROS_BRIEF_REFRESH_MS = 5 * 60 * 1000
|
||||
|
||||
/**
|
||||
* Entitlement check — is the user ALLOWED to use Brief? Combines build-time
|
||||
* flags with runtime GB gate + assistant-mode passthrough. No opt-in check
|
||||
* here — this decides whether opt-in should be HONORED, not whether the user
|
||||
* has opted in.
|
||||
*
|
||||
* Build-time OR-gated on KAIROS || KAIROS_BRIEF (same pattern as
|
||||
* PROACTIVE || KAIROS): assistant mode depends on Brief, so KAIROS alone
|
||||
* must bundle it. KAIROS_BRIEF lets Brief ship independently.
|
||||
*
|
||||
* Use this to decide whether `--brief` / `defaultView: 'chat'` / `--tools`
|
||||
* listing should be honored. Use `isBriefEnabled()` to decide whether the
|
||||
* tool is actually active in the current session.
|
||||
*
|
||||
* CLAUDE_CODE_BRIEF env var force-grants entitlement for dev/testing —
|
||||
* bypasses the GB gate so you can test without being enrolled. Still
|
||||
* requires an opt-in action to activate (--brief, defaultView, etc.), but
|
||||
* the env var alone also sets userMsgOptIn via maybeActivateBrief().
|
||||
*/
|
||||
export function isBriefEntitled(): boolean {
|
||||
// Positive ternary — see docs/feature-gating.md. Negative early-return
|
||||
// would not eliminate the GB gate string from external builds.
|
||||
return feature('KAIROS') || feature('KAIROS_BRIEF')
|
||||
? getKairosActive() ||
|
||||
isEnvTruthy(process.env.CLAUDE_CODE_BRIEF) ||
|
||||
getFeatureValue_CACHED_WITH_REFRESH(
|
||||
'tengu_kairos_brief',
|
||||
false,
|
||||
KAIROS_BRIEF_REFRESH_MS,
|
||||
)
|
||||
: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified activation gate for the Brief tool. Governs model-facing behavior
|
||||
* as a unit: tool availability, system prompt section (getBriefSection),
|
||||
* tool-deferral bypass (isDeferredTool), and todo-nag suppression.
|
||||
*
|
||||
* Activation requires explicit opt-in (userMsgOptIn) set by one of:
|
||||
* - `--brief` CLI flag (maybeActivateBrief in main.tsx)
|
||||
* - `defaultView: 'chat'` in settings (main.tsx init)
|
||||
* - `/brief` slash command (brief.ts)
|
||||
* - `/config` defaultView picker (Config.tsx)
|
||||
* - SendUserMessage in `--tools` / SDK `tools` option (main.tsx)
|
||||
* - CLAUDE_CODE_BRIEF env var (maybeActivateBrief — dev/testing bypass)
|
||||
* Assistant mode (kairosActive) bypasses opt-in since its system prompt
|
||||
* hard-codes "you MUST use SendUserMessage" (systemPrompt.md:14).
|
||||
*
|
||||
* The GB gate is re-checked here as a kill-switch AND — flipping
|
||||
* tengu_kairos_brief off mid-session disables the tool on the next 5-min
|
||||
* refresh even for opted-in sessions. No opt-in → always false regardless
|
||||
* of GB (this is the fix for "brief defaults on for enrolled ants").
|
||||
*
|
||||
* Called from Tool.isEnabled() (lazy, post-init), never at module scope.
|
||||
* getKairosActive() and getUserMsgOptIn() are set in main.tsx before any
|
||||
* caller reaches here.
|
||||
*/
|
||||
export function isBriefEnabled(): boolean {
|
||||
// Top-level feature() guard is load-bearing for DCE: Bun can constant-fold
|
||||
// the ternary to `false` in external builds and then dead-code the BriefTool
|
||||
// object. Composing isBriefEntitled() alone (which has its own guard) is
|
||||
// semantically equivalent but defeats constant-folding across the boundary.
|
||||
return feature('KAIROS') || feature('KAIROS_BRIEF')
|
||||
? (getKairosActive() || getUserMsgOptIn()) && isBriefEntitled()
|
||||
: false
|
||||
}
|
||||
|
||||
export const BriefTool = buildTool({
|
||||
name: BRIEF_TOOL_NAME,
|
||||
aliases: [LEGACY_BRIEF_TOOL_NAME],
|
||||
searchHint:
|
||||
'send a message to the user — your primary visible output channel',
|
||||
maxResultSizeChars: 100_000,
|
||||
userFacingName() {
|
||||
return ''
|
||||
},
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
get outputSchema(): OutputSchema {
|
||||
return outputSchema()
|
||||
},
|
||||
isEnabled() {
|
||||
return isBriefEnabled()
|
||||
},
|
||||
isConcurrencySafe() {
|
||||
return true
|
||||
},
|
||||
isReadOnly() {
|
||||
return true
|
||||
},
|
||||
toAutoClassifierInput(input) {
|
||||
return input.message
|
||||
},
|
||||
async validateInput({ attachments }, _context): Promise<ValidationResult> {
|
||||
if (!attachments || attachments.length === 0) {
|
||||
return { result: true }
|
||||
}
|
||||
return validateAttachmentPaths(attachments)
|
||||
},
|
||||
async description() {
|
||||
return DESCRIPTION
|
||||
},
|
||||
async prompt() {
|
||||
return BRIEF_TOOL_PROMPT
|
||||
},
|
||||
mapToolResultToToolResultBlockParam(output, toolUseID) {
|
||||
const n = output.attachments?.length ?? 0
|
||||
const suffix = n === 0 ? '' : ` (${n} ${plural(n, 'attachment')} included)`
|
||||
return {
|
||||
tool_use_id: toolUseID,
|
||||
type: 'tool_result',
|
||||
content: `Message delivered to user.${suffix}`,
|
||||
}
|
||||
},
|
||||
renderToolUseMessage,
|
||||
renderToolResultMessage,
|
||||
async call({ message, attachments, status }, context) {
|
||||
const sentAt = new Date().toISOString()
|
||||
logEvent('tengu_brief_send', {
|
||||
proactive: status === 'proactive',
|
||||
attachment_count: attachments?.length ?? 0,
|
||||
})
|
||||
if (!attachments || attachments.length === 0) {
|
||||
return { data: { message, sentAt } }
|
||||
}
|
||||
const appState = context.getAppState()
|
||||
const resolved = await resolveAttachments(attachments, {
|
||||
replBridgeEnabled: appState.replBridgeEnabled,
|
||||
signal: context.abortController.signal,
|
||||
})
|
||||
return {
|
||||
data: { message, attachments: resolved, sentAt },
|
||||
}
|
||||
},
|
||||
} satisfies ToolDef<InputSchema, Output>)
|
||||
Reference in New Issue
Block a user