Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 23 additions & 53 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@ Default behavior:

## Core Principles

- `specialist-first` is mandatory
- generalist is fallback, not peer
- `delegate` loads before substantive work
- specialist-first evaluation is mandatory for every task before proceeding
- discovery and selection happen before execution
- `delegate` loads before substantive work
- agent main context stays clean — research, tool execution, logs, and low-level output belong to subagents; the main agent owns planning, delegation, and synthesis only
- generalist is fallback, not peer
- capability alone is never sufficient when a better specialist exists
- delegation distributes work, not accountability
- fallback must be brief, explicit, and defensible
Expand Down Expand Up @@ -51,35 +50,11 @@ Choose specialists by semantic fit, not convenience.

Selection policy:
- select for the immediate task, not the broadest surrounding program of work
- match by description, specialization, scope, and constraints
- prefer decomposing broad tasks into specialist-owned scopes before delegation
- do not hand a mixed multi-domain task to one agent when it can be cleanly split
- if subtasks require materially different specialist expertise and can be assigned without overlapping ownership, splitting is required
- early decomposition in this phase is structural for selection and handoff, not substantive execution or broad local research
- minimal local context gathering is allowed when needed to scope selection and handoff
- prefer the most semantically specific eligible specialist
- use stable documented tie-break rules when candidates are similarly suitable
- match by domain, role, work type, and scope — never narrow the match by requiring a specific technology stack mentioned in the task
- do not use a generalist if an eligible specialist exists
- do not bypass a specialist because the current agent could also do the work

Eligibility minimum:
- immediate task = the next concrete unit of work to assign, not the broader surrounding initiative
- explicit domain or task match in the description
- no conflict with stated constraints
- scope compatible with the immediate task
- no clearly better eligible specialist

Tie-break rules:
- first: higher semantic specificity
- second: stable documented precedence in the current environment
- third: split across specialists when domains or scopes are naturally distinct

Invalid reasons to choose a generalist over an eligible specialist include:
- broader autonomy
- owning the whole rollout
- fewer handoffs
- convenience
- the current agent or chosen agent can probably do it
Detailed selection criteria, including how to avoid the "specificity trap" (over-narrowing by technology stack), tie-break rules, and invalid selection reasons, are fully defined in the `delegate` skill. **You must load and follow the `delegate` skill for complete selection rules.**

---

Expand All @@ -105,22 +80,12 @@ Required behavior:
Generalist execution is an exception path.

Fallback is allowed only when:
- no eligible specialist exists
- no discovered specialist is a defensible direct or adjacent fit for the immediate task
- all discovered candidates are unsuitable for stated constraints
- delegation is explicitly constrained by the user
- delegation failed after a valid handoff, re-evaluation, and no viable specialist path remains under the current task constraints

Fallback requirements:
- explain briefly why delegation did not apply
- keep the explanation factual and specific
- identify the failed discovery, selection, or constraint condition
- under discovery limits, state why no known eligible specialist fits better under the current constraints
- do not treat "I can do it" as sufficient justification
- do not silently fall back
- delegation failed after a valid handoff, re-evaluation, and no viable specialist path remains

Fallback mode:
- prefer delegating to a generalist agent when the environment supports it
- direct execution by the current agent is allowed only when delegation is constrained, unavailable, or already failed under the current task constraints
Detailed fallback requirements, invalid justifications (such as missing technology keywords), and fallback execution modes are defined in the `delegate` skill. **You must load and follow the `delegate` skill to validate any fallback decision.**

---

Expand Down Expand Up @@ -186,16 +151,19 @@ Provide enough explicit context to avoid avoidable clarification loops.
6. Discover eligible specialists.
7. Select the best specialist or specialists.

Hard-gate before execution:
- [ ] `delegate` activated before substantive work
- [ ] specialist-first evaluation completed
- [ ] immediate task identified before agent selection
- [ ] done criteria defined or clarified
- [ ] `wiki/index.md` consulted before broad workspace exploration when available
- [ ] discovery completed
- [ ] eligible candidates assessed
- [ ] delegation decision made under this policy
- [ ] any fallback explicitly justified
## <HARD-GATE> Before Any Execution

**STOP. Do not proceed with any substantive work until ALL conditions below are confirmed true.**

1. **`delegate` activated** — `skill('delegate')` has been loaded and evaluated. No exceptions.
2. **Specialist-first evaluation completed** — you have inspected available agents and selected by semantic fit.
3. **Immediate task identified** — the concrete next unit of work is named, not the broader initiative.
4. **Done criteria defined** — success is unambiguous; if unclear, stop and clarify.
5. **`wiki/index.md` consulted** — read it if it exists before any broad exploration.
6. **Discovery completed** — eligible candidates were gathered from all observable sources.
7. **Delegation decision made** — you chose under this policy, with explicit justification if fallback.

