mirror of
https://github.com/theniceboy/.config.git
synced 2026-04-12 05:15:20 +08:00
agent tracker update
This commit is contained in:
parent
5064629d61
commit
0bfcb8d7c3
31 changed files with 1741 additions and 3320 deletions
|
|
@ -4,6 +4,23 @@
|
|||
- When migrating or refactoring code, do not leave legacy code. Remove all deprecated or unused code.
|
||||
- Put change reasoning in your plan/final message — not in code.
|
||||
|
||||
## Work Summary
|
||||
|
||||
- `set_work_summary` is mandatory protocol, not a suggestion.
|
||||
- Call `set_work_summary` at least once at the start of every busy turn before any substantive tool call, code change, research step, or substantive user-facing response.
|
||||
- Prefer calling it with both fields: `set_work_summary({ theme: "...", now: "..." })`.
|
||||
- `theme` answers: what is this pane about overall? Keep it stable across many turns.
|
||||
- `now` answers: what are you about to do next? Update it whenever the next concrete step changes.
|
||||
- Keep both labels concrete and under 48 characters.
|
||||
- Since the summary line has dedicated space, prefer richer phrases that help a forgetful human re-orient instantly.
|
||||
- Good `theme` examples: `Tmux status summary workflow`, `Agent tracker integration`, `Flutter auth onboarding`.
|
||||
- Good `now` examples: `Patch summary enforcement`, `Read restore path handling`, `Wait for user reply`.
|
||||
- Bad labels: `Working`, `Coding`, `Debugging`, `Researching`, `Task`, `Fixing stuff`.
|
||||
- Bad `now` phrasing: `Debugging summary enforcement`, `Reading restore path handling`, `Waiting on user reply`.
|
||||
- If you are blocked or waiting, keep the `theme` and change `now`, for example `Wait for user reply` or `Wait for tests`.
|
||||
- If the labels are missing or stale, stop and update them first.
|
||||
- Repeating the same `theme` across turns is acceptable when the overall mission has not changed.
|
||||
|
||||
---------
|
||||
|
||||
## Adaptive Burst Workflow
|
||||
|
|
|
|||
93
opencode/plugins/require_work_summary.ts
Normal file
93
opencode/plugins/require_work_summary.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import type { Plugin } from "@opencode-ai/plugin"
|
||||
|
||||
type TurnState = {
|
||||
summaryCalled: boolean
|
||||
}
|
||||
|
||||
const states = new Map<string, TurnState>()
|
||||
|
||||
function sessionState(sessionID: string) {
|
||||
let state = states.get(sessionID)
|
||||
if (!state) {
|
||||
state = { summaryCalled: false }
|
||||
states.set(sessionID, state)
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
function eventSessionID(event: any) {
|
||||
return String(
|
||||
event?.properties?.sessionID ||
|
||||
event?.properties?.session?.id ||
|
||||
event?.properties?.info?.sessionID ||
|
||||
"",
|
||||
).trim()
|
||||
}
|
||||
|
||||
export const RequireWorkSummaryPlugin: Plugin = async () => {
|
||||
return {
|
||||
"chat.message": async (input: any) => {
|
||||
const sessionID = String(input?.sessionID || "").trim()
|
||||
if (!sessionID) {
|
||||
return
|
||||
}
|
||||
sessionState(sessionID).summaryCalled = false
|
||||
},
|
||||
|
||||
"command.execute.before": async (input: any) => {
|
||||
const sessionID = String(input?.sessionID || "").trim()
|
||||
if (!sessionID) {
|
||||
return
|
||||
}
|
||||
sessionState(sessionID).summaryCalled = false
|
||||
},
|
||||
|
||||
"tool.execute.before": async (input: any) => {
|
||||
const sessionID = String(input?.sessionID || "").trim()
|
||||
if (!sessionID) {
|
||||
return
|
||||
}
|
||||
|
||||
const tool = String(input?.tool || "").trim()
|
||||
if (!tool) {
|
||||
return
|
||||
}
|
||||
|
||||
const state = sessionState(sessionID)
|
||||
if (tool === "set_work_summary") {
|
||||
return
|
||||
}
|
||||
|
||||
if (!state.summaryCalled) {
|
||||
throw new Error(
|
||||
"Call set_work_summary first with a specific theme and next-step now label before using other tools.",
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
"tool.execute.after": async (input: any) => {
|
||||
const sessionID = String(input?.sessionID || "").trim()
|
||||
if (!sessionID) {
|
||||
return
|
||||
}
|
||||
|
||||
const tool = String(input?.tool || "").trim()
|
||||
if (tool === "set_work_summary") {
|
||||
sessionState(sessionID).summaryCalled = true
|
||||
}
|
||||
},
|
||||
|
||||
event: async ({ event }) => {
|
||||
if (event?.type === "session.deleted") {
|
||||
const sessionID = eventSessionID(event)
|
||||
if (sessionID) {
|
||||
states.delete(sessionID)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default RequireWorkSummaryPlugin
|
||||
195
opencode/tools/set_work_summary.ts
Normal file
195
opencode/tools/set_work_summary.ts
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
import { tool } from "@opencode-ai/plugin"
|
||||
|
||||
const MAX_THEME_CHARS = 48
|
||||
const MAX_NOW_CHARS = 48
|
||||
const TMUX_THEME_OPTION = "@op_work_theme"
|
||||
const TMUX_NOW_OPTION = "@op_work_now"
|
||||
const TMUX_LEGACY_OPTION = "@op_work_summary"
|
||||
const TRACKER_BIN = `${process.env.HOME || ""}/.config/agent-tracker/bin/agent`
|
||||
const GENERIC_LABELS = new Set([
|
||||
"working",
|
||||
"coding",
|
||||
"debugging",
|
||||
"researching",
|
||||
"thinking",
|
||||
"fixing",
|
||||
"checking",
|
||||
"investigating",
|
||||
"task",
|
||||
"stuff",
|
||||
"project",
|
||||
"feature",
|
||||
"issue",
|
||||
"bug",
|
||||
])
|
||||
|
||||
function capitalizeFirstLetter(value: string): string {
|
||||
return value.replace(/[A-Za-z]/, (letter) => letter.toUpperCase())
|
||||
}
|
||||
|
||||
function normalizeLabel(value: string, maxChars: number): string {
|
||||
const collapsed = value
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/[.!,;:]+$/g, "")
|
||||
.trim()
|
||||
if (collapsed.length <= maxChars) {
|
||||
return capitalizeFirstLetter(collapsed)
|
||||
}
|
||||
|
||||
const words = collapsed.split(" ")
|
||||
let clipped = ""
|
||||
for (const word of words) {
|
||||
const next = clipped ? `${clipped} ${word}` : word
|
||||
if (next.length > maxChars) {
|
||||
break
|
||||
}
|
||||
clipped = next
|
||||
}
|
||||
if (clipped) {
|
||||
return capitalizeFirstLetter(clipped)
|
||||
}
|
||||
|
||||
return capitalizeFirstLetter(collapsed.slice(0, maxChars).trim())
|
||||
}
|
||||
|
||||
function ensureSpecific(label: string, field: string): string {
|
||||
if (!label) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if (GENERIC_LABELS.has(label.toLowerCase())) {
|
||||
throw new Error(
|
||||
`Use a more specific ${field}. Good examples: \"Tmux status summaries\", \"Patch work-context layout\", \"Wait for user reply\".`,
|
||||
)
|
||||
}
|
||||
|
||||
return label
|
||||
}
|
||||
|
||||
async function runTmux(tmuxBin: string, args: string[]) {
|
||||
const proc = Bun.spawn([tmuxBin, ...args], {
|
||||
stdin: "ignore",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
])
|
||||
if (exitCode !== 0) {
|
||||
const message = stderr.trim() || stdout.trim() || `tmux exited ${exitCode}`
|
||||
throw new Error(message)
|
||||
}
|
||||
return stdout.trim()
|
||||
}
|
||||
|
||||
async function readTmuxOption(tmuxBin: string, tmuxPane: string, option: string) {
|
||||
return runTmux(tmuxBin, ["display-message", "-p", "-t", tmuxPane, `#{${option}}`]).catch(() => "")
|
||||
}
|
||||
|
||||
async function writeTmuxOption(tmuxBin: string, tmuxPane: string, option: string, value: string) {
|
||||
if (!value) {
|
||||
await runTmux(tmuxBin, ["set-option", "-p", "-u", "-t", tmuxPane, option])
|
||||
return
|
||||
}
|
||||
|
||||
await runTmux(tmuxBin, ["set-option", "-p", "-t", tmuxPane, option, value])
|
||||
}
|
||||
|
||||
async function runCommand(args: string[]) {
|
||||
const proc = Bun.spawn(args, {
|
||||
stdin: "ignore",
|
||||
stdout: "ignore",
|
||||
stderr: "pipe",
|
||||
})
|
||||
const [stderr, exitCode] = await Promise.all([
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
])
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(stderr.trim() || `${args[0]} exited ${exitCode}`)
|
||||
}
|
||||
}
|
||||
|
||||
function trackerSummary(theme: string, now: string) {
|
||||
if (theme && now) {
|
||||
return `${theme} -> ${now}`
|
||||
}
|
||||
return theme || now
|
||||
}
|
||||
|
||||
export default tool({
|
||||
description:
|
||||
"Set the tmux pane's stable theme and immediate current-step labels for the current OpenCode session.",
|
||||
args: {
|
||||
theme: tool.schema
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Stable grand objective. Answer: what is this pane about overall? Keep it specific and under 48 characters. Prefer richer phrases like 'Tmux status summaries' or 'Agent tracker integration'.",
|
||||
),
|
||||
now: tool.schema
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Immediate next step. Answer: what are you about to do next? Keep it specific and under 48 characters. Use next-action phrasing like 'Read restore code', 'Patch status layout', or 'Wait for user reply'.",
|
||||
),
|
||||
summary: tool.schema
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Legacy alias for theme. Prefer using theme plus now."),
|
||||
},
|
||||
async execute(args) {
|
||||
const tmuxPane = (process.env.TMUX_PANE || "").trim()
|
||||
const tmuxBin = Bun.which("tmux") || "/opt/homebrew/bin/tmux"
|
||||
const hasTheme = typeof args.theme === "string"
|
||||
const hasNow = typeof args.now === "string"
|
||||
const hasSummary = typeof args.summary === "string"
|
||||
|
||||
if (!hasTheme && !hasNow && !hasSummary) {
|
||||
throw new Error("Provide at least one of: theme, now, or summary.")
|
||||
}
|
||||
|
||||
let theme = hasTheme
|
||||
? ensureSpecific(normalizeLabel(args.theme || "", MAX_THEME_CHARS), "theme")
|
||||
: ""
|
||||
const now = hasNow
|
||||
? ensureSpecific(normalizeLabel(args.now || "", MAX_NOW_CHARS), "current-step label")
|
||||
: ""
|
||||
|
||||
if (!hasTheme && hasSummary) {
|
||||
theme = ensureSpecific(normalizeLabel(args.summary || "", MAX_THEME_CHARS), "theme")
|
||||
}
|
||||
|
||||
if (!tmuxPane) {
|
||||
return JSON.stringify({ theme, now })
|
||||
}
|
||||
|
||||
if (!hasTheme && !hasSummary) {
|
||||
theme =
|
||||
(await readTmuxOption(tmuxBin, tmuxPane, TMUX_THEME_OPTION)) ||
|
||||
(await readTmuxOption(tmuxBin, tmuxPane, TMUX_LEGACY_OPTION))
|
||||
}
|
||||
|
||||
const finalNow = hasNow ? now : await readTmuxOption(tmuxBin, tmuxPane, TMUX_NOW_OPTION)
|
||||
const finalTheme = theme
|
||||
|
||||
if (hasTheme || hasSummary) {
|
||||
await writeTmuxOption(tmuxBin, tmuxPane, TMUX_THEME_OPTION, theme)
|
||||
await writeTmuxOption(tmuxBin, tmuxPane, TMUX_LEGACY_OPTION, theme)
|
||||
}
|
||||
|
||||
if (hasNow) {
|
||||
await writeTmuxOption(tmuxBin, tmuxPane, TMUX_NOW_OPTION, now)
|
||||
}
|
||||
|
||||
const trackerText = trackerSummary(finalTheme, finalNow)
|
||||
if (trackerText && (await Bun.file(TRACKER_BIN).exists())) {
|
||||
await runCommand([TRACKER_BIN, "tracker", "command", "-pane", tmuxPane, "-summary", trackerText, "update_task"]).catch(() => {})
|
||||
}
|
||||
|
||||
await runTmux(tmuxBin, ["refresh-client", "-S"])
|
||||
return JSON.stringify({ theme: finalTheme, now: finalNow })
|
||||
},
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue