- Body/title edits need the draft issue's content ID (
DI_...), not the project item ID:gh project item-edit 3 --owner a1exk-dev --id DI_... --body "...". TheDI_ID is incontent.idofgh project item-list 3 --owner a1exk-dev --format json. - This environment's gh (2.101.0) rejects
--owner a1exk-devwith "unknown owner type" ongh projectcalls; use--owner "@me"instead (verified 2026-09-27). --fieldcannot be combined with--id; Status (single-select) is set via node IDs:gh project item-edit --id PVTI_... --field-id PVTSSF_lAHOANlzg84Bky-JzhjiP-Y --project-id PVT_kwHOANlzg84Bky-J --single-select-option-id <option-id>. Option IDs as of 2026-09-27: Todof75ad846, In Progress47fc9ee4, Done98236657. Re-resolve if a call errors.- Field and option IDs resolve via GraphQL:
gh api graphql -f query='{ user(login:"a1exk-dev") { projectV2(number:3) { fields(first:20) { nodes { ... on ProjectV2Field { id name dataType } ... on ProjectV2SingleSelectField { id name options { id name } } } } } } }'(this environment's schema has noProjectV2CustomFieldtype, so do not fragment on it).
- The research note on branch
research/opencode-plugin-apiconcludes no plugin API exists to intercept raw key events. That is wrong:api.renderer.keyInput.on("keypress", ...)(opentuiKeyHandler, node-EventEmitter style) receives every parsed key. Global listeners run before the focused editor's handler, so at key event time the pressed character is not in the buffer yet; the prompt's current state is readable viaapi.renderer.currentFocusedEditor(plainText,cursorOffset), and the buffer supportsinsertText().key.preventDefault()suppresses the typed slash before it reaches the editor. Unsubscribe withkeyInput.offwired throughapi.lifecycle.onDispose. Verified in@opentui/core0.4.5 source and a fresh opencode 1.18.32 TUI session. - The prompt's slash menu has no command or API to open it; it only (re)opens when
onInputsees a buffer that starts with/and has no whitespace before the cursor. An existing first skill with the cursor before its trailing space also meets that condition, but selecting there erases the first skill. Useapi.mode.current() === "autocomplete"to distinguish an active built-in menu from that case. For subsequent slashes,api.ui.dialog.replace(() => api.ui.DialogSelect(...))shows the skill list without changing the draft. Selection callseditor.insertText()at the saved cursor offset.command.palette.showopens the Ctrl+P command list, a different menu. - OpenCode 1.18.32's built-in slash-menu Esc deletes the prefix through the cursor; its Esc handler stops propagation before ordinary
keyInput.on("keypress", ...)listeners.keyInput.prependListener("keypress", ...)runs first. Setting the prompt cursor to 0 during Esc lets the built-in menu close without deleting/; restore the cursor in a resolved Promise callback. For the plugin's later-slash dialog,dialog.replace(render, onClose)invokesonCloseon Esc and also on selection'sdialog.clear(): track selection so only dismissal inserts/. Insert it while the cursor is saved, then move the cursor to 0 until the asynchronousonContentChangehas run; otherwise OpenCode reopens its built-in slash menu when the insertion is before the first whitespace. Verified in a live TUI.
- OpenCode 1.18.34 also opens the built-in menu for a
/typed at position 0 before text and for a/typed inside the first word. The quiet insert insrc/tui.tskeeps it closed in both cases, and it closes a menu that is already open when the/lands at position 0. Typing or backspacing inside that leading word later still reopens the menu. - Live-test with tmux:
XDG_CONFIG_HOME=<scratch> opencode, where<scratch>/opencode/tui.jsonlists only thefile://build, so the globally installed copy does not also handle keys. A run with no plugin shows the unmodified behavior.ctrl+udeletes only to the cursor, so clear a prompt withEndfirst andctrl+usecond, or restart the session between cases. Switching models mid-session opens a Select variant dialog that swallows every keystroke untilEsc.
- Sandbox a 2.x host without disturbing the installed 1.x one:
npm install @opencode/cli@2.0.27into a scratch directory, then runnode_modules/.bin/opencodewithXDG_CONFIG_HOME,XDG_CACHE_HOME,XDG_DATA_HOMEandXDG_STATE_HOMEall pointed inside that tree. 2.0.27 honours all four, so the real~/.config/opencodeand~/.cache/opencodestay untouched. npm blocks the package'spostinstalland the binary still runs. - Given a config holding only a
tui.json, the first launch writescli.jsonnext to it withplugins: ["<name>"]and leavestui.jsonin place, then installs the package into$XDG_CACHE_HOME/opencode/npm/<name>@latest/<ms-epoch>/node_modules/<name>/. The generation directory is a millisecond epoch rather than a sequence number, andnpm/is a separate root from 1.x'spackages/. - A v1-shaped
{ id, tui }default export fails in three places at once: an error toast (Plugin failed: <name>/Run /plugins to view details.) that appears about two seconds after launch and lasts about five, a persistent1 plugin failed /pluginsitem in the status bar, and a/pluginsrow markedfailedwhose dialog is titled "TUI plugin error" and readsInvalid V2 TUI plugin module: <name>. export default { id, tui, setup }satisfies both hosts from one file. On 1.19.0 the plugin loads and the skill rows work,setupuntouched; on 2.0.27setupruns,/pluginslists the plugin as loaded with adisableaction, and the status-bar failure item is gone.context.ui.toast.show({ variant, title, message })called fromsetupreaches the screen. The message wraps across the toast box instead of being clipped, andvariant: "warning"draws an amber border (RGB 245,167,66). The cost of being accepted is that the persistent status-bar item disappears, so a user who misses the five-second toast has nothing left to read.- Patching the cached copy swaps the live module on 2.x as it does on 1.x: rename
<generation>/node_modules/<name>/dist/tui.jsaside and write a wrapper that re-exports it with the extra key. A running session keeps the old code until restart.
- TUI plugins load only from the
pluginarray of atui.json(global config,OPENCODE_CONFIG_DIR, or.opencode/tui.json). Files in.opencode/plugins/*.jsare NOT auto-loaded as TUI plugins; the server runtime auto-globs that directory and fails a TUI-only plugin there with "must default export an object with server()". - A file-path plugin (absolute path or
file://in atui.jsonpluginarray) must exportidon the default export object ({ tui, id }); npm-package plugins get the id from the package name. Withoutid, the TUI loader drops the plugin silently. - TUI loader diagnostics go to
console.errorand do not reach the terminal pty; opencode's own log (~/.local/share/opencode/log/opencode.log) only records the server-side plugin load, not TUI plugin activation. - The npm-cached copy of an installed plugin lives at
~/.cache/opencode/packages/<name>/node_modules/<name>/dist/tui.js(and a<name>@latesttwin). Editing that file swaps the live plugin without republishing; a running session keeps the old code until restart. - OpenCode 1.18.32 binds
ctrl+ktoinput.delete.to.line.endin its effective TUI keybinds, so the plugin's conflict check skips that shortcut with a startup warning.ctrl+shas no default TUI binding and opens the palette when registered by the plugin (checked in a fresh TUI).OPENCODE_CONFIG_DIRadds TUI plugins to the global list; for a smoke test of a local build alongside an installed package, give a file-path wrapper a distinct pluginid, or the duplicate id is skipped.
- Maintainer policy (2026-09-27): merge completed, CI-green ticket work into
developper ticket, then branch the next ticket fromdevelop. First execution: PR #1 (squash) →developatc319b9f. Per ticket: work onfeature/<ticket-slug>offdevelop, push, PR todevelop, squash-merge once the CIcheckjob is green, then move on. - Branch protection: ruleset
protect-branches(id 24085471) coversmain+develop: pull request required (0 approvals), status checkcheckrequired, administrators included, merge and square-merge methods only. Direct pushes tomain/developare rejected; prefer the PR path for everything.
- No release-please: its first run computed 1.0.0 (we need 0.1.0) and its release PR is blocked by the repo's "GitHub Actions is not permitted to create pull requests" setting, which we are not changing. The current process is documented in
docs/agents/changelog.md(agent) anddocs/releasing.md(human). - npm OIDC trusted publisher shape: org
a1exk-dev, repoopencode-skills-autocomplete, workflow.github/workflows/release.yml,npm publishallowed (audienceproject:a1exk-dev:opencode-skills-autocomplete). The npm name is claimed by a one-off0.0.0publish.
- A release pull request into
mainis squash-merged, somain's release commit has one parent and carries the whole diff since the previous release, althoughchangelog.mdstep 4 asks for a no-fast-forward merge commit.developis therefore never an ancestor ofmain, which is why every release needs the explicit backmerge branch. - That shape makes the backmerge conflict on every change the release branch added beyond the generated
CHANGELOG.mdandpackage.jsonlines, becausedevelopholds those same changes under different commits. Resolve each hunk in favour ofmainand verify the whole merge in one step:git diff <release branch head>against the merged tree must be empty, since nothing but the release branch should have reachedmain. gh pr merge --squashnames the squash commit after the branch's commit, not the pull request title. On a backmerge branch that commit is the merge, so its second parent's subject wins: 1.0.0's backmerge landed aschore(release): prepare 1.0.0 (#33) (#34)instead of naming the backmerge. Pass--subjectfor anything but a single-commit branch. Theprotect-branchesruleset blocks the force-push that would relabel it afterwards.gh pr checks --watchaftergh pr update-branchcan report the previous head's finished run as passing, so it exits green while the real run is still queued. ReadheadRefOidfrom the pull request and watch the run whoseheadShamatches it (gh run list --branch <branch>).
.agents/skills/*are vendored copies: 17 frommattpocock/skills(skills/<category>/<skill>/, category isengineeringexceptgrilling,wait-what, andwriting-for-agents, which areproductivity), andhumanizerfrom the root ofblader/humanizer. Refresh by replacing each directory's files from upstreammain.- Two deviations survive a refresh and must be reapplied:
- Upstream names the glossary
GLOSSARY.md(andGLOSSARY-MAP.md,GLOSSARY-FORMAT.md). This repo keepsCONTEXT.md, so rename every reference back after pulling. Affectsdomain-modeling(including itsCONTEXT-FORMAT.mdfilename),wait-what,triage,codebase-design,improve-codebase-architecture,tdd. wait-whatupstream carriesdisable-model-invocation: true, which would stop an agent from running the rootAGENTS.mdwriting rule. Drop that line from the vendored copy. Itsagents/openai.yamlstill setsallow_implicit_invocation: false, so OpenCode treats the skill as user-invoked only.
- Upstream names the glossary
setup-matt-pocock-skillsis intentionally not vendored. Six skills name it as a fallback for a repo that has not been set up;docs/agents/already holds that setup.cspellreads only**/*.mdoutside.agents/, so vendored skill text does not gate CI.