diff --git a/.gitignore b/.gitignore index 8e343d3..00cff47 100755 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,5 @@ tmux/cache agent-tracker/.DS_Store agent-tracker/run !starship-tmux.toml + +!/opencode/ diff --git a/opencode/AGENTS.md b/opencode/AGENTS.md new file mode 100644 index 0000000..648f00a --- /dev/null +++ b/opencode/AGENTS.md @@ -0,0 +1,40 @@ +## Hard Rule: No Change‑Note Comments In Code + +- Agents MUST NOT add comments that describe the change they just made (e.g., “removed”, “legacy”, “cleanup”, “hotfix”, “flag removed”, “temporary workaround”). +- Only add comments for genuinely non‑obvious, persistent logic or external invariants. Keep such comments short (max 2 lines). + +Forbidden examples: +- // shouldShowDoneButton removed; UI reacts to selection +- // legacy code kept for now +- // temporary cleanup / hotfix + +Allowed examples (non‑obvious logic): +- // Bound must be >= 30px to render handles reliably +- // Server returns seconds (not ms); convert before diffing + +Rationale placement: +- Put change reasoning in your plan/final message or PR description — not in code. + +CRITICAL WORKFLOW REQUIREMENT +- When the user asks for something but there's ambiguity, you must always ask for clarification before proceeding. Provide users some options. +- When giving user responses, give short and concise answers. Avoid unnecessary verbosity. +- Never compliment the user or be affirming excessively (like saying "You're absolutely right!" etc). Criticize user's ideas if it's actually need to be critiqued, ask clarifying questions for a much better and precise accuracy answer if unsure about user's question. +- Avoid getting stuck. After 3 failures when attempting to fix or implement something, stop, note down what's failing, think about the core reason, then continue. +- When migrating or refactoring code, do not leave legacy code. Remove all deprecated or unused code. + +Other recommendations: +- When giving the user bullet lists, use different bullet characters for different levels +- Use numbered lists for options/confirmations. +- Prompt users to reply compactly (e.g., "1Y 2N 3Y"). +- Default to numbers for multi-step plans and checklists. + +--------- + +## Code Change Guidelines + +- No useless comments or code. Only comment truly complex logic. +- No need to write a comment like "removed foo" after removing code +- Keep diffs minimal and scoped; do not add files/utilities unless required. +- Prefer existing mechanisms +- Remove dead code, unused imports, debug prints, and extra empty lines. +- Do not leave temporary scaffolding; revert anything not needed. diff --git a/opencode/opencode.json b/opencode/opencode.json new file mode 100644 index 0000000..d817dfb --- /dev/null +++ b/opencode/opencode.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://opencode.ai/config.json", + "small_model": "zai-coding-plan/glm-4.6", + "lsp": { + "dart": { + "command": [ + "dart", + "language-server" + ], + "extensions": [ + ".dart" + ] + } + }, + "permission": { + "edit": "allow", + "bash": "allow", + "webfetch": "allow", + "doom_loop": "allow", + "external_directory": "allow" + }, + "mcp": { + "web": { + "type": "local", + "command": [ + "mctrl-mcp", + "web" + ], + "enabled": true + } + } +} diff --git a/opencode/plugin/tracker-notify.js b/opencode/plugin/tracker-notify.js new file mode 100644 index 0000000..c437538 --- /dev/null +++ b/opencode/plugin/tracker-notify.js @@ -0,0 +1,127 @@ +const NOTIFY_BIN = "/usr/bin/python3"; +const NOTIFY_SCRIPT = "/Users/david/.config/codex/notify.py"; +const MAX_SUMMARY_CHARS = 600; +const TRACKER_BIN = "/Users/david/.config/agent-tracker/bin/tracker-client"; +const finishedSessions = new Set(); // track by sessionID + +export const TrackerNotifyPlugin = async ({ client, directory, $ }) => { + const trackerReady = async () => { + const check = await $`test -x ${TRACKER_BIN}`.nothrow(); + return check?.exitCode === 0; + }; + + const summarizeText = (parts = []) => { + const text = parts + .filter((p) => p?.type === "text" && !p.ignored) + .map((p) => p.text || "") + .join("\n") + .trim(); + return text.slice(0, MAX_SUMMARY_CHARS); + }; + + const collectUserInputs = (messages) => { + return messages + .filter((m) => m?.info?.role === "user") + .slice(-3) + .map((m) => summarizeText(m.parts)) + .filter((text) => text); + }; + + const startTask = async (summary) => { + if (!summary) return; + if (!(await trackerReady())) return; + await $`${TRACKER_BIN} command -summary ${summary} start_task`.nothrow(); + }; + + const finishTask = async (summary) => { + if (!(await trackerReady())) return; + await $`${TRACKER_BIN} command -summary ${summary || "done"} finish_task`.nothrow(); + }; + + const notify = async (sessionID) => { + try { + const messages = + (await client.session.messages({ + path: { id: sessionID }, + query: { directory }, + })) || []; + + const assistant = [...messages] + .reverse() + .find((m) => m?.info?.role === "assistant"); + if (!assistant) return; + + const assistantText = summarizeText(assistant.parts); + if (!assistantText) return; + + const payload = { + type: "agent-turn-complete", + "last-assistant-message": assistantText, + input_messages: collectUserInputs(messages), + }; + + const serialized = JSON.stringify(payload); + try { + await $`${NOTIFY_BIN} ${NOTIFY_SCRIPT} ${serialized}`; + } catch { + // ignore notification failures but still finish + } + } catch { + // Ignore notification failures + } + }; + + const finishOnce = async (sessionID, summary) => { + if (!sessionID || finishedSessions.has(sessionID)) return; + finishedSessions.add(sessionID); + await finishTask(summary?.slice(0, 200) || ""); + }; + + return { + event: async ({ event }) => { + // On idle: ensure finish, even if message hook missed + if (event?.type === "session.idle" && event?.properties?.sessionID) { + const sessionID = event.properties.sessionID; + let text = ""; + try { + const messages = + (await client.session.messages({ + path: { id: sessionID }, + query: { directory }, + })) || []; + const assistant = [...messages] + .reverse() + .find((m) => m?.info?.role === "assistant"); + if (assistant) text = summarizeText(assistant.parts); + } catch { + // ignore fetch errors + } + await finishOnce(sessionID, text || "done"); + await notify(sessionID); + } + }, + "chat.message": async (_input, output) => { + if (output?.message?.role !== "user") return; + const summary = summarizeText(output.parts).slice(0, 200); + await startTask(summary); + }, + "message.updated": async ({ event }) => { + if (event?.properties?.info?.role !== "assistant") return; + const sessionID = event?.properties?.info?.sessionID; + if (!sessionID) return; + + const parts = event?.properties?.parts || []; + const text = summarizeText(parts) || "done"; + + // Prefer explicit finish flag; otherwise if we see any assistant message update after start, finish once. + const isFinished = + event?.properties?.info?.finish || + event?.properties?.info?.summary || + false; + + if (isFinished || !finishedSessions.has(sessionID)) { + await finishOnce(sessionID, text); + } + }, + }; +};