pantheon-opencode 1.3.4 → 1.3.5-beta.57.2e03676
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +919 -0
- package/README.md +64 -3
- package/bin/pantheon-init.mjs +17 -0
- package/opencode.json +3 -11
- package/package.json +2 -1
- package/scripts/build-agents-md.mjs +195 -0
- package/scripts/cost.mjs +17 -0
- package/scripts/doctor.mjs +203 -2
- package/scripts/install/opencode.mjs +91 -22
- package/scripts/release-beta-version.mjs +80 -0
- package/scripts/validate-routing.mjs +165 -1
- package/src/agents/aphrodite.md +3 -1
- package/src/agents/apollo.md +2 -0
- package/src/agents/athena.md +4 -1
- package/src/agents/demeter.md +3 -1
- package/src/agents/gaia.md +2 -0
- package/src/agents/hephaestus.md +3 -1
- package/src/agents/hermes.md +4 -1
- package/src/agents/iris.md +3 -1
- package/src/agents/mnemosyne.md +3 -1
- package/src/agents/nyx.md +3 -1
- package/src/agents/prometheus.md +3 -1
- package/src/agents/talos.md +3 -1
- package/src/agents/themis.md +3 -1
- package/src/pantheon/cost-command.ts +57 -18
- package/src/pantheon/delegation-enforce.ts +139 -5
- package/src/pantheon/delegation-finalize.ts +24 -0
- package/src/pantheon/delegation.ts +128 -15
- package/src/plugin.ts +43 -3
- package/src/plugins/tui/package.json +1 -1
- package/src/routing.yml +12 -9
package/README.md
CHANGED
|
@@ -183,6 +183,39 @@ export OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true
|
|
|
183
183
|
opencode
|
|
184
184
|
```
|
|
185
185
|
|
|
186
|
+
### OpenCode V1/V2 — Dual Version
|
|
187
|
+
|
|
188
|
+
Pantheon's shared config is **V1-shaped and valid under both OpenCode versions**:
|
|
189
|
+
|
|
190
|
+
| | V1 | V2 (beta) |
|
|
191
|
+
|---|---|---|
|
|
192
|
+
| Binary | `opencode` (1.18.18) | `opencode2` (v0.0.0-next-17444) |
|
|
193
|
+
| Config locations | `~/.config/opencode/`, `<project>/opencode.json`, `.opencode/` | Same — V2 reads the same locations |
|
|
194
|
+
| Config handling | Loads V1 shape directly | Normalizes V1 fields **in memory** — no rewrite, no native V2 conversion |
|
|
195
|
+
| State DB | `~/.local/share/opencode/opencode.db` | `~/.local/share/opencode/opencode-v2.db` (isolated via `OPENCODE_DB`) |
|
|
196
|
+
| Service port | 49374 | 49375 (must differ from V1) |
|
|
197
|
+
|
|
198
|
+
The `pantheon_cost` report preserves the V1 database default. Set
|
|
199
|
+
`PANTHEON_OPENCODE_VERSION=v1` or `v2` to select the corresponding state DB
|
|
200
|
+
explicitly (`opencode.db` or `opencode-v2.db`). For custom installations,
|
|
201
|
+
`PANTHEON_COST_DB=/absolute/path/to/opencode.db` takes precedence over the
|
|
202
|
+
version selector; an explicit `dbPath` supplied by the tool caller takes
|
|
203
|
+
precedence over both. The resolver never probes the other version's database,
|
|
204
|
+
and reports an actionable error when the selected DB is missing or has an
|
|
205
|
+
incompatible schema.
|
|
206
|
+
|
|
207
|
+
**What changed for V2 compatibility (Phase 3):**
|
|
208
|
+
|
|
209
|
+
- `subagent_depth` moved from top-level to `experimental.subagent_depth` — V2 silently ignores the top-level key. The installer migrates existing configs automatically.
|
|
210
|
+
- Instruction content consolidated into `AGENTS.md` (generated by `scripts/build-agents-md.mjs` from `src/instructions/*.instructions.md`) — V2 accepts but does **not** load the `instructions` config key; both versions auto-load `AGENTS.md`.
|
|
211
|
+
- `npx pantheon-opencode init --version v2` labels an install for V2 (informational — the config is shared).
|
|
212
|
+
|
|
213
|
+
**Known V2 beta gaps:**
|
|
214
|
+
|
|
215
|
+
- `instructions` config key accepted but not loaded → content lives in `AGENTS.md`.
|
|
216
|
+
- `formatter` / `lsp` config not executed.
|
|
217
|
+
- `--subtask` delegation flag is a no-op.
|
|
218
|
+
|
|
186
219
|
### Background Delegation
|
|
187
220
|
|
|
188
221
|
Zeus (and other root sessions) can dispatch agents as **background child sessions**
|
|
@@ -221,6 +254,25 @@ transition.
|
|
|
221
254
|
**Timeout:** `background_delegation.timeout_ms` (default `900000` = 15 min). A job
|
|
222
255
|
that has not reached a terminal state is finalized as `error`/`timedOut`.
|
|
223
256
|
|
|
257
|
+
**Runtime delegation environment variables:**
|
|
258
|
+
|
|
259
|
+
| Variable | Default | Scope and behavior |
|
|
260
|
+
|---|---:|---|
|
|
261
|
+
| `PANTHEON_ATHENA_APOLLO_BUDGET` | `5` dispatches | In-memory per `createDelegationTools()` factory/process instance and parent session, for the `athena → apollo` read-only exception. |
|
|
262
|
+
| `PANTHEON_HERMES_APOLLO_BUDGET` | `5` dispatches | In-memory per `createDelegationTools()` factory/process instance and parent session, for the `hermes → apollo` read-only exception. |
|
|
263
|
+
| `PANTHEON_DELEGATION_TIMEOUT_MS` | `900000` ms (15 min) | Per delegated child wall-clock timeout. The value is read when the plugin factory is created; unset or invalid values use the default. |
|
|
264
|
+
|
|
265
|
+
Budgets are reserved before asynchronous child-session creation, so real
|
|
266
|
+
concurrent calls cannot overshoot them. They reset when the factory/process
|
|
267
|
+
restarts. If `ToolContext.agent` is missing, the default runtime matrix rejects
|
|
268
|
+
the delegation (fail-closed); no caller identity or budget is inferred from the
|
|
269
|
+
target, prompt, or session state. An explicitly legacy host using
|
|
270
|
+
`enforceRuntimeMatrix: false` skips the budget because the caller cannot be
|
|
271
|
+
identified and emits a warning. These budgets are separate from
|
|
272
|
+
`background_delegation.max_concurrent_per_agent`: concurrency limits the number
|
|
273
|
+
of currently running children, while an exception budget limits total dispatch
|
|
274
|
+
reservations for that parent session during the factory lifetime.
|
|
275
|
+
|
|
224
276
|
**Read-only enforcement:** delegating with `read_only: true`, or delegating to an
|
|
225
277
|
agent in `background_delegation.read_only_agents` (`apollo`, `gaia`), registers
|
|
226
278
|
the child session as read-only — the `tool.execute.before` guard denies
|
|
@@ -853,9 +905,18 @@ to Mnemosyne so counts stay accurate and consistent.
|
|
|
853
905
|
|
|
854
906
|
### What CI enforces automatically
|
|
855
907
|
|
|
856
|
-
`release.yml` validates version consistency across
|
|
857
|
-
`plugin.json`, `
|
|
858
|
-
the release is blocked until
|
|
908
|
+
`release.yml` validates version consistency across the package manifests
|
|
909
|
+
(`package.json`, `plugin.json`, `pyproject.toml`, and the TUI package) before
|
|
910
|
+
publishing. If they diverge, the release is blocked until they are reconciled.
|
|
911
|
+
|
|
912
|
+
Beta releases are calculated from npm's published `latest` at workflow runtime:
|
|
913
|
+
the default is the next patch, formatted as
|
|
914
|
+
`<next-stable>-beta.<PR>.<short-sha>`. Only the exact `release:beta` label
|
|
915
|
+
triggers this path; alternate label variants do not trigger it. Beta recovery
|
|
916
|
+
requires an explicit `workflow_dispatch` with its recovery inputs; a normal
|
|
917
|
+
push does not republish beta. The workflow owns the manifest update, tag, and
|
|
918
|
+
publish, creates the GitHub Release as `Pantheon <version>`, and does not create
|
|
919
|
+
PR comments.
|
|
859
920
|
|
|
860
921
|
### Anti-patterns
|
|
861
922
|
|
package/bin/pantheon-init.mjs
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Thin CLI wrapper that parses arguments and delegates to
|
|
8
8
|
* installOpenCode() from scripts/install/opencode.mjs.
|
|
9
|
+
*
|
|
10
|
+
* Dual-version install (Phase 3): the generated config is V1-shaped and valid
|
|
11
|
+
* under BOTH OpenCode V1 (`opencode` 1.18.x) and V2 (`opencode2`
|
|
12
|
+
* v0.0.0-next-17444). V2 reads the same config locations and normalizes V1
|
|
13
|
+
* fields in memory — no native V2 conversion. `--version v2` labels the
|
|
14
|
+
* install for the operator; state isolation is a runtime concern
|
|
15
|
+
* (OPENCODE_DB ~/.local/share/opencode/opencode-v2.db, service port 49375).
|
|
9
16
|
*/
|
|
10
17
|
import fs from 'fs'
|
|
11
18
|
import os from 'os'
|
|
@@ -43,6 +50,7 @@ function printUsage() {
|
|
|
43
50
|
console.log(' npx pantheon-opencode init --interactive # Force interactive TUI mode')
|
|
44
51
|
console.log(' npx pantheon-opencode init --headless # Force non-interactive mode')
|
|
45
52
|
console.log(' npx pantheon-opencode init -y # Skip confirmations, use defaults')
|
|
53
|
+
console.log(' npx pantheon-opencode init --version v2 # Label install for OpenCode V2 (beta)')
|
|
46
54
|
console.log(' npx pantheon-opencode init --preset <name> # Install and activate model preset')
|
|
47
55
|
console.log(' npx pantheon-opencode set-tier <name> # Set active model preset (global)')
|
|
48
56
|
console.log(
|
|
@@ -182,6 +190,14 @@ async function main() {
|
|
|
182
190
|
const autoYes = args.includes('--yes') || args.includes('-y')
|
|
183
191
|
const presetIndex = args.indexOf('--preset')
|
|
184
192
|
const presetOpt = presetIndex >= 0 ? (args[presetIndex + 1] ?? null) : null
|
|
193
|
+
// --version v1|v2 labels the install target (informational; the config is
|
|
194
|
+
// shared and V1-shaped under both versions). Invalid values fail fast.
|
|
195
|
+
const versionIndex = args.indexOf('--version')
|
|
196
|
+
const versionOpt = versionIndex >= 0 ? (args[versionIndex + 1] ?? null) : null
|
|
197
|
+
if (versionOpt !== null && versionOpt !== 'v1' && versionOpt !== 'v2') {
|
|
198
|
+
console.error(`❌ Invalid --version "${versionOpt}" — expected v1 or v2`)
|
|
199
|
+
process.exit(1)
|
|
200
|
+
}
|
|
185
201
|
|
|
186
202
|
const components = ['agents', 'skills', 'instructions', 'commands', 'plugins']
|
|
187
203
|
if (!skipMCP) components.push('runtime')
|
|
@@ -203,6 +219,7 @@ async function main() {
|
|
|
203
219
|
headless: forceHeadless,
|
|
204
220
|
yes: autoYes,
|
|
205
221
|
preset: presetOpt,
|
|
222
|
+
version: versionOpt ?? 'v1',
|
|
206
223
|
})
|
|
207
224
|
} catch (err) {
|
|
208
225
|
console.error(
|
package/opencode.json
CHANGED
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
13
|
"default_agent": "zeus",
|
|
14
|
-
"
|
|
14
|
+
"experimental": {
|
|
15
|
+
"subagent_depth": 2
|
|
16
|
+
},
|
|
15
17
|
"model": "opencode-go/deepseek-v4-flash",
|
|
16
18
|
"small_model": "opencode-go/deepseek-v4-flash",
|
|
17
19
|
"permission": {
|
|
@@ -28,7 +30,6 @@
|
|
|
28
30
|
"pip *": "allow"
|
|
29
31
|
}
|
|
30
32
|
},
|
|
31
|
-
"instructions": ["AGENTS.md", "src/instructions/*.instructions.md"],
|
|
32
33
|
"mcp": {
|
|
33
34
|
"bifrost": {
|
|
34
35
|
"type": "remote",
|
|
@@ -37,15 +38,6 @@
|
|
|
37
38
|
}
|
|
38
39
|
},
|
|
39
40
|
"agent": {
|
|
40
|
-
"athena": {
|
|
41
|
-
"model": "opencode-go/kimi-k3"
|
|
42
|
-
},
|
|
43
|
-
"themis": {
|
|
44
|
-
"model": "opencode-go/deepseek-v4-pro"
|
|
45
|
-
},
|
|
46
|
-
"hephaestus": {
|
|
47
|
-
"model": "opencode-go/deepseek-v4-pro"
|
|
48
|
-
},
|
|
49
41
|
"talos": {
|
|
50
42
|
"permission": {
|
|
51
43
|
"edit": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon-opencode",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.5-beta.57.2e03676",
|
|
4
4
|
"description": "Multi-agent orchestration for OpenCode — 14 specialized agents, TDD, Themis review gates.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"postinstall": "node scripts/postinstall.mjs",
|
|
11
11
|
"prepare": "husky",
|
|
12
12
|
"setup": "node bin/pantheon-init.mjs init",
|
|
13
|
+
"build:agents-md": "node scripts/build-agents-md.mjs",
|
|
13
14
|
"setup:lite": "node bin/pantheon-init.mjs init --clean --components agents,skills,instructions",
|
|
14
15
|
"doctor": "node scripts/doctor.mjs",
|
|
15
16
|
"prune": "node scripts/prune.mjs",
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* build-agents-md.mjs — Generate the repo AGENTS.md from canonical sources.
|
|
4
|
+
*
|
|
5
|
+
* AGENTS.md is the single instruction file BOTH OpenCode V1 and V2 load
|
|
6
|
+
* (V1 auto-discovers it, V2 loads it explicitly; the `instructions` config
|
|
7
|
+
* key is accepted-but-ignored by V2). To keep instruction content in parity
|
|
8
|
+
* across both versions without duplication, this script consolidates:
|
|
9
|
+
*
|
|
10
|
+
* (a) a static header (project description + agent table + setup notes)
|
|
11
|
+
* (b) every src/instructions/*.instructions.md body, embedded under a
|
|
12
|
+
* `## <name>` section marker (name taken from frontmatter)
|
|
13
|
+
*
|
|
14
|
+
* into one generated AGENTS.md. src/instructions/*.instructions.md remains
|
|
15
|
+
* the source of truth — AGENTS.md is a committed build artifact, never
|
|
16
|
+
* hand-edited.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* node scripts/build-agents-md.mjs # write AGENTS.md to repo root
|
|
20
|
+
* node scripts/build-agents-md.mjs --check # exit 1 if AGENTS.md is stale
|
|
21
|
+
*
|
|
22
|
+
* No runtime dependencies (Node >= 18, stdlib only). Idempotent: running it
|
|
23
|
+
* twice produces byte-identical output.
|
|
24
|
+
*/
|
|
25
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
26
|
+
import { dirname, join } from 'node:path'
|
|
27
|
+
import { fileURLToPath } from 'node:url'
|
|
28
|
+
|
|
29
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
30
|
+
export const ROOT = join(__dirname, '..')
|
|
31
|
+
const INSTRUCTIONS_DIR = join(ROOT, 'src', 'instructions')
|
|
32
|
+
const AGENTS_MD_PATH = join(ROOT, 'AGENTS.md')
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Static header — preserved verbatim from the original AGENTS.md (agent table,
|
|
36
|
+
// setup notes, sandbox rules, conventions). Backticks are escaped for the
|
|
37
|
+
// template literal.
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
const STATIC_HEADER = `# Pantheon Agent System — OpenCode
|
|
40
|
+
|
|
41
|
+
This project uses the Pantheon multi-agent framework with 14 specialized agents.
|
|
42
|
+
|
|
43
|
+
## Available Agents
|
|
44
|
+
|
|
45
|
+
| Agent | Role |
|
|
46
|
+
|-------|------|
|
|
47
|
+
| @aphrodite | Frontend specialist — React 19, TypeScript strict, WCAG accessibility, responsive design, TDD, modern API patterns, deprecated npm detection. Calls apollo for discovery, sends to themis for review. |
|
|
48
|
+
| @apollo | Read-only investigation scout — 3–10 parallel searches across codebase, external docs, and GitHub. Called by: athena, zeus, hermes, aphrodite, demeter. No edits, no commands. |
|
|
49
|
+
| @athena | Strategic planner & architect — research-first, plan-only, never implements. Plans include quality gates (ruff/Biome, dep detection, LTS policy). Calls apollo for discovery. |
|
|
50
|
+
| @demeter | Database specialist — SQLAlchemy 2.0, Alembic, query optimization, N+1 prevention, TDD migrations, modern DB libs. Calls apollo for discovery, sends to themis. |
|
|
51
|
+
| @gaia | Remote sensing domain specialist — satellite image processing, spectral analysis, SAR, change detection, time series, ML/DL classification. Read-only analysis of geospatial data. |
|
|
52
|
+
| @hephaestus | AI tooling & pipelines specialist — LangChain/LangGraph chains, RAG architecture, vector stores, embedding strategies. Forges AI infrastructure. Calls apollo, sends to themis. |
|
|
53
|
+
| @hermes | Backend specialist — FastAPI, Python, async, TDD (RED→GREEN→REFACTOR), modern Python stdlib, obsolete lib detection. Calls apollo for discovery, sends to themis. |
|
|
54
|
+
| @iris | GitHub operations specialist — branches, pull requests, issues, releases, tags. Called by zeus after review. Never pushes or merges without explicit human approval. Integrates with VS Code GitHub Pull Requests extension. |
|
|
55
|
+
| @mnemosyne | Memory bank quality owner — initializes .pantheon/memory-bank/, writes ADRs and task records on explicit request. Called by zeus. Never invoked automatically after phases. |
|
|
56
|
+
| @nyx | Observability & monitoring specialist — OpenTelemetry tracing, token/cost tracking, agent performance analytics, LangSmith integration. Calls apollo for discovery, sends to themis. |
|
|
57
|
+
| @prometheus | Infrastructure + model provider specialist — Docker, CI/CD, multi-model routing, cost optimization, provider abstraction |
|
|
58
|
+
| @talos | Hotfix express lane — direct fixes for small bugs, CSS, typos, minor logic. No TDD ceremony, no orchestration overhead. Standalone, no subagents. Escalates complex issues to zeus. |
|
|
59
|
+
| @themis | Quality & security gate — ruff/Biome linting, dead/legacy code detection, OWASP Top 10, coverage >80%, correctness, deprecation audit. Called by implementers; escalates blockers to zeus. |
|
|
60
|
+
| @zeus | Central orchestrator — never implements. Delegates to: athena, apollo, hermes, aphrodite, demeter, prometheus, themis, iris, mnemosyne, talos, hephaestus, nyx |
|
|
61
|
+
|
|
62
|
+
## OpenCode Setup
|
|
63
|
+
|
|
64
|
+
See [INSTALLATION.md](docs/INSTALLATION.md) for setup instructions.
|
|
65
|
+
|
|
66
|
+
- Build: \`npm test\`
|
|
67
|
+
- Test: \`npm test\`
|
|
68
|
+
- Lint: \`npm run lint\`
|
|
69
|
+
|
|
70
|
+
## Teste de Instalação Global (sandbox)
|
|
71
|
+
|
|
72
|
+
Para validar a instalação global do pacote pantheon-opencode COMO UM USUÁRIO REAL, use o sandbox isolado em \`~/pantheon-sandbox/\` (fora do repo, HOME + prefix npm + venv próprios). O ambiente de dev mistura 3 instalações + config global + venv — NÃO serve para testar instalação/empacotamento.
|
|
73
|
+
|
|
74
|
+
- Rodar: \`bash ~/pantheon-sandbox/run-test.sh\` (opencode mcp list 5/5 connected + doctor 0 erros + abre TUI isolado)
|
|
75
|
+
- Regra para agentes: ao validar instalação global (npm pack, \`init\`, MCPs, hooks), usar o sandbox — NUNCA testar no ambiente de dev
|
|
76
|
+
- Descarte: \`rm -rf ~/pantheon-sandbox\`
|
|
77
|
+
- Detalhes: ver \`~/pantheon-sandbox/README.md\`
|
|
78
|
+
|
|
79
|
+
## Conventions
|
|
80
|
+
|
|
81
|
+
- TDD: Write failing test first, then implement
|
|
82
|
+
- Coverage minimum: 80%
|
|
83
|
+
- Async/await on all I/O
|
|
84
|
+
- Type hints on all functions
|
|
85
|
+
- PRs always update the README — every pull request must document newly added features, behaviors, env vars, or commands in the README before being opened.
|
|
86
|
+
`
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Frontmatter parsing (stdlib only — no yaml dependency)
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Parse YAML-ish frontmatter from an instruction file.
|
|
94
|
+
*
|
|
95
|
+
* Only the `name` field is extracted (single-line scalar). The frontmatter
|
|
96
|
+
* block itself is stripped from the embedded body.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} content - Raw file content
|
|
99
|
+
* @returns {{ name: string, body: string } | null}
|
|
100
|
+
*/
|
|
101
|
+
function parseInstruction(content) {
|
|
102
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/)
|
|
103
|
+
if (!match) return null
|
|
104
|
+
const fm = match[1]
|
|
105
|
+
const nameMatch = fm.match(/^name:\s*["']?([^"'\r\n]+)["']?\s*$/m)
|
|
106
|
+
if (!nameMatch) return null
|
|
107
|
+
return {
|
|
108
|
+
name: nameMatch[1].trim(),
|
|
109
|
+
body: content.slice(match[0].length).trimEnd() + '\n',
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Read all src/instructions/*.instructions.md files, sorted by filename for
|
|
115
|
+
* deterministic output.
|
|
116
|
+
*
|
|
117
|
+
* @returns {Array<{ file: string, name: string, body: string }>}
|
|
118
|
+
*/
|
|
119
|
+
export function readInstructions() {
|
|
120
|
+
if (!existsSync(INSTRUCTIONS_DIR)) return []
|
|
121
|
+
const files = readdirSync(INSTRUCTIONS_DIR)
|
|
122
|
+
.filter((f) => f.endsWith('.instructions.md'))
|
|
123
|
+
.sort()
|
|
124
|
+
const out = []
|
|
125
|
+
for (const file of files) {
|
|
126
|
+
const content = readFileSync(join(INSTRUCTIONS_DIR, file), 'utf8')
|
|
127
|
+
const parsed = parseInstruction(content)
|
|
128
|
+
if (parsed) out.push({ file, ...parsed })
|
|
129
|
+
}
|
|
130
|
+
return out
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Generation
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Generate the full AGENTS.md content.
|
|
139
|
+
*
|
|
140
|
+
* @param {Array<{ file: string, name: string, body: string }>} instructions
|
|
141
|
+
* @returns {string}
|
|
142
|
+
*/
|
|
143
|
+
export function generateAgentsMd(instructions) {
|
|
144
|
+
const sections = [
|
|
145
|
+
'<!-- Generated by scripts/build-agents-md.mjs — do not edit directly. -->',
|
|
146
|
+
'<!-- Source of truth: src/instructions/*.instructions.md (AGENTS.md is a committed build artifact). -->',
|
|
147
|
+
'',
|
|
148
|
+
STATIC_HEADER.trimEnd(),
|
|
149
|
+
'',
|
|
150
|
+
'---',
|
|
151
|
+
'',
|
|
152
|
+
'# Agent Instructions',
|
|
153
|
+
'',
|
|
154
|
+
'The following sections are consolidated from `src/instructions/*.instructions.md`',
|
|
155
|
+
'so both OpenCode V1 (auto-loads AGENTS.md) and V2 (loads AGENTS.md, ignores the',
|
|
156
|
+
'`instructions` config key) receive identical instruction content.',
|
|
157
|
+
'',
|
|
158
|
+
]
|
|
159
|
+
for (const instr of instructions) {
|
|
160
|
+
sections.push(
|
|
161
|
+
`<!-- Source: src/instructions/${instr.file} -->`,
|
|
162
|
+
`## ${instr.name}`,
|
|
163
|
+
'',
|
|
164
|
+
instr.body.trimEnd(),
|
|
165
|
+
'',
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
return sections.join('\n').trimEnd() + '\n'
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// CLI entry point
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
function main() {
|
|
176
|
+
const checkOnly = process.argv.includes('--check')
|
|
177
|
+
const instructions = readInstructions()
|
|
178
|
+
const content = generateAgentsMd(instructions)
|
|
179
|
+
|
|
180
|
+
if (checkOnly) {
|
|
181
|
+
const existing = existsSync(AGENTS_MD_PATH) ? readFileSync(AGENTS_MD_PATH, 'utf8') : ''
|
|
182
|
+
if (existing !== content) {
|
|
183
|
+
console.error(`❌ AGENTS.md is stale — run: node scripts/build-agents-md.mjs`)
|
|
184
|
+
process.exit(1)
|
|
185
|
+
}
|
|
186
|
+
console.log(`✅ AGENTS.md is up to date (${instructions.length} instruction sections)`)
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
writeFileSync(AGENTS_MD_PATH, content, 'utf8')
|
|
191
|
+
const lines = content.split('\n').length
|
|
192
|
+
console.log(`✅ Generated AGENTS.md (${instructions.length} instruction sections, ${lines} lines)`)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
main()
|
package/scripts/cost.mjs
CHANGED
|
@@ -26,6 +26,22 @@ function fail(message) {
|
|
|
26
26
|
process.exitCode = 1
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function assertCompatibleSchema(db) {
|
|
30
|
+
const table = db
|
|
31
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'message'")
|
|
32
|
+
.get()
|
|
33
|
+
if (!table)
|
|
34
|
+
throw new Error('incompatible opencode.db schema: required "message" table is missing')
|
|
35
|
+
const columns = db.prepare('PRAGMA table_info(message)').all()
|
|
36
|
+
const names = new Set(columns.map((column) => column.name))
|
|
37
|
+
const missing = ['data', 'time_created'].filter((name) => !names.has(name))
|
|
38
|
+
if (missing.length > 0) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`incompatible opencode.db schema: message table is missing ${missing.join(', ')}; select the matching V1/V2 database`,
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
29
45
|
function main() {
|
|
30
46
|
const [, , dbPath, daysArg] = process.argv
|
|
31
47
|
const days = Number(daysArg ?? '7')
|
|
@@ -35,6 +51,7 @@ function main() {
|
|
|
35
51
|
|
|
36
52
|
try {
|
|
37
53
|
const db = new DatabaseSync(dbPath, { readOnly: true })
|
|
54
|
+
assertCompatibleSchema(db)
|
|
38
55
|
const since = Date.now() - days * 86_400_000
|
|
39
56
|
const rows = db.prepare('SELECT data FROM message WHERE time_created >= ?').all(since)
|
|
40
57
|
db.close()
|
package/scripts/doctor.mjs
CHANGED
|
@@ -757,7 +757,7 @@ function checkPermissionMismatches(args) {
|
|
|
757
757
|
// Check D: Sync Status
|
|
758
758
|
// ---------------------------------------------------------------------------
|
|
759
759
|
|
|
760
|
-
function checkSyncStatus(
|
|
760
|
+
function checkSyncStatus(_args) {
|
|
761
761
|
section('D. Sync Status')
|
|
762
762
|
|
|
763
763
|
// Platform sync removed in v1.0 (OpenCode-only)
|
|
@@ -979,9 +979,207 @@ function checkVenvLayer(args) {
|
|
|
979
979
|
}
|
|
980
980
|
|
|
981
981
|
// ---------------------------------------------------------------------------
|
|
982
|
-
//
|
|
982
|
+
// Check G: AGENTS.md freshness
|
|
983
983
|
// ---------------------------------------------------------------------------
|
|
984
984
|
|
|
985
|
+
/**
|
|
986
|
+
* Check AGENTS.md only where the repository's generator can provide a real
|
|
987
|
+
* source-of-truth comparison. Installed targets have no canonical instruction
|
|
988
|
+
* sources available to this process, so their freshness is deliberately not
|
|
989
|
+
* guessed from file age.
|
|
990
|
+
*/
|
|
991
|
+
function checkAgentsMdFreshness(args) {
|
|
992
|
+
section('G. AGENTS.md Freshness')
|
|
993
|
+
|
|
994
|
+
const agentsMdPath = join(args.target, 'AGENTS.md')
|
|
995
|
+
if (args.target !== ROOT) {
|
|
996
|
+
const outcome = classifyAgentsMdFreshness({
|
|
997
|
+
targetIsRoot: false,
|
|
998
|
+
agentsMdExists: existsSync(agentsMdPath),
|
|
999
|
+
generatorExists: false,
|
|
1000
|
+
})
|
|
1001
|
+
if (outcome === 'unverified') {
|
|
1002
|
+
info(
|
|
1003
|
+
'AGENTS.md is present; freshness was not checked because the canonical generator sources are not available in an installed target',
|
|
1004
|
+
)
|
|
1005
|
+
} else {
|
|
1006
|
+
warn(
|
|
1007
|
+
'AGENTS.md is not present; freshness cannot be verified for this installed target — run `pantheon-opencode init --components instructions`',
|
|
1008
|
+
)
|
|
1009
|
+
}
|
|
1010
|
+
return
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const generator = join(ROOT, 'scripts', 'build-agents-md.mjs')
|
|
1014
|
+
if (!existsSync(generator)) {
|
|
1015
|
+
info('AGENTS.md freshness skipped — canonical generator is not available')
|
|
1016
|
+
return
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const result = spawn(process.execPath, [generator, '--check'], ROOT)
|
|
1020
|
+
const outcome = classifyAgentsMdFreshness({
|
|
1021
|
+
targetIsRoot: true,
|
|
1022
|
+
agentsMdExists: existsSync(agentsMdPath),
|
|
1023
|
+
generatorExists: true,
|
|
1024
|
+
generatorStatus: result.status,
|
|
1025
|
+
})
|
|
1026
|
+
if (outcome === 'pass') {
|
|
1027
|
+
pass('AGENTS.md matches the canonical generated instructions')
|
|
1028
|
+
} else if (outcome === 'unverified' && !existsSync(agentsMdPath)) {
|
|
1029
|
+
warn(
|
|
1030
|
+
'AGENTS.md is missing — run `node scripts/build-agents-md.mjs` or `pantheon-opencode init`',
|
|
1031
|
+
)
|
|
1032
|
+
} else if (outcome === 'stale') {
|
|
1033
|
+
warn('AGENTS.md is stale — run `node scripts/build-agents-md.mjs` to refresh it')
|
|
1034
|
+
} else {
|
|
1035
|
+
warn(
|
|
1036
|
+
`AGENTS.md freshness could not be verified (generator exited ${result.status ?? 'unknown'})`,
|
|
1037
|
+
)
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* Classify the generator result without relying on timestamps or fragile
|
|
1043
|
+
* process mocks. The doctor CLI uses the same result contract internally.
|
|
1044
|
+
*
|
|
1045
|
+
* @param {{ targetIsRoot: boolean; agentsMdExists: boolean; generatorExists: boolean; generatorStatus?: number | null }} input
|
|
1046
|
+
* @returns {'pass' | 'missing' | 'stale' | 'skipped' | 'unverified'}
|
|
1047
|
+
*/
|
|
1048
|
+
export function classifyAgentsMdFreshness(input) {
|
|
1049
|
+
if (!input.targetIsRoot) return input.agentsMdExists ? 'unverified' : 'missing'
|
|
1050
|
+
if (!input.generatorExists) return 'skipped'
|
|
1051
|
+
if (input.generatorStatus === 0) return 'pass'
|
|
1052
|
+
if (input.generatorStatus === 1 && input.agentsMdExists) return 'stale'
|
|
1053
|
+
return 'unverified'
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// ---------------------------------------------------------------------------
|
|
1057
|
+
// Check H: Config migration — no top-level subagent_depth
|
|
1058
|
+
// ---------------------------------------------------------------------------
|
|
1059
|
+
|
|
1060
|
+
function checkSubagentDepthPlacement(args) {
|
|
1061
|
+
section('H. Config Migration')
|
|
1062
|
+
|
|
1063
|
+
const configs = collectMcpConfigs(args)
|
|
1064
|
+
if (configs.length === 0) {
|
|
1065
|
+
info('subagent_depth placement check skipped — no opencode.json was found')
|
|
1066
|
+
return
|
|
1067
|
+
}
|
|
1068
|
+
for (const cfg of configs) {
|
|
1069
|
+
if (cfg.data.subagent_depth !== undefined) {
|
|
1070
|
+
error(
|
|
1071
|
+
`${cfg.label}: top-level "subagent_depth" is unsupported — run \`pantheon-opencode init\` to migrate it to "experimental.subagent_depth"`,
|
|
1072
|
+
)
|
|
1073
|
+
} else if (
|
|
1074
|
+
cfg.data.experimental &&
|
|
1075
|
+
typeof cfg.data.experimental === 'object' &&
|
|
1076
|
+
cfg.data.experimental.subagent_depth !== undefined
|
|
1077
|
+
) {
|
|
1078
|
+
pass(`${cfg.label}: experimental.subagent_depth = ${cfg.data.experimental.subagent_depth}`)
|
|
1079
|
+
} else {
|
|
1080
|
+
info(
|
|
1081
|
+
`${cfg.label}: experimental.subagent_depth is not configured (older/custom config may omit it)`,
|
|
1082
|
+
)
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// ---------------------------------------------------------------------------
|
|
1088
|
+
// Check J: Agent permission.task presence
|
|
1089
|
+
// ---------------------------------------------------------------------------
|
|
1090
|
+
|
|
1091
|
+
/** @param {string} content @returns {boolean} */
|
|
1092
|
+
export function hasPermissionTask(content) {
|
|
1093
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
1094
|
+
if (!match) return false
|
|
1095
|
+
const permission = match[1].match(/(?:^|\r?\n)permission:\s*\r?\n([\s\S]*?)(?=\r?\n\S|$)/)
|
|
1096
|
+
return Boolean(permission && /^\s+task:\s*/m.test(permission[1]))
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Derive installed agent files from the paths already declared by configs.
|
|
1101
|
+
* This supports both global flat and project .opencode layouts without
|
|
1102
|
+
* assuming a user home directory.
|
|
1103
|
+
*
|
|
1104
|
+
* @param {{path: string, data: object}[]} configs
|
|
1105
|
+
* @returns {string[]}
|
|
1106
|
+
*/
|
|
1107
|
+
export function deriveInstalledAgentFiles(configs) {
|
|
1108
|
+
const directories = new Set()
|
|
1109
|
+
for (const cfg of configs) {
|
|
1110
|
+
for (const agent of Object.values(cfg.data.agent ?? {})) {
|
|
1111
|
+
const source = agent && typeof agent === 'object' ? agent.source : undefined
|
|
1112
|
+
if (typeof source !== 'string' || !source.includes('/')) continue
|
|
1113
|
+
const filePath = resolve(dirname(cfg.path), source)
|
|
1114
|
+
if (filePath.startsWith(`${AGENTS_DIR}/`)) continue
|
|
1115
|
+
if (existsSync(filePath)) directories.add(dirname(filePath))
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const files = []
|
|
1120
|
+
for (const directory of directories) {
|
|
1121
|
+
let entries
|
|
1122
|
+
try {
|
|
1123
|
+
entries = readdirSync(directory, { withFileTypes: true })
|
|
1124
|
+
} catch {
|
|
1125
|
+
continue
|
|
1126
|
+
}
|
|
1127
|
+
for (const entry of entries) {
|
|
1128
|
+
if (entry.isFile() && entry.name.endsWith('.md')) files.push(join(directory, entry.name))
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return [...new Set(files)].sort()
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function checkPermissionTaskPresence(args) {
|
|
1135
|
+
section('J. Installed Agent permission.task')
|
|
1136
|
+
|
|
1137
|
+
if (args.profile === 'lite') {
|
|
1138
|
+
info(
|
|
1139
|
+
'Installed agent permission.task check skipped (lite profile does not require managed agents)',
|
|
1140
|
+
)
|
|
1141
|
+
return
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const installedFiles = deriveInstalledAgentFiles(collectMcpConfigs(args))
|
|
1145
|
+
if (installedFiles.length === 0) {
|
|
1146
|
+
info(
|
|
1147
|
+
'Installed agent permission.task check skipped — no agent source directory could be derived from available config',
|
|
1148
|
+
)
|
|
1149
|
+
return
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const missing = findMissingPermissionTask(installedFiles)
|
|
1153
|
+
if (classifyPermissionTaskCheck(args.profile, installedFiles.length, missing.length) === 'pass') {
|
|
1154
|
+
pass(`All ${installedFiles.length} installed agents have permission.task`)
|
|
1155
|
+
return
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
for (const filePath of missing) {
|
|
1159
|
+
error(
|
|
1160
|
+
`${basename(filePath)}: missing permission.task in managed agent — run \`pantheon-opencode init\` to update installed agents`,
|
|
1161
|
+
)
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/** @param {string[]} filePaths @returns {string[]} */
|
|
1166
|
+
export function findMissingPermissionTask(filePaths) {
|
|
1167
|
+
return filePaths.filter((filePath) => !hasPermissionTask(readFileSync(filePath, 'utf8')))
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Classify the installed-agent permission.task layer for a doctor profile.
|
|
1172
|
+
*
|
|
1173
|
+
* @param {string} profile
|
|
1174
|
+
* @param {number} installedCount
|
|
1175
|
+
* @param {number} missingCount
|
|
1176
|
+
* @returns {'skip' | 'pass' | 'error'}
|
|
1177
|
+
*/
|
|
1178
|
+
export function classifyPermissionTaskCheck(profile, installedCount, missingCount) {
|
|
1179
|
+
if (profile === 'lite' || installedCount === 0) return 'skip'
|
|
1180
|
+
return missingCount === 0 ? 'pass' : 'error'
|
|
1181
|
+
}
|
|
1182
|
+
|
|
985
1183
|
/**
|
|
986
1184
|
* Return the final doctor status message without masking blocking errors.
|
|
987
1185
|
* @param {{ error: number; warn: number }} summaryCounts
|
|
@@ -1069,6 +1267,9 @@ async function main() {
|
|
|
1069
1267
|
checkPermissionMismatches(args)
|
|
1070
1268
|
checkSyncStatus(args)
|
|
1071
1269
|
checkGitStatus(args)
|
|
1270
|
+
checkAgentsMdFreshness(args)
|
|
1271
|
+
checkSubagentDepthPlacement(args)
|
|
1272
|
+
checkPermissionTaskPresence(args)
|
|
1072
1273
|
|
|
1073
1274
|
printSummary(args.target)
|
|
1074
1275
|
|