pantheon-opencode 1.0.0
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 +37 -0
- package/LICENSE +21 -0
- package/README.md +1013 -0
- package/bin/pantheon-init.mjs +183 -0
- package/commands/pantheon-audit.md +25 -0
- package/commands/pantheon-bg.md +10 -0
- package/commands/pantheon-consolidate.md +11 -0
- package/commands/pantheon-deepwork.md +128 -0
- package/commands/pantheon-doc.md +10 -0
- package/commands/pantheon-focus.md +9 -0
- package/commands/pantheon-forget.md +58 -0
- package/commands/pantheon-hash.md +11 -0
- package/commands/pantheon-optimize.md +79 -0
- package/commands/pantheon-remember.md +44 -0
- package/commands/pantheon-search.md +48 -0
- package/commands/pantheon-status.md +71 -0
- package/commands/pantheon-todo.md +11 -0
- package/commands/pantheon.md +49 -0
- package/docs/AGENT-MCP.md +194 -0
- package/docs/ARCHITECTURE.md +384 -0
- package/docs/BRANCH-PROTECTION.md +142 -0
- package/docs/INDEX.md +81 -0
- package/docs/INSTALLATION.md +217 -0
- package/docs/MCP.md +238 -0
- package/docs/MEMORY.md +471 -0
- package/docs/MIGRATION-MEMORY-BANK.md +139 -0
- package/docs/PLATFORMS.md +5 -0
- package/docs/QUICKSTART.md +49 -0
- package/docs/README.md +18 -0
- package/docs/RELEASING.md +256 -0
- package/docs/SETUP.md +5 -0
- package/docs/UPGRADING.md +41 -0
- package/docs/mcp-recommendations.md +439 -0
- package/docs/mcp-tools.md +156 -0
- package/docs/mcp-user-guide.md +204 -0
- package/docs/persistence-mcp.md +111 -0
- package/package.json +72 -0
- package/pantheon.schema.json +158 -0
- package/scripts/__init__.py +0 -0
- package/scripts/_pantheon_paths.py +68 -0
- package/scripts/check-version-consistency.sh +23 -0
- package/scripts/code_mode_server.py +202 -0
- package/scripts/doctor.mjs +763 -0
- package/scripts/generate-prompts.sh +222 -0
- package/scripts/generate-routing-docs.mjs +104 -0
- package/scripts/hash_verify.py +192 -0
- package/scripts/init-pantheon-mcp.sh +118 -0
- package/scripts/install/agents-md.mjs +214 -0
- package/scripts/install/health-check.mjs +196 -0
- package/scripts/install/migrate.mjs +209 -0
- package/scripts/install/opencode.mjs +645 -0
- package/scripts/install/shared.mjs +655 -0
- package/scripts/install/venv.mjs +116 -0
- package/scripts/install-mcp.mjs +885 -0
- package/scripts/install.mjs +26 -0
- package/scripts/manifest.mjs +622 -0
- package/scripts/mcp_persistence_server.py +459 -0
- package/scripts/mcp_resources_server.py +206 -0
- package/scripts/memory_cache.py +78 -0
- package/scripts/memory_mcp_server.py +605 -0
- package/scripts/paths.py +64 -0
- package/scripts/prune_context.py +72 -0
- package/scripts/release-bundle.mjs +109 -0
- package/scripts/scrub-secrets.py +282 -0
- package/scripts/scrub_secrets.py +281 -0
- package/scripts/test-context-compression.sh +166 -0
- package/scripts/themis_heuristic_scan.py +287 -0
- package/scripts/todo_enforcer.py +242 -0
- package/scripts/uninstall.mjs +1057 -0
- package/scripts/validate-routing.mjs +160 -0
- package/scripts/validate_agent_frontmatter.py +226 -0
- package/scripts/versioning.mjs +254 -0
- package/skills-lock.json +16 -0
- package/src/agents/aphrodite.md +162 -0
- package/src/agents/apollo.md +109 -0
- package/src/agents/athena.md +226 -0
- package/src/agents/demeter.md +146 -0
- package/src/agents/gaia.md +82 -0
- package/src/agents/hephaestus.md +105 -0
- package/src/agents/hermes.md +302 -0
- package/src/agents/iris.md +99 -0
- package/src/agents/mnemosyne.md +226 -0
- package/src/agents/nyx.md +87 -0
- package/src/agents/prometheus.md +199 -0
- package/src/agents/talos.md +93 -0
- package/src/agents/themis.md +187 -0
- package/src/agents/zeus.md +209 -0
- package/src/instructions/agent-return-format.instructions.md +26 -0
- package/src/instructions/backend-standards.instructions.md +45 -0
- package/src/instructions/documentation-standards.instructions.md +53 -0
- package/src/instructions/frontend-standards.instructions.md +46 -0
- package/src/instructions/memory-protocol.instructions.md +67 -0
- package/src/instructions/yagni.instructions.md +21 -0
- package/src/instructions/zeus-anti-stall.instructions.md +72 -0
- package/src/instructions/zeus-communication-rules.instructions.md +15 -0
- package/src/instructions/zeus-council-synthesis.instructions.md +105 -0
- package/src/instructions/zeus-timeout-retry.instructions.md +127 -0
- package/src/mcp/_pantheon_paths.py +67 -0
- package/src/mcp/code_mode_server.py +202 -0
- package/src/mcp/init-pantheon-mcp.sh +118 -0
- package/src/mcp/install-mcp.mjs +885 -0
- package/src/mcp/mcp_persistence_server.py +458 -0
- package/src/mcp/mcp_resources_server.py +205 -0
- package/src/mcp/memory_mcp_server.py +604 -0
- package/src/mcp/requirements-mcp-core.txt +5 -0
- package/src/mcp/requirements-mcp.txt +5 -0
- package/src/plugin.ts +19 -0
- package/src/plugins/tui/dist/tui.js +143 -0
- package/src/plugins/tui/dist/tui.tsx +144 -0
- package/src/plugins/tui/package.json +32 -0
- package/src/plugins/tui/src/index.tsx +144 -0
- package/src/plugins/tui/tsdown.config.ts +22 -0
- package/src/routing.yml +499 -0
- package/src/skills/README.md +230 -0
- package/src/skills/agent-coordination/SKILL.md +95 -0
- package/src/skills/artifact-management/SKILL.md +118 -0
- package/src/skills/auto-continue/SKILL.md +280 -0
- package/src/skills/code-review-checklist/SKILL.md +139 -0
- package/src/skills/context-compression/SKILL.md +861 -0
- package/src/skills/git-workflow-and-versioning/SKILL.md +32 -0
- package/src/skills/incremental-implementation/SKILL.md +27 -0
- package/src/skills/memory-bank/SKILL.md +165 -0
- package/src/skills/orchestration-workflow/SKILL.md +311 -0
- package/src/skills/security-hardening/SKILL.md +36 -0
- package/src/skills/session-goal/SKILL.md +138 -0
- package/src/skills/spec-driven-development/SKILL.md +23 -0
- package/src/skills/tdd-with-agents/SKILL.md +170 -0
- package/src/skills/visual-review-pipeline/SKILL.md +200 -0
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* doctor.mjs — Pantheon health check CLI tool
|
|
5
|
+
*
|
|
6
|
+
* Validates that a Pantheon installation is working correctly:
|
|
7
|
+
* - Agent file presence across canonical → platform
|
|
8
|
+
* - MCP configuration (gh_grep, Context7, etc.)
|
|
9
|
+
* - Permission/frontmatter mismatches
|
|
10
|
+
* - Sync freshness
|
|
11
|
+
* - Git status
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* node scripts/doctor.mjs # auto-detect, cwd
|
|
15
|
+
* node scripts/doctor.mjs --target /path/to/project
|
|
16
|
+
* node scripts/doctor.mjs --platform opencode
|
|
17
|
+
* node scripts/doctor.mjs --fix # attempt auto-fixes
|
|
18
|
+
* node scripts/doctor.mjs --verbose
|
|
19
|
+
* node scripts/doctor.mjs --help
|
|
20
|
+
*
|
|
21
|
+
* Exit codes:
|
|
22
|
+
* 0 = all green
|
|
23
|
+
* 1 = warnings only
|
|
24
|
+
* 2 = errors (needs attention)
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { spawnSync } from 'node:child_process'
|
|
28
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
29
|
+
import { dirname, join } from 'node:path'
|
|
30
|
+
import { fileURLToPath } from 'node:url'
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Paths
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
37
|
+
const ROOT = join(__dirname, '..')
|
|
38
|
+
const AGENTS_DIR = join(ROOT, 'agents')
|
|
39
|
+
const PLATFORM_DIR = join(ROOT, 'platform')
|
|
40
|
+
|
|
41
|
+
// All known platforms (must match platform/ subdirectories with adapter.json)
|
|
42
|
+
const _ALL_PLATFORMS = ['opencode', 'claude', 'cursor', 'windsurf', 'copilot', 'continue', 'cline']
|
|
43
|
+
|
|
44
|
+
const PLATFORM_LABELS = {
|
|
45
|
+
opencode: 'OpenCode',
|
|
46
|
+
claude: 'Claude Code',
|
|
47
|
+
cursor: 'Cursor',
|
|
48
|
+
windsurf: 'Windsurf',
|
|
49
|
+
copilot: 'VS Code / Copilot',
|
|
50
|
+
continue: 'Continue.dev',
|
|
51
|
+
cline: 'Cline',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// State
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/** @type {{ pass: number; warn: number; error: number; info: number }} */
|
|
59
|
+
const counts = { pass: 0, warn: 0, error: 0, info: 0 }
|
|
60
|
+
|
|
61
|
+
const results = []
|
|
62
|
+
let exitCode = 0
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// CLI arg parsing
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
function parseArgs(argv) {
|
|
69
|
+
const args = {
|
|
70
|
+
target: null,
|
|
71
|
+
platform: null,
|
|
72
|
+
fix: false,
|
|
73
|
+
verbose: false,
|
|
74
|
+
help: false,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (let i = 2; i < argv.length; i++) {
|
|
78
|
+
switch (argv[i]) {
|
|
79
|
+
case '--target':
|
|
80
|
+
args.target = argv[++i]
|
|
81
|
+
break
|
|
82
|
+
case '--platform':
|
|
83
|
+
args.platform = argv[++i]
|
|
84
|
+
break
|
|
85
|
+
case '--fix':
|
|
86
|
+
args.fix = true
|
|
87
|
+
break
|
|
88
|
+
case '--verbose':
|
|
89
|
+
args.verbose = true
|
|
90
|
+
break
|
|
91
|
+
case '--help':
|
|
92
|
+
case '-h':
|
|
93
|
+
args.help = true
|
|
94
|
+
break
|
|
95
|
+
default:
|
|
96
|
+
console.warn(`⚠️ Unknown option: ${argv[i]}`)
|
|
97
|
+
break
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!args.target) {
|
|
102
|
+
args.target = process.cwd()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Resolve to absolute
|
|
106
|
+
if (!args.target.startsWith('/')) {
|
|
107
|
+
args.target = join(process.cwd(), args.target)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return args
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Output helpers
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
const ICON = {
|
|
118
|
+
pass: '\u2705', // ✅
|
|
119
|
+
warn: '\u26A0\uFE0F', // ⚠️
|
|
120
|
+
error: '\u274C', // ❌
|
|
121
|
+
info: '\u2139\uFE0F', // ℹ️
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function emit(category, message) {
|
|
125
|
+
counts[category]++
|
|
126
|
+
results.push({ category, message })
|
|
127
|
+
if (category === 'pass' && !process.argv.includes('--verbose')) return
|
|
128
|
+
const prefix = ICON[category] ?? category
|
|
129
|
+
console.log(` ${prefix} ${message}`)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function pass(msg) {
|
|
133
|
+
emit('pass', msg)
|
|
134
|
+
}
|
|
135
|
+
function warn(msg) {
|
|
136
|
+
emit('warn', msg)
|
|
137
|
+
if (exitCode < 1) exitCode = 1
|
|
138
|
+
}
|
|
139
|
+
function error(msg) {
|
|
140
|
+
emit('error', msg)
|
|
141
|
+
exitCode = 2
|
|
142
|
+
}
|
|
143
|
+
function info(msg) {
|
|
144
|
+
emit('info', msg)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function section(title) {
|
|
148
|
+
console.log(`\n${'='.repeat(60)}`)
|
|
149
|
+
console.log(` ${title}`)
|
|
150
|
+
console.log(`${'='.repeat(60)}`)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function showHelp() {
|
|
154
|
+
console.log(`
|
|
155
|
+
Pantheon Doctor — Health Check CLI
|
|
156
|
+
|
|
157
|
+
Usage:
|
|
158
|
+
node scripts/doctor.mjs auto-detect, cwd
|
|
159
|
+
node scripts/doctor.mjs --target /path/to/project target project
|
|
160
|
+
node scripts/doctor.mjs --platform opencode specific platform
|
|
161
|
+
node scripts/doctor.mjs --fix attempt auto-fixes
|
|
162
|
+
node scripts/doctor.mjs --verbose detailed output
|
|
163
|
+
node scripts/doctor.mjs --help show this help
|
|
164
|
+
|
|
165
|
+
Checks:
|
|
166
|
+
A. Agent Files — canonical vs platform sync
|
|
167
|
+
B. MCP Configuration — opencode.json MCP settings
|
|
168
|
+
C. Permission Checks — frontmatter validation
|
|
169
|
+
D. Sync Status — npm run sync freshness
|
|
170
|
+
E. Git Status — uncommitted changes
|
|
171
|
+
|
|
172
|
+
Exit codes:
|
|
173
|
+
0 — all checks pass
|
|
174
|
+
1 — warnings
|
|
175
|
+
2 — errors
|
|
176
|
+
|
|
177
|
+
Report bugs: https://github.com/ils15/pantheon/issues
|
|
178
|
+
`)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Helper utilities
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Collect canonical agent names from agents/*.agent.md
|
|
187
|
+
* @returns {string[]} sorted list of agent names (without extension)
|
|
188
|
+
*/
|
|
189
|
+
function getCanonicalAgentNames() {
|
|
190
|
+
if (!existsSync(AGENTS_DIR)) return []
|
|
191
|
+
return readdirSync(AGENTS_DIR)
|
|
192
|
+
.filter((f) => f.endsWith('.agent.md'))
|
|
193
|
+
.map((f) => f.replace(/\.agent\.md$/, ''))
|
|
194
|
+
.sort()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Read JSON from a file, returning null on failure.
|
|
199
|
+
* @param {string} filePath
|
|
200
|
+
* @returns {object|null}
|
|
201
|
+
*/
|
|
202
|
+
function readJson(filePath) {
|
|
203
|
+
try {
|
|
204
|
+
if (!existsSync(filePath)) return null
|
|
205
|
+
const raw = readFileSync(filePath, 'utf8')
|
|
206
|
+
return JSON.parse(raw)
|
|
207
|
+
} catch {
|
|
208
|
+
return null
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Spawn a process and return { status, stdout, stderr }.
|
|
214
|
+
*/
|
|
215
|
+
function spawn(prog, args, cwd = null) {
|
|
216
|
+
const result = spawnSync(prog, args, {
|
|
217
|
+
cwd: cwd ?? ROOT,
|
|
218
|
+
encoding: 'utf8',
|
|
219
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
220
|
+
})
|
|
221
|
+
return {
|
|
222
|
+
status: result.status,
|
|
223
|
+
stdout: (result.stdout ?? '').trim(),
|
|
224
|
+
stderr: (result.stderr ?? '').trim(),
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Check A: Agent Files
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
function checkAgentFiles(args) {
|
|
233
|
+
section('A. Agent Files')
|
|
234
|
+
|
|
235
|
+
const canonical = getCanonicalAgentNames()
|
|
236
|
+
if (canonical.length === 0) {
|
|
237
|
+
error('No canonical agent files found in agents/')
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
pass(`${canonical.length} canonical agents found in agents/`)
|
|
241
|
+
|
|
242
|
+
// Determine which platforms to check
|
|
243
|
+
const platformDirs = readdirSync(PLATFORM_DIR, { withFileTypes: true })
|
|
244
|
+
.filter((d) => d.isDirectory() && d.name !== '_template' && d.name !== 'plans')
|
|
245
|
+
.map((d) => d.name)
|
|
246
|
+
.filter((name) => !args.platform || name === args.platform)
|
|
247
|
+
|
|
248
|
+
if (platformDirs.length === 0) {
|
|
249
|
+
warn(`No platform directories found in platform/`)
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
for (const platform of platformDirs) {
|
|
254
|
+
const adapterPath = join(PLATFORM_DIR, platform, 'adapter.json')
|
|
255
|
+
if (!existsSync(adapterPath)) {
|
|
256
|
+
info(`${platform}: no adapter.json — skipping`)
|
|
257
|
+
continue
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const adapter = readJson(adapterPath)
|
|
261
|
+
if (!adapter) {
|
|
262
|
+
warn(`${platform}: adapter.json parse error — skipping`)
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const label = PLATFORM_LABELS[platform] ?? platform
|
|
267
|
+
const outputDir = adapter.outputDir ?? 'agents'
|
|
268
|
+
const ext = adapter.fileExtension ?? '.md'
|
|
269
|
+
const agentDir = join(PLATFORM_DIR, platform, outputDir)
|
|
270
|
+
|
|
271
|
+
if (!existsSync(agentDir)) {
|
|
272
|
+
if (adapter.skipAgentSync) {
|
|
273
|
+
info(`${label}: agent sync skipped (skipAgentSync) — n/a`)
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
276
|
+
warn(`${label}: output directory missing: ${agentDir}`)
|
|
277
|
+
continue
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (adapter.skipAgentSync) {
|
|
281
|
+
info(`${label}: agent sync skipped (skipAgentSync) — skipping agent check`)
|
|
282
|
+
continue
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const platformFiles = readdirSync(agentDir)
|
|
286
|
+
.filter((f) => statSync(join(agentDir, f)).isFile() && f.endsWith(ext))
|
|
287
|
+
.map((f) => f.replace(new RegExp(`${ext}$`), ''))
|
|
288
|
+
|
|
289
|
+
// Check every canonical agent exists in platform output
|
|
290
|
+
let allPresent = true
|
|
291
|
+
const missing = []
|
|
292
|
+
for (const agent of canonical) {
|
|
293
|
+
if (!platformFiles.includes(agent)) {
|
|
294
|
+
missing.push(agent)
|
|
295
|
+
allPresent = false
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Detect phantom agents (in platform but not canonical — stale)
|
|
300
|
+
const phantom = platformFiles.filter((f) => !canonical.includes(f))
|
|
301
|
+
|
|
302
|
+
if (allPresent && phantom.length === 0) {
|
|
303
|
+
pass(`${label}: all ${canonical.length} agents present`)
|
|
304
|
+
} else {
|
|
305
|
+
if (missing.length > 0) {
|
|
306
|
+
error(`${label}: ${missing.length} canonical agent(s) missing: ${missing.join(', ')}`)
|
|
307
|
+
}
|
|
308
|
+
if (phantom.length > 0) {
|
|
309
|
+
warn(`${label}: ${phantom.length} stale agent(s) in output: ${phantom.join(', ')}`)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
// Check B: MCP Configuration
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
function checkMcpConfig(args) {
|
|
320
|
+
section('B. MCP Configuration')
|
|
321
|
+
|
|
322
|
+
// Determine which opencode.json to check:
|
|
323
|
+
// 1. Target project's opencode.json
|
|
324
|
+
// 2. Pantheon ROOT opencode.json
|
|
325
|
+
const candidates = [
|
|
326
|
+
{ label: 'project root', path: join(args.target, 'opencode.json') },
|
|
327
|
+
{ label: 'repository root', path: join(ROOT, 'opencode.json') },
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
// Also check user config if target matches Pantheon ROOT
|
|
331
|
+
if (args.target === ROOT) {
|
|
332
|
+
candidates.push({
|
|
333
|
+
label: 'user config',
|
|
334
|
+
path: join(ROOT, '.opencode', 'opencode.json'),
|
|
335
|
+
})
|
|
336
|
+
} else {
|
|
337
|
+
const homeConfig = join(process.env.HOME ?? '~', '.config', 'opencode', 'opencode.json')
|
|
338
|
+
candidates.push({ label: 'user config', path: homeConfig })
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** @type {{label:string, data:object}[]} */
|
|
342
|
+
const configs = []
|
|
343
|
+
for (const c of candidates) {
|
|
344
|
+
const data = readJson(c.path)
|
|
345
|
+
if (data) {
|
|
346
|
+
configs.push({ label: c.label, data, path: c.path })
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (configs.length === 0) {
|
|
351
|
+
warn('No opencode.json found in target or user config')
|
|
352
|
+
return
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
for (const cfg of configs) {
|
|
356
|
+
info(`Checking ${cfg.label} opencode.json`)
|
|
357
|
+
|
|
358
|
+
const mcp = cfg.data.mcp
|
|
359
|
+
if (!mcp || typeof mcp !== 'object') {
|
|
360
|
+
warn(`${cfg.label}: no "mcp" section`)
|
|
361
|
+
continue
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// 1. gh_grep MCP check
|
|
365
|
+
const ghGrep = mcp.gh_grep
|
|
366
|
+
if (ghGrep) {
|
|
367
|
+
if (ghGrep.type === 'remote' && ghGrep.url === 'https://mcp.grep.app') {
|
|
368
|
+
pass(`${cfg.label}: gh_grep MCP configured correctly`)
|
|
369
|
+
} else {
|
|
370
|
+
const gotUrl = ghGrep.url ?? '<missing>'
|
|
371
|
+
const detail =
|
|
372
|
+
ghGrep.type !== 'remote'
|
|
373
|
+
? `type is "${ghGrep.type}" instead of "remote"`
|
|
374
|
+
: `url is "${gotUrl}"`
|
|
375
|
+
warn(`${cfg.label}: gh_grep MCP may need review — ${detail}`)
|
|
376
|
+
}
|
|
377
|
+
} else {
|
|
378
|
+
warn(`${cfg.label}: gh_grep MCP not configured`)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 2. Context7 MCP check
|
|
382
|
+
const context7 = mcp.context7
|
|
383
|
+
if (context7) {
|
|
384
|
+
const isLocal = context7.type === 'local'
|
|
385
|
+
const hasCommand =
|
|
386
|
+
Array.isArray(context7.command) &&
|
|
387
|
+
context7.command.length > 0 &&
|
|
388
|
+
context7.command.some((s) => s.includes('context7-mcp') || s.includes('context7'))
|
|
389
|
+
const enabled = context7.enabled !== false
|
|
390
|
+
|
|
391
|
+
if (isLocal && hasCommand && enabled) {
|
|
392
|
+
pass(`${cfg.label}: Context7 MCP configured correctly`)
|
|
393
|
+
} else {
|
|
394
|
+
const details = []
|
|
395
|
+
if (!isLocal) details.push(`type should be "local"`)
|
|
396
|
+
if (!hasCommand) details.push("command missing or doesn't mention context7-mcp")
|
|
397
|
+
if (!enabled) details.push('not enabled')
|
|
398
|
+
warn(`${cfg.label}: Context7 MCP issues — ${details.join(', ')}`)
|
|
399
|
+
}
|
|
400
|
+
} else {
|
|
401
|
+
warn(`${cfg.label}: Context7 MCP not configured`)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// 3. Check for deprecated "tools" field format
|
|
405
|
+
// In opencode.json, tools was deprecated in favor of "permission" field
|
|
406
|
+
if (cfg.data.tools && !cfg.data.permission) {
|
|
407
|
+
warn(`${cfg.label}: uses deprecated top-level "tools" field — migrate to "permission"`)
|
|
408
|
+
} else if (cfg.data.tools && cfg.data.permission) {
|
|
409
|
+
info(`${cfg.label}: has both "tools" and "permission" — verify "tools" is intentional`)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// 4. Check MCP servers list — verify tool references
|
|
413
|
+
// Look at agent files for mcpServers and cross-reference with opencode.json MCP config
|
|
414
|
+
if (args.verbose) {
|
|
415
|
+
checkAgentMcpReferences(cfg)
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Cross-reference agent mcpServers references with opencode.json MCP config entries.
|
|
422
|
+
*/
|
|
423
|
+
function checkAgentMcpReferences(cfg) {
|
|
424
|
+
const agentFiles = readdirSync(AGENTS_DIR).filter((f) => f.endsWith('.agent.md'))
|
|
425
|
+
const mcpNames = new Set(Object.keys(cfg.data.mcp ?? {}))
|
|
426
|
+
const referencedNames = new Set()
|
|
427
|
+
|
|
428
|
+
for (const file of agentFiles) {
|
|
429
|
+
const content = readFileSync(join(AGENTS_DIR, file), 'utf8')
|
|
430
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/)
|
|
431
|
+
if (!match) continue
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
// Minimal YAML parse for mcpServers field
|
|
435
|
+
const fmRaw = match[1]
|
|
436
|
+
const mcpMatch = fmRaw.match(/mcpServers:\s*\n([\s\S]*?)(?=\n\w|\n---|$)/)
|
|
437
|
+
if (!mcpMatch) continue
|
|
438
|
+
|
|
439
|
+
const listRaw = mcpMatch[1]
|
|
440
|
+
const nameMatches = listRaw.matchAll(/^\s+-\s+name:\s*['"]?([\w-]+)['"]?\s*$/gm)
|
|
441
|
+
for (const nm of nameMatches) {
|
|
442
|
+
referencedNames.add(nm[1])
|
|
443
|
+
}
|
|
444
|
+
} catch {
|
|
445
|
+
// skip unparseable
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
for (const ref of referencedNames) {
|
|
450
|
+
if (!mcpNames.has(ref)) {
|
|
451
|
+
warn(`${cfg.label}: agent references MCP "${ref}" but it's not in opencode.json mcp config`)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ---------------------------------------------------------------------------
|
|
457
|
+
// Check C: Permission Mismatches
|
|
458
|
+
// ---------------------------------------------------------------------------
|
|
459
|
+
|
|
460
|
+
function checkPermissionMismatches(args) {
|
|
461
|
+
section('C. Permission Mismatches')
|
|
462
|
+
|
|
463
|
+
// Locate the validation script
|
|
464
|
+
const validator = join(ROOT, 'scripts', 'validate-agent-frontmatter.py')
|
|
465
|
+
if (!existsSync(validator)) {
|
|
466
|
+
warn('validate-agent-frontmatter.py not found — cannot check permissions')
|
|
467
|
+
return
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Determine which agent directories to check
|
|
471
|
+
// Validation script checks ~/.config/opencode/agents and platform/opencode/agents
|
|
472
|
+
// Let's run it regardless
|
|
473
|
+
info('Running validate-agent-frontmatter.py...')
|
|
474
|
+
|
|
475
|
+
const result = spawn('python3', [validator])
|
|
476
|
+
|
|
477
|
+
if (result.status === 0) {
|
|
478
|
+
pass('Frontmatter validation passed')
|
|
479
|
+
if (result.stdout && args.verbose) {
|
|
480
|
+
console.log(` ${result.stdout}`)
|
|
481
|
+
}
|
|
482
|
+
} else {
|
|
483
|
+
// Parse output for errors and warnings
|
|
484
|
+
const lines = (result.stderr || '').split('\n').filter(Boolean)
|
|
485
|
+
const errLines = lines.filter((l) => l.startsWith('\u274C')) // ❌
|
|
486
|
+
const warnLines = lines.filter((l) => l.includes('\u26A0')) // ⚠️
|
|
487
|
+
|
|
488
|
+
if (errLines.length > 0) {
|
|
489
|
+
error(`${errLines.length} frontmatter error(s) found`)
|
|
490
|
+
if (args.verbose) {
|
|
491
|
+
for (const line of errLines) {
|
|
492
|
+
console.log(` ${line}`)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (warnLines.length > 0) {
|
|
498
|
+
warn(`${warnLines.length} frontmatter warning(s) found`)
|
|
499
|
+
if (args.verbose) {
|
|
500
|
+
for (const line of warnLines) {
|
|
501
|
+
console.log(` ${line}`)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// If there were only warnings but no errors, the script exits 0
|
|
507
|
+
// If it exited non-zero, there are real errors
|
|
508
|
+
if (errLines.length === 0 && result.status !== 0) {
|
|
509
|
+
error('Frontmatter validation script failed')
|
|
510
|
+
if (result.stderr && args.verbose) {
|
|
511
|
+
console.log(` ${result.stderr}`)
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Also do a quick inline check for agents with bash: deny missing the ⛔ section
|
|
517
|
+
info('Checking permission.body consistency...')
|
|
518
|
+
const canonical = getCanonicalAgentNames()
|
|
519
|
+
let bodyIssues = 0
|
|
520
|
+
|
|
521
|
+
for (const agent of canonical) {
|
|
522
|
+
const filePath = join(AGENTS_DIR, `${agent}.agent.md`)
|
|
523
|
+
if (!existsSync(filePath)) continue
|
|
524
|
+
|
|
525
|
+
const content = readFileSync(filePath, 'utf8')
|
|
526
|
+
|
|
527
|
+
// Quick frontmatter parse
|
|
528
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/)
|
|
529
|
+
if (!fmMatch) continue
|
|
530
|
+
|
|
531
|
+
const rawFm = fmMatch[1]
|
|
532
|
+
const body = fmMatch[2]
|
|
533
|
+
|
|
534
|
+
// Check bash: deny → must have ⛔ TOOLS NOT AVAILABLE
|
|
535
|
+
const bashDeny = /permission:[\s\S]*?bash:\s*deny/.test(rawFm)
|
|
536
|
+
if (bashDeny && !body.includes('\u26D4 TOOLS NOT AVAILABLE'.normalize())) {
|
|
537
|
+
warn(`${agent}: permission.bash is deny but body missing "⛔ TOOLS NOT AVAILABLE"`)
|
|
538
|
+
bodyIssues++
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Check edit: deny → should not reference "edit" in body
|
|
542
|
+
const editDeny = /permission:[\s\S]*?edit:\s*deny/.test(rawFm)
|
|
543
|
+
if (editDeny) {
|
|
544
|
+
const editCount = (body.match(/\bedit\b/gi) || []).length
|
|
545
|
+
if (editCount > 0) {
|
|
546
|
+
warn(`${agent}: permission.edit is deny but "edit" appears ${editCount} time(s) in body`)
|
|
547
|
+
bodyIssues++
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (bodyIssues === 0) {
|
|
553
|
+
pass('All permission.body consistency checks passed')
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// ---------------------------------------------------------------------------
|
|
558
|
+
// Check D: Sync Status
|
|
559
|
+
// ---------------------------------------------------------------------------
|
|
560
|
+
|
|
561
|
+
function checkSyncStatus(args) {
|
|
562
|
+
section('D. Sync Status')
|
|
563
|
+
|
|
564
|
+
const syncScript = join(ROOT, 'scripts', 'sync-platforms.mjs')
|
|
565
|
+
if (!existsSync(syncScript)) {
|
|
566
|
+
warn('sync-platforms.mjs not found')
|
|
567
|
+
return
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Use the validate-sync approach: dry-run with clean
|
|
571
|
+
const result = spawn(process.execPath, [syncScript, '--dry-run', '--clean'])
|
|
572
|
+
// sync-platforms exits 1 when there are changes in dry-run mode
|
|
573
|
+
|
|
574
|
+
if (result.status === 0) {
|
|
575
|
+
pass('All platforms are in sync with canonical agents')
|
|
576
|
+
} else if (result.status === 1) {
|
|
577
|
+
// Parse output to determine what would change
|
|
578
|
+
const lines = (result.stdout || '').split('\n').filter(Boolean)
|
|
579
|
+
const updateLines = lines.filter((l) => l.includes('would update') || l.includes('~'))
|
|
580
|
+
const staleLines = lines.filter((l) => l.includes('stale'))
|
|
581
|
+
|
|
582
|
+
const updateCount = updateLines.length
|
|
583
|
+
const staleCount = staleLines.length
|
|
584
|
+
|
|
585
|
+
warn(
|
|
586
|
+
`Platforms are out of sync (${updateCount} file(s) would change${staleCount > 0 ? `, ${staleCount} stale` : ''})`,
|
|
587
|
+
)
|
|
588
|
+
if (args.verbose || updateCount + staleCount < 20) {
|
|
589
|
+
for (const line of updateLines) {
|
|
590
|
+
console.log(` ${line}`)
|
|
591
|
+
}
|
|
592
|
+
for (const line of staleLines) {
|
|
593
|
+
console.log(` ${line}`)
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (args.fix) {
|
|
598
|
+
info('Attempting --fix: running npm run sync...')
|
|
599
|
+
const syncResult = spawn('npm', ['run', 'sync'])
|
|
600
|
+
if (syncResult.status === 0) {
|
|
601
|
+
pass('Sync completed successfully')
|
|
602
|
+
} else {
|
|
603
|
+
error(`Sync failed (exit ${syncResult.status})`)
|
|
604
|
+
if (syncResult.stderr) {
|
|
605
|
+
console.log(` ${syncResult.stderr}`)
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
} else {
|
|
609
|
+
info('Use --fix to run "npm run sync" automatically')
|
|
610
|
+
}
|
|
611
|
+
} else {
|
|
612
|
+
error(`Sync dry-run failed (exit ${result.status})`)
|
|
613
|
+
if (result.stderr) {
|
|
614
|
+
console.log(` ${result.stderr}`)
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// ---------------------------------------------------------------------------
|
|
620
|
+
// Check E: Git Status
|
|
621
|
+
// ---------------------------------------------------------------------------
|
|
622
|
+
|
|
623
|
+
function checkGitStatus(args) {
|
|
624
|
+
section('E. Git Status')
|
|
625
|
+
|
|
626
|
+
// Check if the target is a git repo
|
|
627
|
+
const gitRoot = spawn('git', ['rev-parse', '--show-toplevel'])
|
|
628
|
+
if (gitRoot.status !== 0) {
|
|
629
|
+
info('Not a git repository — skipping')
|
|
630
|
+
return
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Check for uncommitted changes in agents/ and platform/
|
|
634
|
+
const gitStatus = spawn('git', ['status', '--porcelain', '--', 'agents/', 'platform/'])
|
|
635
|
+
|
|
636
|
+
if (gitStatus.status !== 0) {
|
|
637
|
+
warn('Could not check git status')
|
|
638
|
+
return
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const lines = gitStatus.stdout ? gitStatus.stdout.split('\n').filter(Boolean) : []
|
|
642
|
+
|
|
643
|
+
if (lines.length === 0) {
|
|
644
|
+
pass('No uncommitted changes in agents/ or platform/')
|
|
645
|
+
return
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Categorize by type
|
|
649
|
+
const modified = []
|
|
650
|
+
const added = []
|
|
651
|
+
const deleted = []
|
|
652
|
+
const untracked = []
|
|
653
|
+
|
|
654
|
+
for (const line of lines) {
|
|
655
|
+
const statusCode = line.substring(0, 2).trim()
|
|
656
|
+
const filePath = line.substring(3)
|
|
657
|
+
if (statusCode === 'M' || statusCode === 'MM') modified.push(filePath)
|
|
658
|
+
else if (statusCode === 'A') added.push(filePath)
|
|
659
|
+
else if (statusCode === 'D') deleted.push(filePath)
|
|
660
|
+
else if (statusCode === '?' || statusCode === '??') untracked.push(filePath)
|
|
661
|
+
else modified.push(filePath)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const total = lines.length
|
|
665
|
+
const details = []
|
|
666
|
+
if (modified.length > 0) details.push(`${modified.length} modified`)
|
|
667
|
+
if (added.length > 0) details.push(`${added.length} staged`)
|
|
668
|
+
if (deleted.length > 0) details.push(`${deleted.length} deleted`)
|
|
669
|
+
if (untracked.length > 0) details.push(`${untracked.length} untracked`)
|
|
670
|
+
|
|
671
|
+
warn(`${total} uncommitted change(s) in agents/ or platform/ (${details.join(', ')})`)
|
|
672
|
+
if (args.verbose) {
|
|
673
|
+
for (const f of modified.slice(0, 10)) {
|
|
674
|
+
console.log(` M ${f}`)
|
|
675
|
+
}
|
|
676
|
+
for (const f of added.slice(0, 5)) {
|
|
677
|
+
console.log(` A ${f}`)
|
|
678
|
+
}
|
|
679
|
+
for (const f of deleted.slice(0, 5)) {
|
|
680
|
+
console.log(` D ${f}`)
|
|
681
|
+
}
|
|
682
|
+
for (const f of untracked.slice(0, 5)) {
|
|
683
|
+
console.log(` ?? ${f}`)
|
|
684
|
+
}
|
|
685
|
+
if (lines.length > 20) {
|
|
686
|
+
console.log(` ... and ${lines.length - 20} more`)
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// ---------------------------------------------------------------------------
|
|
692
|
+
// Summary
|
|
693
|
+
// ---------------------------------------------------------------------------
|
|
694
|
+
|
|
695
|
+
function printSummary(targetPath) {
|
|
696
|
+
console.log(`\n${'='.repeat(60)}`)
|
|
697
|
+
console.log(' Summary')
|
|
698
|
+
console.log(`${'='.repeat(60)}`)
|
|
699
|
+
console.log(` ${ICON.pass} ${counts.pass} passed`)
|
|
700
|
+
console.log(` ${ICON.warn} ${counts.warn} warnings`)
|
|
701
|
+
console.log(` ${ICON.error} ${counts.error} errors`)
|
|
702
|
+
console.log(` ${ICON.info} ${counts.info} info`)
|
|
703
|
+
console.log('')
|
|
704
|
+
|
|
705
|
+
if (exitCode === 0) {
|
|
706
|
+
console.log(' ✅ All checks passed!')
|
|
707
|
+
} else if (exitCode === 1) {
|
|
708
|
+
console.log(' ⚠️ Warnings found — review above (some may be auto-fixable with --fix)')
|
|
709
|
+
} else {
|
|
710
|
+
console.log(' ❌ Errors found — needs attention')
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
console.log(`\n Target: ${process.argv.includes('--target') ? targetPath : 'current directory'}`)
|
|
714
|
+
console.log('')
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// ---------------------------------------------------------------------------
|
|
718
|
+
// Main
|
|
719
|
+
// ---------------------------------------------------------------------------
|
|
720
|
+
|
|
721
|
+
function main() {
|
|
722
|
+
const args = parseArgs(process.argv)
|
|
723
|
+
|
|
724
|
+
if (args.help) {
|
|
725
|
+
showHelp()
|
|
726
|
+
process.exit(0)
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (!existsSync(args.target)) {
|
|
730
|
+
console.error(`❌ Target directory does not exist: ${args.target}`)
|
|
731
|
+
process.exit(2)
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Verify it looks like a Pantheon project (has agents/ and platform/)
|
|
735
|
+
const agentsOk = existsSync(join(args.target, 'agents'))
|
|
736
|
+
const platformOk = existsSync(join(args.target, 'platform'))
|
|
737
|
+
|
|
738
|
+
if (!agentsOk && !platformOk) {
|
|
739
|
+
// Maybe they pointed at a project root that isn't Pantheon
|
|
740
|
+
// Check if this is a Pantheon install target (has opencode.json with mcp)
|
|
741
|
+
const opencodeJson = readJson(join(args.target, 'opencode.json'))
|
|
742
|
+
if (!opencodeJson) {
|
|
743
|
+
warn(`Target does not appear to be a Pantheon project (no agents/ or platform/ directory)`)
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const _isPantheonRoot =
|
|
748
|
+
join(args.target, 'scripts', 'doctor.mjs') === join(ROOT, 'scripts', 'doctor.mjs') ||
|
|
749
|
+
existsSync(join(args.target, 'scripts', 'doctor.mjs')) === false
|
|
750
|
+
|
|
751
|
+
// Run checks
|
|
752
|
+
checkAgentFiles(args)
|
|
753
|
+
checkMcpConfig(args)
|
|
754
|
+
checkPermissionMismatches(args)
|
|
755
|
+
checkSyncStatus(args)
|
|
756
|
+
checkGitStatus(args)
|
|
757
|
+
|
|
758
|
+
printSummary(args.target)
|
|
759
|
+
|
|
760
|
+
process.exit(exitCode)
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
main()
|