pantheon-opencode 1.2.0 → 1.2.2

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.
Files changed (63) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +112 -15
  3. package/bin/pantheon-init.mjs +93 -0
  4. package/commands/pantheon-vision.md +67 -0
  5. package/opencode.json +14 -10
  6. package/package.json +20 -4
  7. package/scripts/_pantheon_paths.py +0 -2
  8. package/scripts/changelog-extract.mjs +112 -0
  9. package/scripts/doctor.mjs +307 -15
  10. package/scripts/hooks/audit-imports.sh +43 -0
  11. package/scripts/hooks/format-multi-language.sh +96 -0
  12. package/scripts/hooks/log-session-start.sh +24 -0
  13. package/scripts/hooks/on-subagent-delegation-start.sh +91 -0
  14. package/scripts/hooks/on-subagent-delegation-stop.sh +37 -0
  15. package/scripts/hooks/run-type-check.sh +21 -0
  16. package/scripts/hooks/scan-secrets.sh +55 -0
  17. package/scripts/hooks/validate-post-conditions.sh +21 -0
  18. package/scripts/hooks/validate-talos-scope.sh +43 -0
  19. package/scripts/hooks/validate-tool-safety.sh +46 -0
  20. package/scripts/install/health-check.mjs +101 -1
  21. package/scripts/install/model-picker.mjs +118 -0
  22. package/scripts/install/opencode.mjs +155 -43
  23. package/scripts/install/plugin.mjs +4 -2
  24. package/scripts/install/venv.mjs +25 -2
  25. package/scripts/install-mcp.mjs +170 -37
  26. package/scripts/mcp_persistence_server.py +2 -1
  27. package/scripts/prune.mjs +359 -0
  28. package/scripts/secret-scan.mjs +88 -0
  29. package/scripts/uninstall.mjs +3 -3
  30. package/scripts/validate-routing.mjs +15 -0
  31. package/scripts/version-check.mjs +146 -0
  32. package/scripts/versioning.mjs +13 -14
  33. package/src/agents/hermes.md +2 -2
  34. package/src/agents/mnemosyne.md +1 -1
  35. package/src/agents/zeus.md +1 -1
  36. package/src/mcp/_pantheon_paths.py +0 -2
  37. package/src/mcp/install-mcp.mjs +6 -913
  38. package/src/mcp/mcp_persistence_server.py +2 -1
  39. package/src/mcp/pantheon_vision_server.py +495 -0
  40. package/src/mcp/requirements-mcp.txt +8 -5
  41. package/src/mcp/requirements-vision.txt +3 -0
  42. package/src/pantheon/auto-wake.ts +91 -0
  43. package/src/pantheon/background-job-board.ts +568 -0
  44. package/src/pantheon/file-persistence.ts +95 -0
  45. package/src/pantheon/presets.d.mts +108 -0
  46. package/src/pantheon/presets.mjs +554 -0
  47. package/src/pantheon/vision.ts +1437 -0
  48. package/src/plugin.ts +69 -25
  49. package/src/plugins/hook-runner.ts +186 -0
  50. package/src/plugins/pantheon-hooks.ts +153 -0
  51. package/src/plugins/tui/dist/tui.d.ts +1 -1
  52. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  53. package/src/plugins/tui/dist/tui.js +812 -223
  54. package/src/plugins/tui/dist/tui.js.map +1 -1
  55. package/src/plugins/tui/dist/tui.tsx +881 -121
  56. package/src/plugins/tui/package.json +3 -3
  57. package/src/plugins/tui/src/index.tsx +881 -121
  58. package/src/routing.yml +148 -0
  59. package/src/skills/context-compression/SKILL.md +1 -1
  60. package/src/skills/loop-engineering/SKILL.md +1 -1
  61. package/src/skills/orchestration-workflow/SKILL.md +11 -11
  62. package/scripts/scrub_secrets.py +0 -281
  63. package/src/mcp/requirements-mcp-core.txt +0 -5
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+
3
+ /** Scan versionable files without printing secret values. */
4
+
5
+ import { execFileSync } from 'node:child_process'
6
+ import { readFileSync } from 'node:fs'
7
+ import { resolve } from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
9
+
10
+ const bifrostHeader = ['x', '-bf-', 'vk'].join('')
11
+ const bifrostTokenPrefix = ['sk', '-bf-'].join('')
12
+ const apiKeyName = ['api', 'Key'].join('')
13
+ const authorizationName = ['Author', 'ization'].join('')
14
+
15
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
16
+
17
+ // Files that may reference credential PATTERN NAMES (not values): gitleaks
18
+ // config, security policy, scan tooling and its fixtures. Keep in sync with
19
+ // the [allowlist] paths in .gitleaks.toml.
20
+ export const allowlistedFiles = new Set([
21
+ '.gitleaks.toml',
22
+ '.gitleaksignore',
23
+ 'SECURITY.md',
24
+ 'scripts/secret-scan.mjs',
25
+ 'tests/test_secret_scan.mjs',
26
+ ])
27
+
28
+ const patterns = [
29
+ { label: 'Bifrost credential header', regex: new RegExp(escapeRegExp(bifrostHeader), 'i') },
30
+ {
31
+ label: 'Bifrost credential value',
32
+ regex: new RegExp(`${escapeRegExp(bifrostTokenPrefix)}[A-Za-z0-9_-]{8,}`, 'i'),
33
+ },
34
+ {
35
+ label: 'literal API key value',
36
+ regex: new RegExp(
37
+ `["']${escapeRegExp(apiKeyName)}["']\\s*[:=]\\s*["'](?!\\$\\{|process\\.env|os\\.getenv|env:)[^"']{12,}["']`,
38
+ 'i',
39
+ ),
40
+ },
41
+ {
42
+ label: 'literal Authorization bearer value',
43
+ regex: new RegExp(
44
+ `["']${escapeRegExp(authorizationName)}["']\\s*[:=]\\s*["']Bearer\\s+[A-Za-z0-9._-]{12,}["']`,
45
+ 'i',
46
+ ),
47
+ },
48
+ ]
49
+
50
+ export function scanText(text, file = '<text>') {
51
+ return patterns.filter(({ regex }) => regex.test(text)).map(({ label }) => ({ file, label }))
52
+ }
53
+
54
+ export function versionableFiles() {
55
+ return execFileSync('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard'], {
56
+ encoding: 'buffer',
57
+ })
58
+ .toString()
59
+ .split('\0')
60
+ .filter(Boolean)
61
+ }
62
+
63
+ export function scanVersionableFiles() {
64
+ const findings = []
65
+ for (const file of versionableFiles()) {
66
+ if (allowlistedFiles.has(file)) continue
67
+ let text
68
+ try {
69
+ const content = readFileSync(file)
70
+ if (content.includes(0)) continue
71
+ text = content.toString('utf8')
72
+ } catch {
73
+ continue
74
+ }
75
+ findings.push(...scanText(text, file))
76
+ }
77
+ return findings
78
+ }
79
+
80
+ if (fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
81
+ const findings = scanVersionableFiles()
82
+ if (findings.length > 0) {
83
+ for (const finding of findings) console.error(`[SECRET SCAN] ${finding.file}: ${finding.label}`)
84
+ process.exitCode = 1
85
+ } else {
86
+ console.log('Secret scan passed: no versionable Bifrost/API credentials detected.')
87
+ }
88
+ }
@@ -36,14 +36,14 @@ const PANTHEON_AGENT_NAMES = [
36
36
  'themis',
37
37
  'prometheus',
38
38
  'hephaestus',
39
-
40
-
39
+
40
+
41
41
  'nyx',
42
42
  'gaia',
43
43
  'iris',
44
44
  'mnemosyne',
45
45
  'talos',
46
-
46
+
47
47
  ]