If ANY condition is false: **STOP. Go back to Phase 1.**

### Phase 2: Execute

Expand All @@ -204,6 +172,8 @@ Hard-gate before execution:
3. Allow subdelegation when it improves specialization or decomposition.
4. Review and synthesize before returning results upward.

**Agent Main Rule:** Do not execute tools directly unless task constraints explicitly require main-agent execution. All research, tool invocation, log collection, and low-level work belongs to subagents. The main agent context stays clean for planning, delegation, and synthesis.

### Phase 3: After Task

1. Load `wiki`.
Expand Down
2 changes: 1 addition & 1 deletion src/opencode/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "opencode CLI",
"id": "opencode",
"version": "0.3.5",
"version": "0.3.6",
"description": "Installs the opencode AI coding agent CLI and ensures volume-mounted data directories are owned by the correct user.",
"documentationURL": "https://opencode.ai",
"options": {
Expand Down
27 changes: 27 additions & 0 deletions src/opencode/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ ensure_prerequisites() {
missing="${missing} curl"
fi

if ! command -v jq >/dev/null 2>&1; then
missing="${missing} jq"
fi

if [ ! -f /etc/ssl/certs/ca-certificates.crt ]; then
missing="${missing} ca-certificates"
fi
Expand Down Expand Up @@ -185,6 +189,29 @@ chown -R "${USERNAME}:${USERNAME}" \
"${USER_HOME}/.local/share/opencode" \
"${USER_HOME}/.local/state/opencode"

log "Configuring LSP in opencode.json..."

opencode_config="${USER_HOME}/.config/opencode/opencode.json"

if [ -f "$opencode_config" ]; then
if ! jq -e '.lsp' "$opencode_config" >/dev/null 2>&1; then
jq '. + {"lsp": {}}' "$opencode_config" > "${opencode_config}.tmp" && mv "${opencode_config}.tmp" "$opencode_config"
log "Added 'lsp' key to existing opencode.json"
else
log "'lsp' key already exists in opencode.json"
fi
else
cat > "$opencode_config" <<'EOF'
{
"$schema": "https://opencode.ai/config.json",
"lsp": {}
}
EOF
log "Created opencode.json with LSP enabled"
fi

chown "${USERNAME}:${USERNAME}" "$opencode_config"

cat > /usr/local/bin/opencode-fix-permissions <<EOF
#!/bin/bash
set -euo pipefail
Expand Down
10 changes: 10 additions & 0 deletions test/opencode/autoupdate_disabled.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ set -e

source dev-container-features-test-lib

OPENCODE_CONFIG=""
[ -f /root/.config/opencode/opencode.json ] && OPENCODE_CONFIG=/root/.config/opencode/opencode.json
[ -f /home/vscode/.config/opencode/opencode.json ] && OPENCODE_CONFIG=/home/vscode/.config/opencode/opencode.json

check "opencode binary exists" test -f /usr/local/bin/opencode

check "opencode.json exists" test -n "$OPENCODE_CONFIG"

check "opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG"

check "opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG")" -eq 0

reportResults
10 changes: 10 additions & 0 deletions test/opencode/autoupdate_enabled.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@ set -e

source dev-container-features-test-lib

OPENCODE_CONFIG=""
[ -f /root/.config/opencode/opencode.json ] && OPENCODE_CONFIG=/root/.config/opencode/opencode.json
[ -f /home/vscode/.config/opencode/opencode.json ] && OPENCODE_CONFIG=/home/vscode/.config/opencode/opencode.json

check "opencode binary exists" test -f /usr/local/bin/opencode

check "opencode is installed in PATH" command -v opencode

check "opencode.json exists" test -n "$OPENCODE_CONFIG"

check "opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG"

check "opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG")" -eq 0

reportResults
10 changes: 10 additions & 0 deletions test/opencode/custom_username.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@ set -e

source dev-container-features-test-lib

OPENCODE_CONFIG=""
[ -f /root/.config/opencode/opencode.json ] && OPENCODE_CONFIG=/root/.config/opencode/opencode.json
[ -f /home/vscode/.config/opencode/opencode.json ] && OPENCODE_CONFIG=/home/vscode/.config/opencode/opencode.json

check "opencode binary exists" test -f /usr/local/bin/opencode

check "opencode-fix-permissions script exists" test -f /usr/local/bin/opencode-fix-permissions

check "opencode.json exists" test -n "$OPENCODE_CONFIG"

check "opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG"

check "opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG")" -eq 0

