pantheon-opencode 1.6.0-beta.6 → 1.6.0-beta.7
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 +27 -820
- package/CHANGELOG.md +116 -0
- package/README.md +132 -39
- package/README.pt-BR.md +47 -23
- package/package.json +6 -1
- package/plugin.json +1 -1
- package/pyproject.toml +3 -1
- package/scripts/build-agents-md.mjs +162 -28
- package/scripts/doctor.mjs +104 -22
- package/scripts/install/agents-md.mjs +2 -2
- package/scripts/install/config-migration.mjs +154 -7
- package/scripts/install/opencode.mjs +202 -40
- package/scripts/install/shared.mjs +115 -7
- package/scripts/install/strings.mjs +8 -0
- package/scripts/probe-pantheon-cost.mjs +16 -3
- package/scripts/sync-artifacts.mjs +2 -2
- package/scripts/test-opencode-v2-sandbox.sh +820 -86
- package/skills-lock.json +6 -5
- package/src/agents/aphrodite.md +17 -28
- package/src/agents/demeter.md +8 -18
- package/src/agents/hephaestus.md +10 -11
- package/src/agents/hermes.md +15 -30
- package/src/agents/prometheus.md +16 -10
- package/src/agents/talos.md +4 -1
- package/src/agents/themis.md +4 -3
- package/src/agents/zeus.md +16 -280
- package/src/instructions/agent-return-format.instructions.md +4 -50
- package/src/instructions/backend-standards.instructions.md +2 -0
- package/src/instructions/frontend-standards.instructions.md +3 -1
- package/src/instructions/memory-protocol.instructions.md +12 -121
- package/src/instructions/zeus-anti-stall.instructions.md +8 -111
- package/src/instructions/zeus-communication-rules.instructions.md +0 -1
- package/src/instructions/zeus-memory-operations.instructions.md +26 -0
- package/src/instructions/zeus-timeout-retry.instructions.md +9 -130
- package/src/pantheon/secret-scanner.ts +489 -0
- package/src/pantheon/v2-delegation-enforce.ts +86 -0
- package/src/pantheon/v2-execute-after.ts +186 -0
- package/src/pantheon/v2-unsupported.mjs +0 -23
- package/src/plugin-v2.ts +254 -102
- package/src/plugins/pantheon-hooks.ts +82 -35
- package/src/plugins/tui/README.md +14 -0
- package/src/plugins/tui/dist/tui.d.ts +11 -8
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +102 -62
- package/src/plugins/tui/package-lock.json +30 -33
- package/src/plugins/tui/package.json +5 -1
- package/src/plugins/tui/src/index.tsx +163 -107
- package/src/plugins/tui/tsdown.config.ts +17 -0
- package/src/routing.yml +12 -9
- package/src/skills/README.md +9 -4
- package/src/skills/agent-coordination/SKILL.md +13 -83
- package/src/skills/artifact-management/SKILL.md +12 -5
- package/src/skills/auto-continue/SKILL.md +37 -27
- package/src/skills/code-review-checklist/SKILL.md +5 -5
- package/src/skills/council-synthesis/SKILL.md +29 -0
- package/src/skills/tdd-with-agents/SKILL.md +18 -10
- package/scripts/hooks/scan-secrets.sh +0 -97
- package/src/instructions/zeus-council-synthesis.instructions.md +0 -299
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
* across both versions without duplication, this script consolidates:
|
|
9
9
|
*
|
|
10
10
|
* (a) a static header (project description + agent table + setup notes)
|
|
11
|
-
* (b)
|
|
12
|
-
*
|
|
11
|
+
* (b) shared instruction bodies embedded under a `## <name>` section marker;
|
|
12
|
+
* agent-scoped bodies are rendered into only their matching agent prompt
|
|
13
13
|
*
|
|
14
|
-
* into
|
|
15
|
-
* the source of truth
|
|
14
|
+
* into AGENTS.md plus selective agent prompt content. The instruction files
|
|
15
|
+
* remain the source of truth; AGENTS.md is a committed build artifact, never
|
|
16
16
|
* hand-edited.
|
|
17
17
|
*
|
|
18
18
|
* Usage:
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
* No runtime dependencies (Node >= 18, stdlib only). Idempotent: running it
|
|
23
23
|
* twice produces byte-identical output.
|
|
24
24
|
*/
|
|
25
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
26
|
-
import { dirname, join } from 'node:path'
|
|
25
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
26
|
+
import { dirname, join, resolve } from 'node:path'
|
|
27
27
|
import { fileURLToPath } from 'node:url'
|
|
28
28
|
|
|
29
29
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
@@ -44,19 +44,19 @@ This project uses the Pantheon multi-agent framework with 14 specialized agents.
|
|
|
44
44
|
|
|
45
45
|
| Agent | Role |
|
|
46
46
|
|-------|------|
|
|
47
|
-
| @aphrodite | Frontend specialist — React 19, TypeScript strict, WCAG accessibility, responsive design, TDD, modern API patterns, deprecated npm detection.
|
|
47
|
+
| @aphrodite | Frontend specialist — React 19, TypeScript strict, WCAG accessibility, responsive design, TDD, modern API patterns, deprecated npm detection. Uses discovery/review proportionally to scope and risk. |
|
|
48
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
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.
|
|
50
|
+
| @demeter | Database specialist — SQLAlchemy 2.0, Alembic, query optimization, N+1 prevention, TDD migrations, modern DB libs. Preserves review gates for schema/data changes. |
|
|
51
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
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.
|
|
53
|
+
| @hermes | Backend specialist — FastAPI, Python, async, TDD (RED→GREEN→REFACTOR), modern Python stdlib, obsolete lib detection. Uses discovery/review proportionally to scope and risk. |
|
|
54
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
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
56
|
| @nyx | Observability & monitoring specialist — OpenTelemetry tracing, token/cost tracking, agent performance analytics, LangSmith integration. Calls apollo for discovery, sends to themis. |
|
|
57
57
|
| @prometheus | Infrastructure + model provider specialist — Docker, CI/CD, multi-model routing, cost optimization, provider abstraction |
|
|
58
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
|
|
59
|
+
| @themis | Quality & security gate — ruff/Biome linting, dead/legacy code detection, OWASP Top 10, applicable coverage requirements, correctness, deprecation audit. Reviews material/sensitive changes. |
|
|
60
60
|
| @zeus | Central orchestrator — never implements. Delegates to: athena, apollo, hermes, aphrodite, demeter, prometheus, themis, iris, mnemosyne, talos, hephaestus, nyx |
|
|
61
61
|
|
|
62
62
|
## OpenCode Setup
|
|
@@ -73,13 +73,14 @@ Para validar a instalação global do pacote pantheon-opencode COMO UM USUÁRIO
|
|
|
73
73
|
|
|
74
74
|
- Rodar: \`bash ~/pantheon-sandbox/run-test.sh\` (opencode mcp list 5/5 connected + doctor 0 erros + abre TUI isolado)
|
|
75
75
|
- Regra para agentes: ao validar instalação global (npm pack, \`init\`, MCPs, hooks), usar o sandbox — NUNCA testar no ambiente de dev
|
|
76
|
-
-
|
|
77
|
-
-
|
|
76
|
+
- Prepare/atualize com \`bash scripts/test-opencode-v2-sandbox.sh --prepare\`; isso gera \`run-test.sh\`, \`start-pantheon.sh\` e o README do sandbox
|
|
77
|
+
- Descarte somente pelo reset com guardas: \`bash scripts/test-opencode-v2-sandbox.sh --reset\` (nunca use \`rm -rf\` manualmente)
|
|
78
|
+
- Detalhes: ver \`~/pantheon-sandbox/README.md\` após o prepare
|
|
78
79
|
|
|
79
80
|
## Conventions
|
|
80
81
|
|
|
81
|
-
- TDD:
|
|
82
|
-
-
|
|
82
|
+
- TDD: RED→GREEN→REFACTOR for testable behavior; use focused checks for micro-edits
|
|
83
|
+
- Honor repository coverage thresholds when relevant; preserve stronger auth/security/data-integrity gates
|
|
83
84
|
- Async/await on all I/O
|
|
84
85
|
- Type hints on all functions
|
|
85
86
|
- PRs always update the README — every pull request must document newly added features, behaviors, env vars, or commands in the README before being opened.
|
|
@@ -92,20 +93,41 @@ Para validar a instalação global do pacote pantheon-opencode COMO UM USUÁRIO
|
|
|
92
93
|
/**
|
|
93
94
|
* Parse YAML-ish frontmatter from an instruction file.
|
|
94
95
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
96
|
+
* Preserve the file-scope `applyTo` glob separately from the explicit
|
|
97
|
+
* `agents` selectors. In particular, a Python workspace glob is never
|
|
98
|
+
* interpreted as an agent name. The frontmatter block is stripped from
|
|
99
|
+
* the embedded body.
|
|
97
100
|
*
|
|
98
101
|
* @param {string} content - Raw file content
|
|
99
|
-
* @returns {{ name: string, body: string
|
|
102
|
+
* @returns {{ name: string, body: string, applyTo: string | null,
|
|
103
|
+
* agents: string[], fallback: string | null, appliesToAgent: (name: string) => boolean } | null}
|
|
100
104
|
*/
|
|
101
|
-
function parseInstruction(content) {
|
|
105
|
+
export function parseInstruction(content) {
|
|
102
106
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/)
|
|
103
107
|
if (!match) return null
|
|
104
108
|
const fm = match[1]
|
|
105
109
|
const nameMatch = fm.match(/^name:\s*["']?([^"'\r\n]+)["']?\s*$/m)
|
|
106
110
|
if (!nameMatch) return null
|
|
111
|
+
const applyToMatch = fm.match(/^applyTo:\s*["']?([^"'\r\n]+)["']?\s*$/m)
|
|
112
|
+
const agentsMatch = fm.match(/^agents:\s*\[([^\]\r\n]*)\]\s*$/m)
|
|
113
|
+
const agents = agentsMatch
|
|
114
|
+
? agentsMatch[1]
|
|
115
|
+
.split(',')
|
|
116
|
+
.map((agent) => agent.trim().replace(/^(["'])(.*)\1$/, '$2'))
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
: fm.match(/^agents:/m)
|
|
119
|
+
? [] // Malformed explicit selector: fail closed; never make it global.
|
|
120
|
+
: applyToMatch
|
|
121
|
+
? [] // File-scoped instructions need an explicit agent association.
|
|
122
|
+
: ['*'] // Legacy instructions without selectors remain shared.
|
|
123
|
+
const fallbackMatch = fm.match(/^fallback:\s*["']?([^"'\r\n]+)["']?\s*$/m)
|
|
124
|
+
const appliesToAgent = (agent) => agents.includes('*') || agents.includes(agent)
|
|
107
125
|
return {
|
|
108
126
|
name: nameMatch[1].trim(),
|
|
127
|
+
applyTo: applyToMatch?.[1].trim() ?? null,
|
|
128
|
+
agents,
|
|
129
|
+
fallback: fallbackMatch?.[1].trim() ?? null,
|
|
130
|
+
appliesToAgent,
|
|
109
131
|
body: `${content.slice(match[0].length).trimEnd()}\n`,
|
|
110
132
|
}
|
|
111
133
|
}
|
|
@@ -114,7 +136,8 @@ function parseInstruction(content) {
|
|
|
114
136
|
* Read all src/instructions/*.instructions.md files, sorted by filename for
|
|
115
137
|
* deterministic output.
|
|
116
138
|
*
|
|
117
|
-
* @returns {Array<{ file: string, name: string, body: string
|
|
139
|
+
* @returns {Array<{ file: string, name: string, body: string, applyTo: string | null,
|
|
140
|
+
* agents: string[], fallback: string | null, appliesToAgent: (name: string) => boolean }>}
|
|
118
141
|
*/
|
|
119
142
|
export function readInstructions() {
|
|
120
143
|
if (!existsSync(INSTRUCTIONS_DIR)) return []
|
|
@@ -137,10 +160,13 @@ export function readInstructions() {
|
|
|
137
160
|
/**
|
|
138
161
|
* Generate the full AGENTS.md content.
|
|
139
162
|
*
|
|
140
|
-
* @param {Array<{ file: string, name: string, body: string }>} instructions
|
|
163
|
+
* @param {Array<{ file: string, name: string, body: string, agents?: string[] }>} instructions
|
|
141
164
|
* @returns {string}
|
|
142
165
|
*/
|
|
143
166
|
export function generateAgentsMd(instructions) {
|
|
167
|
+
const sharedInstructions = instructions.filter(
|
|
168
|
+
(instr) => !instr.agents || instr.agents.includes('*'),
|
|
169
|
+
)
|
|
144
170
|
const sections = [
|
|
145
171
|
'<!-- Generated by scripts/build-agents-md.mjs — do not edit directly. -->',
|
|
146
172
|
'<!-- Source of truth: src/instructions/*.instructions.md (AGENTS.md is a committed build artifact). -->',
|
|
@@ -153,10 +179,11 @@ export function generateAgentsMd(instructions) {
|
|
|
153
179
|
'',
|
|
154
180
|
'The following sections are consolidated from `src/instructions/*.instructions.md`',
|
|
155
181
|
'so both OpenCode V1 (auto-loads AGENTS.md) and V2 (loads AGENTS.md, ignores the',
|
|
156
|
-
'`instructions` config key) receive identical
|
|
182
|
+
'`instructions` config key) receive identical shared rules. Agent-scoped rules',
|
|
183
|
+
'are rendered into only the matching installed agent prompts.',
|
|
157
184
|
'',
|
|
158
185
|
]
|
|
159
|
-
for (const instr of
|
|
186
|
+
for (const instr of sharedInstructions) {
|
|
160
187
|
sections.push(
|
|
161
188
|
`<!-- Source: src/instructions/${instr.file} -->`,
|
|
162
189
|
`## ${instr.name}`,
|
|
@@ -168,6 +195,110 @@ export function generateAgentsMd(instructions) {
|
|
|
168
195
|
return `${sections.join('\n').trimEnd()}\n`
|
|
169
196
|
}
|
|
170
197
|
|
|
198
|
+
const GENERATED_AGENT_BLOCK =
|
|
199
|
+
/\n?<!-- Pantheon selective instructions: generated -->[\s\S]*?<!-- \/Pantheon selective instructions -->\n?/g
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Render the agent-specific instruction set into the supported agent prompt.
|
|
203
|
+
* The shared rules remain in AGENTS.md; file globs are retained as scope
|
|
204
|
+
* guidance and are never used as agent selectors.
|
|
205
|
+
*
|
|
206
|
+
* @param {string} agentName - Canonical agent name from the prompt filename
|
|
207
|
+
* @param {string} prompt - Canonical source agent prompt
|
|
208
|
+
* @param {Array<{ file: string, name: string, body: string, applyTo?: string | null,
|
|
209
|
+
* agents?: string[], fallback?: string | null }>} instructions
|
|
210
|
+
* @returns {string}
|
|
211
|
+
*/
|
|
212
|
+
export function generateAgentPrompt(agentName, prompt, instructions = readInstructions()) {
|
|
213
|
+
const basePrompt = prompt.replace(GENERATED_AGENT_BLOCK, '').trimEnd()
|
|
214
|
+
const selected = instructions.filter(
|
|
215
|
+
(instr) => instr.agents && !instr.agents.includes('*') && instr.agents.includes(agentName),
|
|
216
|
+
)
|
|
217
|
+
if (selected.length === 0) {
|
|
218
|
+
return [
|
|
219
|
+
basePrompt,
|
|
220
|
+
'',
|
|
221
|
+
'<!-- Pantheon selective instructions: generated -->',
|
|
222
|
+
'No agent-specific instructions are assigned; follow the shared rules in AGENTS.md.',
|
|
223
|
+
'<!-- /Pantheon selective instructions -->',
|
|
224
|
+
'',
|
|
225
|
+
].join('\n')
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const sections = selected.map((instr) => {
|
|
229
|
+
const scope = instr.applyTo
|
|
230
|
+
? `\n\n> Apply only when a target workspace file matches \`${instr.applyTo}\`.`
|
|
231
|
+
: ''
|
|
232
|
+
const fallback =
|
|
233
|
+
instr.fallback === 'shared'
|
|
234
|
+
? '\n\n> For work outside this scope, use the shared rules in AGENTS.md.'
|
|
235
|
+
: ''
|
|
236
|
+
return [
|
|
237
|
+
`<!-- Source: src/instructions/${instr.file} -->`,
|
|
238
|
+
`## ${instr.name}${scope}`,
|
|
239
|
+
'',
|
|
240
|
+
`${instr.body.trimEnd()}${fallback}`,
|
|
241
|
+
].join('\n')
|
|
242
|
+
})
|
|
243
|
+
return [
|
|
244
|
+
basePrompt,
|
|
245
|
+
'',
|
|
246
|
+
'<!-- Pantheon selective instructions: generated -->',
|
|
247
|
+
sections.join('\n\n'),
|
|
248
|
+
'<!-- /Pantheon selective instructions -->',
|
|
249
|
+
'',
|
|
250
|
+
].join('\n')
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Copy agent prompts with scoped instructions embedded in each actual prompt.
|
|
255
|
+
* Used by both init and postinstall sync so the installed runtime receives the
|
|
256
|
+
* same selection without a per-turn injection hook.
|
|
257
|
+
*
|
|
258
|
+
* @param {string} sourceDir - Canonical src/agents directory
|
|
259
|
+
* @param {string} targetDir - OpenCode agents directory
|
|
260
|
+
* @param {boolean} dryRun - Do not write when true
|
|
261
|
+
* @param {boolean} existingOnly - Refresh installed prompts without adding missing agents
|
|
262
|
+
* @returns {{ created: number, skipped: number }}
|
|
263
|
+
*/
|
|
264
|
+
export function copyAgentPrompts(sourceDir, targetDir, dryRun = false, existingOnly = false) {
|
|
265
|
+
const instructions = readInstructions()
|
|
266
|
+
if (!dryRun && !existingOnly) mkdirSync(targetDir, { recursive: true })
|
|
267
|
+
let created = 0
|
|
268
|
+
let skipped = 0
|
|
269
|
+
for (const file of readdirSync(sourceDir)
|
|
270
|
+
.filter((entry) => entry.endsWith('.md'))
|
|
271
|
+
.sort()) {
|
|
272
|
+
if (file.toLowerCase() === 'readme.md') continue
|
|
273
|
+
const sourcePath = join(sourceDir, file)
|
|
274
|
+
if (!statSync(sourcePath).isFile()) continue
|
|
275
|
+
const agentName = file.replace(/\.md$/, '')
|
|
276
|
+
const output = generateAgentPrompt(agentName, readFileSync(sourcePath, 'utf8'), instructions)
|
|
277
|
+
const targetPath = join(targetDir, file)
|
|
278
|
+
if (existingOnly && !existsSync(targetPath)) continue
|
|
279
|
+
const existing = existsSync(targetPath) ? readFileSync(targetPath, 'utf8') : null
|
|
280
|
+
if (existing === output) {
|
|
281
|
+
skipped++
|
|
282
|
+
} else {
|
|
283
|
+
if (!dryRun) writeFileSync(targetPath, output, 'utf8')
|
|
284
|
+
created++
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return { created, skipped }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Count LF-delimited logical lines without assuming a trailing newline.
|
|
292
|
+
*
|
|
293
|
+
* @param {string} content - File content
|
|
294
|
+
* @returns {number}
|
|
295
|
+
*/
|
|
296
|
+
export function logicalLineCount(content) {
|
|
297
|
+
if (content.length === 0) return 0
|
|
298
|
+
const separators = content.match(/\n/g)?.length ?? 0
|
|
299
|
+
return separators + (content.endsWith('\n') ? 0 : 1)
|
|
300
|
+
}
|
|
301
|
+
|
|
171
302
|
// ---------------------------------------------------------------------------
|
|
172
303
|
// CLI entry point
|
|
173
304
|
// ---------------------------------------------------------------------------
|
|
@@ -175,6 +306,9 @@ export function generateAgentsMd(instructions) {
|
|
|
175
306
|
function main() {
|
|
176
307
|
const checkOnly = process.argv.includes('--check')
|
|
177
308
|
const instructions = readInstructions()
|
|
309
|
+
const sharedCount = instructions.filter(
|
|
310
|
+
(instr) => !instr.agents || instr.agents.includes('*'),
|
|
311
|
+
).length
|
|
178
312
|
const content = generateAgentsMd(instructions)
|
|
179
313
|
|
|
180
314
|
if (checkOnly) {
|
|
@@ -183,15 +317,15 @@ function main() {
|
|
|
183
317
|
console.error(`❌ AGENTS.md is stale — run: node scripts/build-agents-md.mjs`)
|
|
184
318
|
process.exit(1)
|
|
185
319
|
}
|
|
186
|
-
console.log(`✅ AGENTS.md is up to date (${
|
|
320
|
+
console.log(`✅ AGENTS.md is up to date (${sharedCount} shared instruction sections)`)
|
|
187
321
|
return
|
|
188
322
|
}
|
|
189
323
|
|
|
190
324
|
writeFileSync(AGENTS_MD_PATH, content, 'utf8')
|
|
191
|
-
const lines = content
|
|
192
|
-
console.log(
|
|
193
|
-
`✅ Generated AGENTS.md (${instructions.length} instruction sections, ${lines} lines)`,
|
|
194
|
-
)
|
|
325
|
+
const lines = logicalLineCount(content)
|
|
326
|
+
console.log(`✅ Generated AGENTS.md (${sharedCount} shared instruction sections, ${lines} lines)`)
|
|
195
327
|
}
|
|
196
328
|
|
|
197
|
-
|
|
329
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
330
|
+
main()
|
|
331
|
+
}
|
package/scripts/doctor.mjs
CHANGED
|
@@ -1200,33 +1200,58 @@ export function resolveMcpCwd(entry, configPath = '') {
|
|
|
1200
1200
|
// ---------------------------------------------------------------------------
|
|
1201
1201
|
|
|
1202
1202
|
/**
|
|
1203
|
-
* Prerelease-aware comparison for pantheon versions (X.Y.Z
|
|
1204
|
-
*
|
|
1203
|
+
* Prerelease-aware comparison for pantheon versions (X.Y.Z with an optional
|
|
1204
|
+
* semver prerelease suffix: `-beta.9`, `-rc.1`, ...). A stable release
|
|
1205
|
+
* outranks any prerelease of the same core version; two prereleases compare
|
|
1206
|
+
* identifier by identifier, numeric identifiers numerically. Returns
|
|
1205
1207
|
* <0, 0, >0 (a - b). The shared compareVersions/parseVersion above ignores
|
|
1206
|
-
* the
|
|
1207
|
-
* every
|
|
1208
|
+
* the prerelease suffix (it only reads 4 numeric groups), which would make
|
|
1209
|
+
* every prerelease pair compare equal.
|
|
1208
1210
|
*/
|
|
1209
1211
|
function comparePantheonVersions(a, b) {
|
|
1210
1212
|
const parse = (v) => {
|
|
1211
1213
|
const m = String(v)
|
|
1212
1214
|
.trim()
|
|
1213
|
-
.match(/^(\d+)\.(\d+)\.(\d+)(?:-
|
|
1215
|
+
.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/)
|
|
1214
1216
|
if (!m) return null
|
|
1215
1217
|
return {
|
|
1216
1218
|
core: [+m[1], +m[2], +m[3]],
|
|
1217
|
-
|
|
1219
|
+
prerelease: m[4] === undefined ? null : m[4].split('.'),
|
|
1218
1220
|
}
|
|
1219
1221
|
}
|
|
1222
|
+
// Per semver precedence: numeric identifiers compare numerically and rank
|
|
1223
|
+
// below alphanumeric ones; a shorter identifier list ranks below a longer
|
|
1224
|
+
// one sharing its prefix; a version WITHOUT a prerelease outranks any
|
|
1225
|
+
// prerelease of the same core version.
|
|
1226
|
+
const comparePrerelease = (pa, pb) => {
|
|
1227
|
+
if (pa === null && pb === null) return 0
|
|
1228
|
+
if (pa === null) return 1
|
|
1229
|
+
if (pb === null) return -1
|
|
1230
|
+
const length = Math.max(pa.length, pb.length)
|
|
1231
|
+
for (let i = 0; i < length; i++) {
|
|
1232
|
+
const x = pa[i]
|
|
1233
|
+
const y = pb[i]
|
|
1234
|
+
if (x === undefined) return -1
|
|
1235
|
+
if (y === undefined) return 1
|
|
1236
|
+
const xNumeric = /^\d+$/.test(x)
|
|
1237
|
+
const yNumeric = /^\d+$/.test(y)
|
|
1238
|
+
if (xNumeric && yNumeric) {
|
|
1239
|
+
if (Number(x) !== Number(y)) return Number(x) < Number(y) ? -1 : 1
|
|
1240
|
+
} else if (xNumeric !== yNumeric) {
|
|
1241
|
+
return xNumeric ? -1 : 1
|
|
1242
|
+
} else if (x !== y) {
|
|
1243
|
+
return x < y ? -1 : 1
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return 0
|
|
1247
|
+
}
|
|
1220
1248
|
const pa = parse(a)
|
|
1221
1249
|
const pb = parse(b)
|
|
1222
1250
|
if (!pa || !pb) return 0
|
|
1223
1251
|
for (let i = 0; i < 3; i++) {
|
|
1224
1252
|
if (pa.core[i] !== pb.core[i]) return pa.core[i] - pb.core[i]
|
|
1225
1253
|
}
|
|
1226
|
-
|
|
1227
|
-
// as drift for any caller using a strict === 0 check.
|
|
1228
|
-
if (pa.beta === pb.beta) return 0
|
|
1229
|
-
return pa.beta < pb.beta ? -1 : 1
|
|
1254
|
+
return comparePrerelease(pa.prerelease, pb.prerelease)
|
|
1230
1255
|
}
|
|
1231
1256
|
|
|
1232
1257
|
/**
|
|
@@ -1292,8 +1317,24 @@ export function checkInstallVersionDrift(args) {
|
|
|
1292
1317
|
// registered plugin points into against this package's own version. Without
|
|
1293
1318
|
// network access the "latest" version IS the running package's version.
|
|
1294
1319
|
|
|
1320
|
+
// Single source of truth: the package name is read from package.json, the same
|
|
1321
|
+
// way scripts/install/opencode.mjs derives the V2 pin spec. The fallback keeps
|
|
1322
|
+
// the doctor usable on a broken manifest.
|
|
1323
|
+
const PACKAGE_MANIFEST = readJson(join(ROOT, 'package.json'))
|
|
1324
|
+
const PANTHEON_PACKAGE_NAME =
|
|
1325
|
+
typeof PACKAGE_MANIFEST?.name === 'string' && PACKAGE_MANIFEST.name
|
|
1326
|
+
? PACKAGE_MANIFEST.name
|
|
1327
|
+
: 'pantheon-opencode'
|
|
1328
|
+
// Matches the exact version-pinned V2 spec the installer writes
|
|
1329
|
+
// (`<name>@X.Y.Z` plus any semver prerelease suffix). The name is escaped
|
|
1330
|
+
// because package names may contain regex-significant characters (`.` in
|
|
1331
|
+
// unscoped names, `/` in scoped ones).
|
|
1332
|
+
const PINNED_PLUGIN_SPEC_PATTERN = new RegExp(
|
|
1333
|
+
`^${PANTHEON_PACKAGE_NAME.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&')}@(\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?)$`,
|
|
1334
|
+
)
|
|
1335
|
+
|
|
1295
1336
|
/** Locates an installed copy of this package inside a dependency tree. */
|
|
1296
|
-
const INSTALLED_PACKAGE_MARKER =
|
|
1337
|
+
const INSTALLED_PACKAGE_MARKER = `/node_modules/${PANTHEON_PACKAGE_NAME}/`
|
|
1297
1338
|
|
|
1298
1339
|
/**
|
|
1299
1340
|
* Return the installed-package root containing a plugin path, or null when the
|
|
@@ -1362,8 +1403,10 @@ export function collectRegisteredPluginRefs(configs) {
|
|
|
1362
1403
|
if (typeof ref === 'string') {
|
|
1363
1404
|
refs.push({ path: ref, configPath: cfg.path, configLabel: cfg.label })
|
|
1364
1405
|
} else if (ref && typeof ref === 'object') {
|
|
1365
|
-
//
|
|
1366
|
-
|
|
1406
|
+
// npm plugin entries carry their identity under `package`; local
|
|
1407
|
+
// entries use `path`/`source`. Preserve either so H3 can inspect the
|
|
1408
|
+
// exact pinned npm version as well as installed filesystem copies.
|
|
1409
|
+
const objectPath = ['package', 'path', 'source', 'id', 'name'].find(
|
|
1367
1410
|
(k) => typeof ref[k] === 'string',
|
|
1368
1411
|
)
|
|
1369
1412
|
if (objectPath) {
|
|
@@ -1376,6 +1419,20 @@ export function collectRegisteredPluginRefs(configs) {
|
|
|
1376
1419
|
return refs
|
|
1377
1420
|
}
|
|
1378
1421
|
|
|
1422
|
+
/**
|
|
1423
|
+
* Read an exact version from the package-root plugin syntax written by the
|
|
1424
|
+
* V2 installer (`pantheon-opencode@1.6.0`), including ANY semver prerelease
|
|
1425
|
+
* suffix (`-beta.11`, `-rc.1`, ...). Unpinned and unrelated packages
|
|
1426
|
+
* deliberately return null: H3 cannot infer their resolved npm cache version.
|
|
1427
|
+
* @param {string} pluginRef
|
|
1428
|
+
* @returns {string|null}
|
|
1429
|
+
*/
|
|
1430
|
+
function readPinnedPantheonPluginVersion(pluginRef) {
|
|
1431
|
+
if (typeof pluginRef !== 'string') return null
|
|
1432
|
+
const match = pluginRef.match(PINNED_PLUGIN_SPEC_PATTERN)
|
|
1433
|
+
return match?.[1] ?? null
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1379
1436
|
/**
|
|
1380
1437
|
* Compare the version of the package each registered plugin points into (when
|
|
1381
1438
|
* it is an installed copy under node_modules/pantheon-opencode) against this
|
|
@@ -1404,6 +1461,21 @@ export function checkPluginVersionDrift(args) {
|
|
|
1404
1461
|
let compared = 0
|
|
1405
1462
|
let driftReported = false
|
|
1406
1463
|
for (const ref of refs) {
|
|
1464
|
+
const pinnedNpmVersion = readPinnedPantheonPluginVersion(ref.path)
|
|
1465
|
+
if (pinnedNpmVersion) {
|
|
1466
|
+
const identity = `npm:${PANTHEON_PACKAGE_NAME}@${pinnedNpmVersion}`
|
|
1467
|
+
if (seen.has(identity)) continue
|
|
1468
|
+
seen.add(identity)
|
|
1469
|
+
compared++
|
|
1470
|
+
|
|
1471
|
+
if (classifyPluginVersionDrift(pinnedNpmVersion, packageVersion) !== 'drift') continue
|
|
1472
|
+
driftReported = true
|
|
1473
|
+
warn(
|
|
1474
|
+
`${ref.configLabel}: V2 plugin is pinned to ${PANTHEON_PACKAGE_NAME}@${pinnedNpmVersion}, but this package is v${packageVersion} — the registered tool surface may be stale. Run \`npx pantheon-opencode init\` to realign the pinned plugin version`,
|
|
1475
|
+
)
|
|
1476
|
+
continue
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1407
1479
|
const absolute = isAbsolute(ref.path) ? ref.path : resolve(dirname(ref.configPath), ref.path)
|
|
1408
1480
|
const installed = readInstalledPluginVersion(absolute)
|
|
1409
1481
|
if (!installed) continue
|
|
@@ -1442,21 +1514,29 @@ export function checkPluginVersionDrift(args) {
|
|
|
1442
1514
|
* The V2 contract is the PLURAL `plugins` key; V1 is the singular `plugin` key.
|
|
1443
1515
|
* So the key alone is not the test — a config can legitimately carry a
|
|
1444
1516
|
* third-party entry in `plugins`. What identifies the generation is a Pantheon
|
|
1445
|
-
* V2 entry inside it.
|
|
1446
|
-
*
|
|
1447
|
-
*
|
|
1517
|
+
* V2 entry inside it. Recognises the canonical directory, an absolute path,
|
|
1518
|
+
* the package subpath export, and the version-pinned package-root entry emitted
|
|
1519
|
+
* by the installer. The exact package name is checked to avoid matching
|
|
1520
|
+
* similarly named third-party plugins.
|
|
1448
1521
|
*
|
|
1449
1522
|
* @param {unknown} ref
|
|
1450
1523
|
* @returns {boolean}
|
|
1451
1524
|
*/
|
|
1452
1525
|
function isV2PluginRef(ref) {
|
|
1453
|
-
const
|
|
1526
|
+
const candidates =
|
|
1454
1527
|
typeof ref === 'string'
|
|
1455
|
-
? ref
|
|
1528
|
+
? [ref]
|
|
1456
1529
|
: ref && typeof ref === 'object'
|
|
1457
|
-
?
|
|
1458
|
-
|
|
1459
|
-
|
|
1530
|
+
? ['package', 'path', 'source', 'id', 'name']
|
|
1531
|
+
.map((key) => ref[key])
|
|
1532
|
+
.filter((value) => typeof value === 'string')
|
|
1533
|
+
: []
|
|
1534
|
+
return candidates.some(
|
|
1535
|
+
(candidate) =>
|
|
1536
|
+
candidate.includes('plugin-v2') ||
|
|
1537
|
+
candidate === PANTHEON_PACKAGE_NAME ||
|
|
1538
|
+
candidate.startsWith(`${PANTHEON_PACKAGE_NAME}@`),
|
|
1539
|
+
)
|
|
1460
1540
|
}
|
|
1461
1541
|
|
|
1462
1542
|
/**
|
|
@@ -1832,7 +1912,9 @@ export function classifyNodeSqliteProbe(probe) {
|
|
|
1832
1912
|
if (probe.available) {
|
|
1833
1913
|
return {
|
|
1834
1914
|
status: 'ok',
|
|
1835
|
-
message:
|
|
1915
|
+
message:
|
|
1916
|
+
`node:sqlite available on ${probe.version} — read-only SQLite backend prerequisite satisfied; ` +
|
|
1917
|
+
'doctor does not verify pantheon_cost tool registration',
|
|
1836
1918
|
}
|
|
1837
1919
|
}
|
|
1838
1920
|
return {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* agents-md.mjs — Dynamic AGENTS.md generator from canonical agent frontmatter
|
|
4
4
|
*
|
|
5
|
-
* Reads all agents/*.
|
|
5
|
+
* Reads all canonical src/agents/*.md files, extracts name + description from frontmatter,
|
|
6
6
|
* and generates AGENTS.md content dynamically. Eliminates hardcoded agent tables
|
|
7
7
|
* in platform installers (claude.mjs, cursor.mjs, windsurf.mjs, cline.mjs, etc.).
|
|
8
8
|
*
|
|
@@ -64,7 +64,7 @@ function parseAgentFrontmatter(content) {
|
|
|
64
64
|
// ---------------------------------------------------------------------------
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
|
-
* Read all canonical agents/*.
|
|
67
|
+
* Read all canonical src/agents/*.md files, parse frontmatter,
|
|
68
68
|
* and return an array of agent metadata objects sorted alphabetically by name.
|
|
69
69
|
*
|
|
70
70
|
* Skips files that don't match the `*.agent.md` pattern, files without
|