48
48
 
49
49
  const PLATFORM_LABELS = {
@@ -18,6 +18,7 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs'
18
18
  import { dirname, join } from 'node:path'
19
19
  import { fileURLToPath } from 'node:url'
20
20
  import yaml from 'js-yaml'
21
+ import { validatePresetDefs } from '../src/pantheon/presets.mjs'
21
22
 
22
23
  const __dirname = dirname(fileURLToPath(import.meta.url))
23
24
  const ROOT = join(__dirname, '..')
@@ -150,6 +151,20 @@ for (const [name, info] of Object.entries(routing.agents || {})) {
150
151
  }
151
152
  }
152
153
 
154
+ // F. Model preset validation
155
+ const presetDefs = routing.presets || {}
156
+ console.log(`\n Model presets: ${Object.keys(presetDefs).length} defined`)
157
+
158
+ const presetKnownAgents = routingAgents.filter((a) => a !== 'zen' && a !== 'zeus_copilot')
159
+ const presetValidation = validatePresetDefs(presetDefs, { agents: presetKnownAgents })
160
+ for (const err of presetValidation.errors) {
161
+ check(false, `Preset ${err}`)
162
+ }
163
+ for (const w of presetValidation.warnings) {
164
+ warn(`Preset ${w}`)
165
+ }
166
+ console.log(` Presets defined: ${Object.keys(presetDefs).length}`)
167
+
153
168
  // Summary