reportResults
10 changes: 10 additions & 0 deletions test/opencode/default.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@ set -e

source dev-container-features-test-lib

OPENCODE_CONFIG=""
[ -f /root/.config/opencode/opencode.json ] && OPENCODE_CONFIG=/root/.config/opencode/opencode.json
[ -f /home/vscode/.config/opencode/opencode.json ] && OPENCODE_CONFIG=/home/vscode/.config/opencode/opencode.json

check "opencode binary exists" test -f /usr/local/bin/opencode

check "opencode-fix-permissions script exists" test -f /usr/local/bin/opencode-fix-permissions

check "opencode.json exists" test -n "$OPENCODE_CONFIG"

check "opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG"

check "opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG")" -eq 0

reportResults
10 changes: 10 additions & 0 deletions test/opencode/specific_version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ set -e

source dev-container-features-test-lib

OPENCODE_CONFIG=""
[ -f /root/.config/opencode/opencode.json ] && OPENCODE_CONFIG=/root/.config/opencode/opencode.json
[ -f /home/vscode/.config/opencode/opencode.json ] && OPENCODE_CONFIG=/home/vscode/.config/opencode/opencode.json

check "opencode binary exists" test -f /usr/local/bin/opencode

check "opencode-fix-permissions script exists" test -f /usr/local/bin/opencode-fix-permissions
Expand All @@ -11,4 +15,10 @@ check "marker file exists for specific version 1.3.17" test -f /usr/local/share/

check "installed version is 1.3.17" bash -c "opencode --version | grep -q '1.3.17'"

check "opencode.json exists" test -n "$OPENCODE_CONFIG"

check "opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG"

check "opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG")" -eq 0

reportResults
10 changes: 10 additions & 0 deletions test/opencode/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,20 @@ set -e

source dev-container-features-test-lib

OPENCODE_CONFIG=""
[ -f /root/.config/opencode/opencode.json ] && OPENCODE_CONFIG=/root/.config/opencode/opencode.json
[ -f /home/vscode/.config/opencode/opencode.json ] && OPENCODE_CONFIG=/home/vscode/.config/opencode/opencode.json

check "opencode binary exists" test -f /usr/local/bin/opencode

check "opencode-fix-permissions script exists" test -f /usr/local/bin/opencode-fix-permissions

check "opencode postStartCommand script exists for autoupdate" test -f /usr/local/share/devcontainer-features/opencode-postStartCommand.sh

check "opencode.json exists" test -n "$OPENCODE_CONFIG"

check "opencode.json has lsp key" jq -e '.lsp' "$OPENCODE_CONFIG"

check "opencode.json lsp is empty" test "$(jq '.lsp | length' "$OPENCODE_CONFIG")" -eq 0

reportResults
9 changes: 8 additions & 1 deletion wiki/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,11 @@ Options from `devcontainer-feature.json` are exported as environment variables (
Tests live in `test/<feature>/`:
- `default.sh` / `default_auto.sh` — default behavior
- `scenarios.json` — scenario definitions
- `custom_*.sh` — specific option combinations
- `custom_*.sh` — specific option combinations

### User-Home File Paths in Tests

When testing files inside user home directories (e.g. `~/.config/opencode/opencode.json`),
the path depends on the base image. Autogenerated tests use plain images (`debian:latest`,
`ubuntu:latest`) where user is `root` (`/root/`). Scenario tests use `devcontainers/base`
images where user is `vscode` (`/home/vscode/`). Always probe both paths dynamically.
15 changes: 15 additions & 0 deletions wiki/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,21 @@ devcontainer features test --skip-scenarios -f opencode .
devcontainer features test --skip-autogenerated --skip-duplicated -f opencode .
```

## Test Execution

There are two test modes with different behaviors:

- **Autogenerated** (`--skip-scenarios`): builds fresh from a base image, runs `test/<feature>/test.sh` as the test suite. Use for testing against plain images like `debian:latest` or `ubuntu:latest` where the user is `root`.
- **Scenario** (`--skip-autogenerated`): builds from predefined scenarios in `test/<feature>/scenarios.json`, runs **only** the scenario-specific `.sh` file (e.g. `default.sh`). The generic `test.sh` is **not** executed here.

When writing tests that check user-home files (like `~/.config/opencode/opencode.json`), both user contexts must be supported:

```bash
OPENCODE_CONFIG=""
[ -f /root/.config/opencode/opencode.json ] && OPENCODE_CONFIG=/root/.config/opencode/opencode.json
[ -f /home/vscode/.config/opencode/opencode.json ] && OPENCODE_CONFIG=/home/vscode/.config/opencode/opencode.json
```

## Available Features

| Feature | Description | Autoupdate |
Expand Down
Loading