diff --git a/dot_claude/output-styles/private_strict.md b/.chezmoitemplates/strict-output-style.md.tmpl similarity index 98% rename from dot_claude/output-styles/private_strict.md rename to .chezmoitemplates/strict-output-style.md.tmpl index 7269520..fc8d847 100644 --- a/dot_claude/output-styles/private_strict.md +++ b/.chezmoitemplates/strict-output-style.md.tmpl @@ -1,9 +1,3 @@ ---- -name: Strict -description: List-first output, fixed order, code-anchored, no filler — tuned for low visual load -keep-coding-instructions: true ---- - ## The shape of every answer This shape governs conversational replies to the user, and nothing else. Do not diff --git a/dot_claude/output-styles/private_strict.md.tmpl b/dot_claude/output-styles/private_strict.md.tmpl new file mode 100644 index 0000000..bd2892d --- /dev/null +++ b/dot_claude/output-styles/private_strict.md.tmpl @@ -0,0 +1,7 @@ +--- +name: Strict +description: List-first output, fixed order, code-anchored, no filler — tuned for low visual load +keep-coding-instructions: true +--- + +{{ template "strict-output-style.md.tmpl" . -}} diff --git a/dot_codex/AGENTS.md.tmpl b/dot_codex/AGENTS.md.tmpl index e32d2b6..13bdbd2 100644 --- a/dot_codex/AGENTS.md.tmpl +++ b/dot_codex/AGENTS.md.tmpl @@ -18,4 +18,5 @@ unavailable. {{ template "global-ai-workflow-guidelines.md.tmpl" . }} +{{ template "strict-output-style.md.tmpl" . }} @~/.codex/RTK.md diff --git a/dot_codex/hooks/executable_reinforce-git-consent.sh b/dot_codex/hooks/executable_reinforce-git-consent.sh new file mode 100644 index 0000000..411afa1 --- /dev/null +++ b/dot_codex/hooks/executable_reinforce-git-consent.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Reinforce the commit and push consent rule on every Codex user turn. +set -euo pipefail + +reminder="Git consent reminder: +Commit or push only when the user asks for it as part of the current task (an ongoing piece of work, not a single agent run). +Permission lasts until that task ends and does not carry over to a later task, even in the same session. When unsure whether the task has changed, ask. +A commit request does not include pushing; a push needs its own request. +A request to create a pull request on an unpushed branch allows exactly one push to publish that branch; later pushes need a new request." + +jq -cn --arg ctx "$reminder" \ + '{hookSpecificOutput: {hookEventName: "UserPromptSubmit", additionalContext: $ctx}}' diff --git a/dot_codex/hooks/executable_reinforce-strict-output-style.sh b/dot_codex/hooks/executable_reinforce-strict-output-style.sh new file mode 100644 index 0000000..9ade5af --- /dev/null +++ b/dot_codex/hooks/executable_reinforce-strict-output-style.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Reinforce the Strict output style on every Codex user turn. +set -euo pipefail + +reminder="Strict output-style reminder (conversational replies only, never inside files): +Use exactly these five sections, with these headings and emojis, in this order, and no others: +1. 🔍 Findings/Context +2. ⚠️ Limits/Risks +3. ✅ Done +4. ✋ Recommendations +5. ➡️ Next +Findings holds only facts about the current state, never a proposed change. Recommendations holds every proposal in full (a plan or set of edits awaiting approval, even if it is the only option; which option you favor and why; optional follow-ups); Next is required actions and decisions, such as the approval request for that plan. A required decision you also have an opinion on stays as one Next item with the pick stated inline. +Omit a section only when it is empty. Do not rename, merge, or invent sections. Keep the whole block together at the end of the reply. Escape hatch: a one or two line answer may drop all five headings together, never a subset." + +jq -cn --arg ctx "$reminder" \ + '{hookSpecificOutput: {hookEventName: "UserPromptSubmit", additionalContext: $ctx}}' diff --git a/private_dot_config/opencode/AGENTS.md.tmpl b/private_dot_config/opencode/AGENTS.md.tmpl index 19f208c..ca5f51b 100644 --- a/private_dot_config/opencode/AGENTS.md.tmpl +++ b/private_dot_config/opencode/AGENTS.md.tmpl @@ -14,3 +14,4 @@ delegation. {{ template "global-ai-workflow-guidelines.md.tmpl" . }} +{{ template "strict-output-style.md.tmpl" . -}} diff --git a/private_dot_config/opencode/plugins/git-consent.ts b/private_dot_config/opencode/plugins/git-consent.ts new file mode 100644 index 0000000..1d6a770 --- /dev/null +++ b/private_dot_config/opencode/plugins/git-consent.ts @@ -0,0 +1,25 @@ +const reminder = `Git consent reminder: +Commit or push only when the user asks for it as part of the current task (an ongoing piece of work, not a single agent run). +Permission lasts until that task ends and does not carry over to a later task, even in the same session. When unsure whether the task has changed, ask. +A commit request does not include pushing; a push needs its own request. +A request to create a pull request on an unpushed branch allows exactly one push to publish that branch; later pushes need a new request.` + +type ContextEvent = { + system: Array<{ type: "text"; text: string }> +} + +type PluginContext = { + session: { + hook(name: "context", handler: (event: ContextEvent) => void): Promise + } +} + +export default { + id: "git-consent", + + async setup(ctx: PluginContext) { + await ctx.session.hook("context", event => { + event.system.push({ type: "text", text: reminder }) + }) + }, +} diff --git a/private_dot_config/opencode/plugins/strict-output-style.ts b/private_dot_config/opencode/plugins/strict-output-style.ts new file mode 100644 index 0000000..7420790 --- /dev/null +++ b/private_dot_config/opencode/plugins/strict-output-style.ts @@ -0,0 +1,29 @@ +const reminder = `Strict output-style reminder (conversational replies only, never inside files): +Use exactly these five sections, with these headings and emojis, in this order, and no others: +1. 🔍 Findings/Context +2. ⚠️ Limits/Risks +3. ✅ Done +4. ✋ Recommendations +5. ➡️ Next +Findings holds only facts about the current state, never a proposed change. Recommendations holds every proposal in full (a plan or set of edits awaiting approval, even if it is the only option; which option you favor and why; optional follow-ups); Next is required actions and decisions, such as the approval request for that plan. A required decision you also have an opinion on stays as one Next item with the pick stated inline. +Omit a section only when it is empty. Do not rename, merge, or invent sections. Keep the whole block together at the end of the reply. Escape hatch: a one or two line answer may drop all five headings together, never a subset.` + +type ContextEvent = { + system: Array<{ type: "text"; text: string }> +} + +type PluginContext = { + session: { + hook(name: "context", handler: (event: ContextEvent) => void): Promise + } +} + +export default { + id: "strict-output-style", + + async setup(ctx: PluginContext) { + await ctx.session.hook("context", event => { + event.system.push({ type: "text", text: reminder }) + }) + }, +} diff --git a/run_after_ensure-codex-hooks.sh b/run_after_ensure-codex-hooks.sh new file mode 100644 index 0000000..de31be3 --- /dev/null +++ b/run_after_ensure-codex-hooks.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Register global reminder hooks without owning Codex's hooks file. Existing +# hooks and unrelated configuration remain unchanged. + +CONFIG_DIR="$HOME/.codex" +CONFIG_FILE="$CONFIG_DIR/hooks.json" +# Codex expands HOME when it runs the registered commands. +# shellcheck disable=SC2016 +HOOK_COMMANDS=( + '"$HOME/.codex/hooks/reinforce-strict-output-style.sh"' + '"$HOME/.codex/hooks/reinforce-git-consent.sh"' +) + +if ! command -v codex &>/dev/null; then + exit 0 +fi + +if ! command -v jq &>/dev/null; then + echo "Cannot merge Codex hooks: jq is not installed." >&2 + exit 1 +fi + +mkdir -p "$CONFIG_DIR" + +if [[ -f "$CONFIG_FILE" ]]; then + if ! jq -e ' + type == "object" and + ( + if has("hooks") then + (.hooks | type == "object") and + ( + if (.hooks | has("UserPromptSubmit")) then + (.hooks.UserPromptSubmit | type == "array") and + all( + .hooks.UserPromptSubmit[]; + type == "object" and + ((has("hooks") | not) or (.hooks | type == "array")) + ) + else + true + end + ) + else + true + end + ) + ' "$CONFIG_FILE" >/dev/null; then + echo "Cannot merge Codex hooks: $CONFIG_FILE has an unsupported structure." >&2 + exit 1 + fi + + if jq -e --args ' + . as $root | + $ARGS.positional as $commands | + all( + $commands[]; + . as $command | + ([ + $root.hooks.UserPromptSubmit[]?.hooks[]? + | select(.type == "command" and .command == $command) + ] | length) == 1 and + ([ + $root.hooks.UserPromptSubmit[]?.hooks[]? + | select(. == {type: "command", command: $command, timeout: 5}) + ] | length) == 1 + ) + ' "${HOOK_COMMANDS[@]}" <"$CONFIG_FILE" >/dev/null; then + exit 0 + fi +fi + +tmp="$(mktemp "${CONFIG_FILE}.tmp.XXXXXX")" +trap 'rm -f "$tmp"' EXIT + +# Dollar-prefixed names in this filter are jq variables. +# shellcheck disable=SC2016 +merge_filter=' + def is_owned($commands): + .type == "command" and + (.command as $command | $commands | index($command) != null); + + $ARGS.positional as $commands | + .hooks //= {} | + .hooks.UserPromptSubmit //= [] | + .hooks.UserPromptSubmit |= ( + map( + if ((.hooks // []) | any(.[]; is_owned($commands))) then + .hooks |= map(select(is_owned($commands) | not)) | + if (.hooks | length) == 0 then empty else . end + else + . + end + ) + [ + $commands[] | + {hooks: [{type: "command", command: ., timeout: 5}]} + ] + ) +' + +if [[ -f "$CONFIG_FILE" ]]; then + jq --args "$merge_filter" "${HOOK_COMMANDS[@]}" <"$CONFIG_FILE" >"$tmp" + + if chmod --reference="$CONFIG_FILE" "$tmp" 2>/dev/null; then + : + else + chmod "$(stat -f '%Lp' "$CONFIG_FILE")" "$tmp" + fi +else + jq -n --args "$merge_filter" "${HOOK_COMMANDS[@]}" >"$tmp" +fi + +mv "$tmp" "$CONFIG_FILE" +trap - EXIT