154
169
  console.log(`\n${'='.repeat(50)}`)
155
170
  const status = failures === 0 ? '✅ PASSED' : `❌ FAILED (${failures}/${checks} checks failed)`
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * version-check.mjs — Single source of truth enforcement
4
+ *
5
+ * package.json is the SOURCE OF TRUTH for the Pantheon version.
6
+ * Every other manifest (pyproject.toml, plugin.json, src/plugins/tui/package.json)
7
+ * MUST carry the same version. This script:
8
+ *
9
+ * - compares every manifest against package.json and prints a divergence
10
+ * table when they differ (exit code 1),
11
+ * - with --fix, rewrites each divergent manifest to the package.json version.
12
+ *
13
+ * Mirrors the MANIFEST_FILES list in scripts/versioning.mjs so `apply`
14
+ * and `check` can never drift apart.
15
+ *
16
+ * Usage:
17
+ * node scripts/version-check.mjs # verify (exit 0 = in sync)
18
+ * node scripts/version-check.mjs --fix # sync all manifests
19
+ */
20
+
21
+ import { readFileSync, writeFileSync } from 'node:fs'
22
+ import { join } from 'node:path'
23
+ import { fileURLToPath, pathToFileURL } from 'node:url'
24
+
25
+ /** Same set of manifests as versioning.mjs (minus package.json itself). */
26
+ export const MANIFESTS = [
27
+ { name: 'pyproject.toml', file: 'pyproject.toml', kind: 'toml' },
28
+ { name: 'plugin.json', file: 'plugin.json', kind: 'json' },
29
+ { name: 'src/plugins/tui/package.json', file: join('src', 'plugins', 'tui', 'package.json'), kind: 'json' },
30
+ ]
31
+
32
+ function readTomlVersion(filePath) {
33
+ const text = readFileSync(filePath, 'utf-8')
34
+ const match = text.match(/^version\s*=\s*"([^"]+)"/m)
35
+ if (!match) throw new Error(`no version field in ${filePath}`)
36
+ return match[1]
37
+ }
38
+
39
+ function readJsonVersion(filePath) {
40
+ return JSON.parse(readFileSync(filePath, 'utf-8')).version
41
+ }
42
+
43
+ function readManifestVersion(filePath, kind) {
44
+ try {
45
+ return kind === 'toml' ? readTomlVersion(filePath) : readJsonVersion(filePath)
46
+ } catch {
47
+ return null
48
+ }
49
+ }
50
+
51
+ function writeManifestVersion(filePath, kind, version) {
52
+ if (kind === 'toml') {
53
+ const text = readFileSync(filePath, 'utf-8')
54
+ const updated = text.replace(/^(version\s*=\s*")[^"]+(")/m, `$1${version}$2`)
55
+ writeFileSync(filePath, updated)
56
+ } else {
57
+ const content = JSON.parse(readFileSync(filePath, 'utf-8'))
58
+ content.version = version
59
+ writeFileSync(filePath, `${JSON.stringify(content, null, 2)}\n`)
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Compare every manifest version against package.json (source of truth).
65
+ *
66
+ * @param {string} root - repository root containing package.json + manifests
67
+ * @returns {{source: string, manifests: Array<{name: string, version: string|null, ok: boolean}>, ok: boolean}}
68
+ * - source: version read from package.json
69
+ * - manifests: per-manifest {name, version (null when unreadable), ok}
70
+ * - ok: true when every manifest matches the source version
71
+ */
72
+ export function compareVersions(root) {
73
+ const source = JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8')).version
74
+ const manifests = MANIFESTS.map(({ name, file, kind }) => {
75
+ const version = readManifestVersion(join(root, file), kind)
76
+ return { name, version, ok: version === source }
77
+ })
78
+ return { source, manifests, ok: manifests.every((m) => m.ok) }
79
+ }
80
+
81
+ /**
82
+ * Rewrite every divergent manifest to the package.json version.
83
+ *
84
+ * @param {string} root - repository root
85
+ * @returns {number} count of manifests rewritten
86
+ */
87
+ export function syncToSource(root) {
88
+ const { source, manifests } = compareVersions(root)
89
+ let changed = 0
90
+ for (const manifest of manifests) {
91
+ if (manifest.ok) continue
92
+ const def = MANIFESTS.find((m) => m.name === manifest.name)
93
+ writeManifestVersion(join(root, def.file), def.kind, source)
94
+ changed += 1
95
+ }
96
+ return changed
97
+ }
98
+
99
+ function printDivergenceTable(result) {
100
+ const rows = [
101
+ ['manifest', 'version', 'status'],
102
+ ...result.manifests.map((m) => [m.name, m.version ?? 'MISSING', m.ok ? 'ok' : 'DIVERGENT']),
103
+ ]
104
+ const widths = rows[0].map((_, i) => Math.max(...rows.map((row) => row[i].length)))
105
+ console.log(`Source of truth: package.json → v${result.source}\n`)
106
+ for (const row of rows) {
107
+ console.log(row.map((cell, i) => cell.padEnd(widths[i])).join(' '))
108
+ }
109
+ console.log('')
110
+ }
111
+
112
+ const isDirectRun =
113
+ process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href
114
+
115
+ if (isDirectRun) {
116
+ // npm scripts run from the package root, so the working directory is the
117
+ // repository root to check. Explicit cwd keeps the CLI testable.
118
+ const root = process.cwd()
119
+ const fix = process.argv.includes('--fix')
120
+ let result
121
+ try {
122
+ result = compareVersions(root)
123
+ } catch (error) {
124
+ console.error(`version-check: cannot read package.json — ${error.message}`)
125
+ process.exit(1)
126
+ }
127
+
128
+ if (result.ok) {
129
+ console.log(`✓ All manifests match package.json v${result.source}`)
130
+ process.exit(0)
131
+ }
132
+
133
+ printDivergenceTable(result)
134
+ if (fix) {
135
+ const changed = syncToSource(root)
136
+ const after = compareVersions(root)
137
+ if (after.ok) {
138
+ console.log(`✓ Synced ${changed} manifest(s) to v${after.source}`)
139
+ process.exit(0)
140
+ }
141
+ console.error(`version-check: sync incomplete — ${after.manifests.filter((m) => !m.ok).length} manifest(s) still divergent`)
142
+ process.exit(1)
143
+ }
144
+ console.error('version-check: manifests out of sync (run with --fix to sync)')
145
+ process.exit(1)
146
+ }
@@ -29,7 +29,7 @@ const MANIFEST_FILES = [
29
29
  'pyproject.toml',
30
30
  'package.json',
31
31
  'plugin.json',
32
- '.github/plugin/plugin.json',
32
+ 'src/plugins/tui/package.json',
33
33
  ]
34
34
 
35
35
  const CHANGELOG_PATH = join(ROOT, 'CHANGELOG.md')
@@ -47,8 +47,11 @@ function run(cmd) {
47
47
  }
48
48
 
49
49
  function getLatestTag() {
50
- // Use numerically highest tag, not just nearest git ancestor
51
- const tag = run("git tag -l 'v[0-9]*.[0-9]*.[0-9]*' | sort -V | tail -1")
50
+ // Stable tags only: strict vX.Y.Z with NO pre-release suffix.
51
+ // The loose glob `v[0-9]*.[0-9]*.[0-9]*` ALSO matches v1.2.0-beta.9.*
52
+ // (the trailing `*` swallows the -beta suffix), which would poison the
53
+ // version gate — so filter strictly before sorting.
54
+ const tag = run("git tag -l 'v*' | grep -E '^v[0-9]+\\.[0-9]+\\.[0-9]+$' | sort -V | tail -1")
52
55
  return tag || 'v0.0.0'
53
56
  }
54
57
 
@@ -207,13 +210,15 @@ switch (command) {
207
210
  const latestVer = latestTag.replace(/^v/, '')
208
211
 
209
212
  // If package.json is already ahead of the latest tag, someone bumped
210
- // without tagging — warn and use the current version as-is.
213
+ // without tagging — warn and use the current version as-is. Tags are
214
+ // workflow-owned, so there is nothing to create here.
211
215
  if (current !== latestVer) {
212
216
  console.log(`⚠ package.json (${current}) already ahead of latest tag (${latestTag}).`)
213
217
  console.log(` Syncing all manifests to ${current} and promoting CHANGELOG.`)
214
218
  updateManifests(current)
215
219
  const date = new Date().toISOString().slice(0, 10)
216
220
  promoteUnreleased(current, date)
221
+ console.log(`Tag v${current} will be created by the release workflow after merge to main.`)
217
222
  break
218
223
  }
219
224
 
@@ -226,16 +231,10 @@ switch (command) {
226
231
  promoteUnreleased(newVersion, date)
227
232
  console.log(`\nDone. Commit with: git add -A && git commit -m "chore(release): v${newVersion}"`)
228
233
 
229
- // Create git tag
230
- try {
231
- const tag = `v${newVersion}`
232
- execSync(`git tag -a ${tag} -m "chore(release): ${tag}"`, { stdio: 'inherit' })
233
- console.log(`Tag ${tag} created locally. Push: git push origin ${tag}`)
234
- console.log(`Auto-release will trigger on main after merge.`)
235
- } catch (e) {
236
- console.warn(`Tag creation failed: ${e.message}`)
237
- console.log('Create manually: git tag -a v' + newVersion + ' -m "chore(release): v' + newVersion + '"')
238
- }
234
+ // Tags are owned by the release workflow — never create them locally.
235
+ // A local tag on a pre-merge commit drifts from the merged main state
236
+ // (the untagged-v1.2.1 root cause) and would desync the version gate.
237
+ console.log(`Tag v${newVersion} will be created by the release workflow after merge to main.`)
239
238
  break
240
239
  }
241
240
 
@@ -50,7 +50,7 @@ permission:
50
50
 
51
51
  You are the **BACKEND TASK IMPLEMENTER** (Hermes) called by Zeus to implement FastAPI endpoints, services, and routers. Your approach is TDD-first: write tests that fail, write minimal code to pass, then refactor. You focus purely on implementation following provided plans.
52
52
 
53
- ## Core Capabilities
53
+ ## Core Capabilities
54
54
 
55
55
  ### 1. **Test-Driven Development**
56
56
  See `skill: tdd-with-agents` for the full TDD cycle.
@@ -67,7 +67,7 @@ See `skill: tdd-with-agents` for the full TDD cycle.
67
67
  - Return clear, structured results
68
68
  - Report readiness for next phase
69
69
 
70
- ### 4. **Parallel Execution Mode**
70
+ ### 4. **Parallel Execution Mode**
71
71
  - **You can run simultaneously with @aphrodite and @demeter** when scopes don't overlap
72
72
  - Your scope: backend files only (routers, services, tests)
73
73
  - Signal clearly when your phase is done so Themis can review
@@ -98,7 +98,7 @@ Mnemosyne executes the expanded compression pipeline. When Zeus delegates compre
98
98
 
99
99
  5. **Archive IMPL/REVIEW**: Append to `02-progress-log.md` (same as Level 1)
100
100
 
101
- 6. **Update Cross-References**:
101
+ 6. **Update Cross-References**:
102
102
  - Append new entries to `_xref/index.md`
103
103
  - Increment `_xref/_next_id.json`
104
104
 
@@ -81,7 +81,7 @@ Antes de usar a arvore de roteamento, consulte o memory:
81
81
 
82
82
  ```
83
83
  memory_search(task_prompt, top_k=2)
84
- → score > 0.85?
84
+ → score > 0.85?
85
85
  SIM → usa resultado cacheado (agent, background, pattern)
86
86
  NAO → aplica arvore de roteamento + memory_store() pra proxima vez
87
87
  ```
@@ -63,5 +63,3 @@ def pantheon_project() -> Path | None:
63
63
  return Path(cwd).resolve()
64
64
 
65
65
  return None
66
-
67
-