pantheon-opencode 1.3.3 → 1.3.5-beta.52.c75b04d
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 +157 -15
- package/bin/pantheon-init.mjs +177 -120
- package/opencode.json +3 -6
- package/package.json +8 -4
- package/scripts/build-agents-md.mjs +195 -0
- package/scripts/cost.mjs +17 -0
- package/scripts/doctor.mjs +56 -22
- package/scripts/hooks/on-subagent-delegation-stop.sh +22 -5
- package/scripts/install/opencode.mjs +189 -38
- package/scripts/install/plugin.mjs +112 -28
- package/scripts/install-mcp.mjs +1 -1
- package/scripts/redaction-gate.mjs +69 -0
- package/scripts/release-beta-version.mjs +80 -0
- package/scripts/release-notes.mjs +317 -0
- package/scripts/test-context-compression.sh +5 -2
- package/scripts/validate-package.mjs +56 -0
- package/scripts/versioning.mjs +117 -18
- package/src/agents/zeus.md +6 -2
- package/src/pantheon/background-job-board.ts +24 -38
- package/src/pantheon/chat-reminders.ts +79 -0
- package/src/pantheon/compaction-assert.ts +160 -0
- package/src/pantheon/cost-command.ts +57 -18
- package/src/pantheon/delegation-compaction.ts +143 -16
- package/src/pantheon/delegation-finalize.ts +8 -1
- package/src/pantheon/delegation-notify.ts +19 -157
- package/src/pantheon/delegation.ts +330 -28
- package/src/pantheon/dispatch-guard.ts +9 -4
- package/src/pantheon/goal-loop.ts +7 -4
- package/src/pantheon/hashline/tool.ts +2 -1
- package/src/pantheon/logger.ts +117 -0
- package/src/pantheon/plugin-once.ts +76 -0
- package/src/pantheon/preemptive-compact.ts +76 -0
- package/src/pantheon/presets.d.mts +31 -0
- package/src/pantheon/presets.mjs +105 -3
- package/src/pantheon/todo-enforcer.ts +79 -27
- package/src/pantheon/todo-preserve.ts +295 -0
- package/src/plugin.ts +177 -50
- package/src/plugins/pantheon-hooks.ts +35 -22
- package/src/plugins/tui/dist/server.d.ts +17 -0
- package/src/plugins/tui/dist/server.d.ts.map +1 -0
- package/src/plugins/tui/dist/server.js +20 -0
- package/src/plugins/tui/dist/server.js.map +1 -0
- package/src/plugins/tui/dist/tui.d.ts +303 -1
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +977 -350
- package/src/plugins/tui/dist/tui.tsx +1427 -150
- package/src/plugins/tui/package.json +4 -3
- package/src/plugins/tui/src/index.tsx +1427 -150
- package/src/plugins/tui/src/server.ts +15 -0
- package/src/plugins/tui/tsdown.config.ts +1 -1
- package/src/routing.yml +3 -1
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.52.c75b04d",
|
|
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",
|
|
@@ -21,7 +22,7 @@
|
|
|
21
22
|
"version:status": "node scripts/versioning.mjs status",
|
|
22
23
|
"version:apply": "node scripts/versioning.mjs apply",
|
|
23
24
|
"version:check": "node scripts/version-check.mjs",
|
|
24
|
-
"release:dry-run": "node scripts/versioning.mjs status && npm pack --dry-run",
|
|
25
|
+
"release:dry-run": "node scripts/versioning.mjs status && node scripts/release-notes.mjs && npm pack --dry-run",
|
|
25
26
|
"publish:next": "npm publish --tag next --access public",
|
|
26
27
|
"lint": "npx biome ci .",
|
|
27
28
|
"typecheck": "npx tsc --noEmit",
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
"hooks:install": "pre-commit install",
|
|
31
32
|
"hooks:update": "pre-commit autoupdate",
|
|
32
33
|
"test:security": "npm run secret-scan && node tests/test_install_mcp_vision.mjs && node tests/test_doctor_layers.mjs",
|
|
33
|
-
"test:node": "node --test tests/*.mjs",
|
|
34
|
+
"test:node": "node --test tests/*.mjs tests/pantheon/*.mjs",
|
|
34
35
|
"test:ts": "for f in tests/pantheon/*.test.ts; do npx tsx \"$f\" || exit 1; done",
|
|
35
36
|
"test": "python3 -m pytest tests/ -q 2>&1 || echo \"No tests found\"",
|
|
36
37
|
"test:ci": "python3 -m pytest tests/ -q --tb=short 2>&1 || true"
|
|
@@ -64,7 +65,10 @@
|
|
|
64
65
|
"scripts/hooks/**",
|
|
65
66
|
"src/mcp/requirements*",
|
|
66
67
|
"src/plugins/tui/**",
|
|
67
|
-
"src/
|
|
68
|
+
"!src/plugins/tui/node_modules/**",
|
|
69
|
+
"src/skills/**",
|
|
70
|
+
"!**/__pycache__/**",
|
|
71
|
+
"!**/*.pyc"
|
|
68
72
|
],
|
|
69
73
|
"dependencies": {
|
|
70
74
|
"js-yaml": "^4.3.0",
|
|
@@ -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
|
@@ -34,7 +34,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
34
34
|
|
|
35
35
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
36
36
|
const ROOT = join(__dirname, '..')
|
|
37
|
-
const AGENTS_DIR = join(ROOT,
|
|
37
|
+
const AGENTS_DIR = join(ROOT, 'src', 'agents')
|
|
38
38
|
const PLATFORM_DIR = join(ROOT, 'platform')
|
|
39
39
|
|
|
40
40
|
// All known platforms (must match platform/ subdirectories with adapter.json)
|
|
@@ -246,7 +246,9 @@ function checkAgentFiles(args) {
|
|
|
246
246
|
|
|
247
247
|
// Determine which platforms to check
|
|
248
248
|
if (!existsSync(PLATFORM_DIR)) {
|
|
249
|
-
info(
|
|
249
|
+
info(
|
|
250
|
+
`Platform component not distributed in this package — platform check skipped (${args.profile} profile)`,
|
|
251
|
+
)
|
|
250
252
|
return
|
|
251
253
|
}
|
|
252
254
|
const platformDirs = readdirSync(PLATFORM_DIR, { withFileTypes: true })
|
|
@@ -255,7 +257,9 @@ function checkAgentFiles(args) {
|
|
|
255
257
|
.filter((name) => !args.platform || name === args.platform)
|
|
256
258
|
|
|
257
259
|
if (platformDirs.length === 0) {
|
|
258
|
-
info(
|
|
260
|
+
info(
|
|
261
|
+
`No platform directories found in platform/ — platform check skipped (${args.profile} profile)`,
|
|
262
|
+
)
|
|
259
263
|
return
|
|
260
264
|
}
|
|
261
265
|
|
|
@@ -377,7 +381,8 @@ function checkMcpConfig(args) {
|
|
|
377
381
|
|
|
378
382
|
const mcp = cfg.data.mcp
|
|
379
383
|
if (!mcp || typeof mcp !== 'object') {
|
|
380
|
-
if (requiredMcpNames.length > 0)
|
|
384
|
+
if (requiredMcpNames.length > 0)
|
|
385
|
+
error(`${cfg.label}: required MCPs missing — no "mcp" section`)
|
|
381
386
|
else info(`${cfg.label}: no "mcp" section (${args.profile} profile — optional)`)
|
|
382
387
|
continue
|
|
383
388
|
}
|
|
@@ -450,7 +455,9 @@ function checkMcpConfig(args) {
|
|
|
450
455
|
const exe = cmd[0]
|
|
451
456
|
if (!exe.includes('/')) {
|
|
452
457
|
// PATH-resolved executable — cannot verify statically; surface for manual check
|
|
453
|
-
info(
|
|
458
|
+
info(
|
|
459
|
+
`${cfg.label}: MCP "${name}" uses PATH executable "${exe}" (verify with \`which ${exe}\`)`,
|
|
460
|
+
)
|
|
454
461
|
continue
|
|
455
462
|
}
|
|
456
463
|
if (!existsSync(exe)) {
|
|
@@ -467,7 +474,8 @@ function checkMcpConfig(args) {
|
|
|
467
474
|
if (requiredMcpNames.length > 0) {
|
|
468
475
|
const configured = new Set(configs.flatMap((cfg) => Object.keys(cfg.data.mcp ?? {})))
|
|
469
476
|
for (const required of requiredMcpNames) {
|
|
470
|
-
if (configured.has(required))
|
|
477
|
+
if (configured.has(required))
|
|
478
|
+
pass(`Required MCP "${required}" configured in an available config`)
|
|
471
479
|
else error(`Required MCP "${required}" not configured in any available config`)
|
|
472
480
|
}
|
|
473
481
|
}
|
|
@@ -535,12 +543,17 @@ function mcpInitializeSmoke(command, cwd, timeoutMs) {
|
|
|
535
543
|
const msg = JSON.parse(line.trim())
|
|
536
544
|
if (msg.id === 1 && msg.result) {
|
|
537
545
|
const info = msg.result.serverInfo
|
|
538
|
-
const label = info?.name
|
|
546
|
+
const label = info?.name
|
|
547
|
+
? `${info.name}${info.version ? ` ${info.version}` : ''}`
|
|
548
|
+
: undefined
|
|
539
549
|
finish({ ok: true, serverName: label })
|
|
540
550
|
return
|
|
541
551
|
}
|
|
542
552
|
if (msg.error) {
|
|
543
|
-
finish({
|
|
553
|
+
finish({
|
|
554
|
+
ok: false,
|
|
555
|
+
reason: `initialize error: ${msg.error.code ?? ''} ${msg.error.message ?? ''}`,
|
|
556
|
+
})
|
|
544
557
|
}
|
|
545
558
|
} catch {
|
|
546
559
|
/* not JSON yet — keep buffering */
|
|
@@ -592,7 +605,9 @@ async function checkMcpRuntimeSmoke(args) {
|
|
|
592
605
|
for (const { name, command, cwd, source } of local) {
|
|
593
606
|
const result = await mcpInitializeSmoke(command, cwd, 10000)
|
|
594
607
|
if (result.ok) {
|
|
595
|
-
pass(
|
|
608
|
+
pass(
|
|
609
|
+
`MCP "${name}" (${source}): initialize handshake OK${result.serverName ? ` — ${result.serverName}` : ''}`,
|
|
610
|
+
)
|
|
596
611
|
} else {
|
|
597
612
|
error(`MCP "${name}" (${source}): ${result.reason}`)
|
|
598
613
|
}
|
|
@@ -645,7 +660,9 @@ function checkPermissionMismatches(args) {
|
|
|
645
660
|
// Locate the validation script
|
|
646
661
|
const validator = join(ROOT, 'scripts', 'validate-agent-frontmatter.py')
|
|
647
662
|
if (!existsSync(validator)) {
|
|
648
|
-
info(
|
|
663
|
+
info(
|
|
664
|
+
`validate-agent-frontmatter.py not found — optional helper skipped (${args.profile} profile)`,
|
|
665
|
+
)
|
|
649
666
|
return
|
|
650
667
|
}
|
|
651
668
|
|
|
@@ -876,12 +893,24 @@ function checkRequirementLine(line, installed) {
|
|
|
876
893
|
const cmp = compareVersions(have, want)
|
|
877
894
|
let ok = false
|
|
878
895
|
switch (op) {
|
|
879
|
-
case '==':
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
case '
|
|
883
|
-
|
|
884
|
-
|
|
896
|
+
case '==':
|
|
897
|
+
ok = cmp === 0
|
|
898
|
+
break
|
|
899
|
+
case '>=':
|
|
900
|
+
ok = cmp >= 0
|
|
901
|
+
break
|
|
902
|
+
case '<=':
|
|
903
|
+
ok = cmp <= 0
|
|
904
|
+
break
|
|
905
|
+
case '>':
|
|
906
|
+
ok = cmp > 0
|
|
907
|
+
break
|
|
908
|
+
case '<':
|
|
909
|
+
ok = cmp < 0
|
|
910
|
+
break
|
|
911
|
+
case '!=':
|
|
912
|
+
ok = cmp !== 0
|
|
913
|
+
break
|
|
885
914
|
case '~=': {
|
|
886
915
|
const spec = parseVersion(want)
|
|
887
916
|
const got = parseVersion(have)
|
|
@@ -961,12 +990,12 @@ function checkVenvLayer(args) {
|
|
|
961
990
|
*/
|
|
962
991
|
export function summaryMessage(summaryCounts, summaryExitCode) {
|
|
963
992
|
if (summaryCounts.error > 0 || summaryExitCode !== 0) {
|
|
964
|
-
const errorText =
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
: ''
|
|
993
|
+
const errorText =
|
|
994
|
+
summaryCounts.error > 0
|
|
995
|
+
? `${summaryCounts.error} blocking error(s) found`
|
|
996
|
+
: 'Blocking check failure'
|
|
997
|
+
const warningText =
|
|
998
|
+
summaryCounts.warn > 0 ? `; ${summaryCounts.warn} warning(s) remain advisory` : ''
|
|
970
999
|
return `❌ ${errorText}${warningText} — exit code ${summaryExitCode}`
|
|
971
1000
|
}
|
|
972
1001
|
|
|
@@ -1005,6 +1034,11 @@ async function main() {
|
|
|
1005
1034
|
process.exit(0)
|
|
1006
1035
|
}
|
|
1007
1036
|
|
|
1037
|
+
// Report the resolved validation profile up front (auto → global/sandbox)
|
|
1038
|
+
// so callers — including the `doctor` CLI wrapper — can see which policy
|
|
1039
|
+
// gate applied before any check output.
|
|
1040
|
+
console.log(`Validation profile: ${args.profile}`)
|
|
1041
|
+
|
|
1008
1042
|
if (!existsSync(args.target)) {
|
|
1009
1043
|
console.error(`❌ Target directory does not exist: ${args.target}`)
|
|
1010
1044
|
process.exit(2)
|
|
@@ -383,12 +383,27 @@ fi
|
|
|
383
383
|
# Emit a schema-compatible line: {event, timestamp, agent, status, reason,
|
|
384
384
|
# session_id, result, delegation_id, task_id, duration_ms} — P0-2: the
|
|
385
385
|
# delegation_id joins this record to the matching SubagentStart; task_id and
|
|
386
|
-
# duration_ms ride in their OWN fields.
|
|
386
|
+
# duration_ms ride in their OWN fields. 1.3.4: task_id is OMITTED when empty
|
|
387
|
+
# (never "" — the real child id is always present when a job exists) and
|
|
388
|
+
# duration_ms is emitted as a NUMBER (the plugin computes it; stringifying it
|
|
389
|
+
# broke downstream aggregation). Built with json.dumps so arbitrary
|
|
387
390
|
# reason/result text cannot break the JSON.
|
|
388
391
|
LOG_LINE=$(python3 -c '
|
|
389
392
|
import json
|
|
390
393
|
import sys
|
|
391
|
-
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _num(v):
|
|
397
|
+
"""Best-effort numeric parse — the plugin duration_ms is a number; a
|
|
398
|
+
non-numeric value (e.g. unset) becomes null, never a string."""
|
|
399
|
+
try:
|
|
400
|
+
return int(float(v))
|
|
401
|
+
except (TypeError, ValueError):
|
|
402
|
+
return None
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
task_id = sys.argv[8] or ""
|
|
406
|
+
line = {
|
|
392
407
|
"event": "SubagentStop",
|
|
393
408
|
"timestamp": sys.argv[1],
|
|
394
409
|
"agent": sys.argv[2],
|
|
@@ -397,9 +412,11 @@ print(json.dumps({
|
|
|
397
412
|
"session_id": sys.argv[5],
|
|
398
413
|
"result": sys.argv[6],
|
|
399
414
|
"delegation_id": sys.argv[7],
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
415
|
+
}
|
|
416
|
+
if task_id:
|
|
417
|
+
line["task_id"] = task_id
|
|
418
|
+
line["duration_ms"] = _num(sys.argv[9])
|
|
419
|
+
print(json.dumps(line, ensure_ascii=False))
|
|
403
420
|
' "$TIMESTAMP" "$AGENT_NAME" "$STATUS" "$REASON" "$SESSION_ID" "$RESULT_SNIPPET" "$DELEGATION_ID" "$TASK_ID" "$DURATION_MS")
|
|
404
421
|
|
|
405
422
|
LOG_FILE="$LOG_DIR/delegations.log"
|