pantheon-opencode 1.5.0-beta.2 → 1.5.0-beta.20

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 (107) hide show
  1. package/.pantheon/code-mode/manifest.json +21 -0
  2. package/AGENTS.md +14 -63
  3. package/CHANGELOG.md +1260 -0
  4. package/README.md +162 -23
  5. package/README.pt-BR.md +104 -19
  6. package/bin/pantheon-init.mjs +178 -21
  7. package/commands/pantheon.md +1 -35
  8. package/package.json +23 -15
  9. package/plugin.json +1 -1
  10. package/pyproject.toml +62 -0
  11. package/scripts/code_mode_server.py +431 -55
  12. package/scripts/doctor.mjs +207 -9
  13. package/scripts/install/cli-ui.mjs +17 -11
  14. package/scripts/install/config-migration.mjs +4 -6
  15. package/scripts/install/opencode.mjs +137 -35
  16. package/scripts/install/plugin.mjs +12 -45
  17. package/scripts/install/shared.mjs +43 -2
  18. package/scripts/install/strings.mjs +129 -0
  19. package/scripts/install-mcp.mjs +1 -0
  20. package/scripts/manifest-inventory.mjs +410 -0
  21. package/scripts/mcp_persistence_server.py +536 -164
  22. package/scripts/package-evidence.mjs +242 -0
  23. package/scripts/probe-pantheon-cost.mjs +10 -1
  24. package/scripts/release-beta-version.mjs +56 -60
  25. package/scripts/sync-artifacts.mjs +169 -0
  26. package/scripts/sync-tui.mjs +129 -64
  27. package/scripts/test-opencode-v1-v2-sandbox.sh +79 -161
  28. package/scripts/validation-policy.mjs +7 -7
  29. package/scripts/version-check.mjs +113 -108
  30. package/scripts/versioning.mjs +230 -86
  31. package/src/agents/apollo.md +1 -0
  32. package/src/agents/gaia.md +1 -0
  33. package/src/agents/mnemosyne.md +1 -1
  34. package/src/agents/zeus.md +11 -54
  35. package/src/instructions/memory-protocol.instructions.md +6 -3
  36. package/src/instructions/zeus-anti-stall.instructions.md +2 -4
  37. package/src/instructions/zeus-council-synthesis.instructions.md +3 -28
  38. package/src/instructions/zeus-timeout-retry.instructions.md +3 -28
  39. package/src/mcp/code_mode_server.py +431 -55
  40. package/src/mcp/mcp_persistence_server.py +536 -164
  41. package/src/mcp/toon_codec.py +2 -4
  42. package/src/pantheon/background-job-board.ts +47 -5
  43. package/src/pantheon/{delegation-compaction.ts → compaction-context.ts} +10 -100
  44. package/src/pantheon/cost-command.ts +309 -44
  45. package/src/pantheon/delegation-enforce.ts +59 -14
  46. package/src/pantheon/file-persistence-salvage.ts +64 -0
  47. package/src/pantheon/file-persistence.ts +194 -45
  48. package/src/pantheon/goal-loop.ts +1 -1
  49. package/src/pantheon/goal-store.ts +1 -1
  50. package/src/pantheon/idle-continuation.ts +4 -6
  51. package/src/pantheon/model-command.ts +1 -2
  52. package/src/pantheon/native-task-status.ts +139 -0
  53. package/src/pantheon/presets.d.mts +0 -20
  54. package/src/pantheon/presets.mjs +0 -64
  55. package/src/pantheon/session-guard.ts +2 -2
  56. package/src/pantheon/session-id.ts +30 -0
  57. package/src/pantheon/shared-board.ts +37 -0
  58. package/src/pantheon/task-result-guard.ts +106 -0
  59. package/src/pantheon/todo-enforcer.ts +3 -3
  60. package/src/pantheon/tool-ceiling.ts +159 -0
  61. package/src/pantheon/tool-context.ts +23 -0
  62. package/src/pantheon/v2-bridge.ts +1 -4
  63. package/src/plugin-v2.ts +8 -21
  64. package/src/plugin.ts +83 -251
  65. package/src/plugins/pantheon-hooks.ts +51 -0
  66. package/src/plugins/tui/dist/server.d.ts +1 -1
  67. package/src/plugins/tui/dist/server.d.ts.map +1 -1
  68. package/src/plugins/tui/dist/server.js +1 -1
  69. package/src/plugins/tui/dist/server.js.map +1 -1
  70. package/src/plugins/tui/dist/tui.d.ts +261 -77
  71. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  72. package/src/plugins/tui/dist/tui.js +640 -234
  73. package/src/plugins/tui/dist/tui.js.map +1 -1
  74. package/src/plugins/tui/package-lock.json +2 -2
  75. package/src/plugins/tui/package.json +2 -2
  76. package/src/plugins/tui/src/index.tsx +872 -226
  77. package/src/routing.yml +0 -16
  78. package/src/skills/context-compression/SKILL.md +1 -1
  79. package/scripts/generate-preset-docs.mjs +0 -300
  80. package/scripts/generate-prompts.sh +0 -222
  81. package/scripts/hash_verify.py +0 -192
  82. package/scripts/install/component.mjs +0 -24
  83. package/scripts/install/migrate-config.mjs +0 -302
  84. package/scripts/test-context-compression.sh +0 -169
  85. package/src/pantheon/auto-wake.ts +0 -89
  86. package/src/pantheon/chat-reminders.ts +0 -9
  87. package/src/pantheon/comment-checker.ts +0 -48
  88. package/src/pantheon/compaction-assert.ts +0 -160
  89. package/src/pantheon/cost-tracker.ts +0 -108
  90. package/src/pantheon/delegate-manager.ts +0 -669
  91. package/src/pantheon/delegation-classifier.ts +0 -307
  92. package/src/pantheon/delegation-finalize.ts +0 -374
  93. package/src/pantheon/delegation-notify.ts +0 -212
  94. package/src/pantheon/delegation-toast.ts +0 -73
  95. package/src/pantheon/delegation.ts +0 -1120
  96. package/src/pantheon/dispatch-guard.ts +0 -109
  97. package/src/pantheon/permission-eval.ts +0 -284
  98. package/src/pantheon/preemptive-compact.ts +0 -76
  99. package/src/pantheon/retry-policy.ts +0 -212
  100. package/src/pantheon/step-cap.ts +0 -110
  101. package/src/pantheon/token-opt.ts +0 -781
  102. package/src/pantheon/toon-codec.ts +0 -481
  103. package/src/pantheon/v2-events.ts +0 -131
  104. package/src/pantheon/v2-hooks.ts +0 -287
  105. package/src/pantheon/v2-tool-definitions.ts +0 -331
  106. package/src/pantheon/zeus-delegate-with-retry.ts +0 -406
  107. package/src/plugins/tui/dist/tui.tsx +0 -2605
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env node
2
+ /** Build and verify the single immutable npm package evidence artifact. */
3
+
4
+ import { execFileSync } from 'node:child_process'
5
+ import { createHash } from 'node:crypto'
6
+ import {
7
+ existsSync,
8
+ lstatSync,
9
+ mkdirSync,
10
+ mkdtempSync,
11
+ readdirSync,
12
+ readFileSync,
13
+ realpathSync,
14
+ rmSync,
15
+ writeFileSync,
16
+ } from 'node:fs'
17
+ import { tmpdir } from 'node:os'
18
+ import { isAbsolute, join } from 'node:path'
19
+ import { validateInventory } from './manifest-inventory.mjs'
20
+
21
+ const CI_ARGS = ['ci', '--ignore-scripts', '--omit=dev', '--no-audit', '--no-fund']
22
+ const REQUIRED_PAYLOAD = [
23
+ 'package/package.json',
24
+ 'package/plugin.json',
25
+ 'package/pyproject.toml',
26
+ 'package/src/plugins/tui/package.json',
27
+ 'package/src/plugins/tui/package-lock.json',
28
+ 'package/src/plugin.ts',
29
+ 'package/src/plugin-v2.ts',
30
+ 'package/src/pantheon/v2-bridge.ts',
31
+ 'package/src/plugins/tui/dist/tui.js',
32
+ 'package/src/plugins/tui/dist/server.js',
33
+ 'package/bin/pantheon-init.mjs',
34
+ 'package/scripts/doctor.mjs',
35
+ 'package/.pantheon/code-mode/compress-inline.py',
36
+ ]
37
+
38
+ function fail(message) {
39
+ throw new Error(message)
40
+ }
41
+
42
+ function resolveTargetSha(root, supplied) {
43
+ const raw =
44
+ supplied ?? execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' })
45
+ const sha = String(raw).trim().toLowerCase()
46
+ if (!/^[0-9a-f]{40}$/.test(sha))
47
+ fail('target SHA must be a full lowercase 40-character hexadecimal SHA')
48
+ return sha
49
+ }
50
+
51
+ function createOutputDirectory(outputDir) {
52
+ const directory = outputDir ?? mkdtempSync(join(tmpdir(), 'pantheon-package-evidence-'))
53
+ mkdirSync(directory, { recursive: true })
54
+ if (readdirSync(directory).length !== 0) fail(`output directory must be empty: ${directory}`)
55
+ return { directory }
56
+ }
57
+
58
+ function packOnce(root, outputDir) {
59
+ const raw = execFileSync(
60
+ 'npm',
61
+ ['pack', '--ignore-scripts', '--json', '--pack-destination', outputDir],
62
+ { cwd: root, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 },
63
+ )
64
+ let result
65
+ try {
66
+ result = JSON.parse(raw)
67
+ } catch (error) {
68
+ fail(`npm pack returned malformed JSON: ${error.message}`)
69
+ }
70
+ if (!Array.isArray(result) || result.length !== 1 || typeof result[0]?.filename !== 'string') {
71
+ fail('npm pack must produce exactly one JSON result')
72
+ }
73
+ const tgzFiles = readdirSync(outputDir).filter((name) => name.endsWith('.tgz'))
74
+ if (tgzFiles.length !== 1)
75
+ fail(`package evidence requires exactly one .tgz, found ${tgzFiles.length}`)
76
+ if (tgzFiles[0] !== result[0].filename)
77
+ fail('npm pack result does not identify the sole .tgz artifact')
78
+ return join(outputDir, tgzFiles[0])
79
+ }
80
+
81
+ function validateArchiveEntries(tarball) {
82
+ const listing = execFileSync('tar', ['-tzf', tarball], { encoding: 'utf8' })
83
+ const entries = listing.split('\n').filter(Boolean)
84
+ if (entries.length === 0) fail('package tarball is empty')
85
+ for (const entry of entries) {
86
+ const normalized = entry.replace(/\/$/, '')
87
+ const parts = normalized.split('/')
88
+ if (
89
+ isAbsolute(normalized) ||
90
+ normalized.includes('\\') ||
91
+ parts.includes('..') ||
92
+ (!normalized.startsWith('package/') && normalized !== 'package')
93
+ ) {
94
+ fail(`unsafe package archive entry: ${entry}`)
95
+ }
96
+ }
97
+ const detailed = execFileSync('tar', ['-tvzf', tarball], { encoding: 'utf8' })
98
+ for (const line of detailed.split('\n').filter(Boolean)) {
99
+ if (/^[lh]/i.test(line)) fail(`package archive contains a link entry: ${line}`)
100
+ }
101
+ return new Set(entries)
102
+ }
103
+
104
+ function extractSafely(tarball, workDir) {
105
+ const extractionRoot = join(workDir, 'extract')
106
+ mkdirSync(extractionRoot)
107
+ validateArchiveEntries(tarball)
108
+ execFileSync(
109
+ 'tar',
110
+ ['-xzf', tarball, '--directory', extractionRoot, '--no-same-owner', '--no-same-permissions'],
111
+ { encoding: 'utf8' },
112
+ )
113
+ const realRoot = realpathSync(extractionRoot)
114
+ const walk = (current) => {
115
+ const stat = lstatSync(current)
116
+ if (stat.isSymbolicLink()) fail(`package archive extraction contains a symlink: ${current}`)
117
+ const resolved = realpathSync(current)
118
+ if (resolved !== realRoot && !resolved.startsWith(`${realRoot}/`)) {
119
+ fail(`package archive extraction escaped its temporary directory: ${current}`)
120
+ }
121
+ if (stat.isDirectory()) for (const name of readdirSync(current)) walk(join(current, name))
122
+ }
123
+ walk(extractionRoot)
124
+ return join(extractionRoot, 'package')
125
+ }
126
+
127
+ function validatePayload(packageRoot, inventory) {
128
+ for (const relativePath of REQUIRED_PAYLOAD) {
129
+ const path = join(packageRoot, relativePath.replace(/^package\//, ''))
130
+ if (!existsSync(path) || !lstatSync(path).isFile())
131
+ fail(`required package payload is missing: ${relativePath}`)
132
+ }
133
+ for (const document of inventory.documents) {
134
+ const { entry, path: sourcePath } = document
135
+ const packagedPath = join(packageRoot, entry.file)
136
+ if (entry.role === 'lock' && entry.file === 'package-lock.json' && !existsSync(packagedPath)) {
137
+ // npm-packlist excludes the package root lockfile by policy. It remains
138
+ // validated below as source evidence, but is not fabricated in the tgz.
139
+ continue
140
+ }
141
+ if (!existsSync(packagedPath) || !lstatSync(packagedPath).isFile())
142
+ fail(`versioned package payload is missing: package/${entry.file}`)
143
+ if (sha256(sourcePath) !== sha256(packagedPath))
144
+ fail(`published package payload differs from source: ${entry.file}`)
145
+ }
146
+ }
147
+
148
+ function collectLockfileEvidence(inventory, entries, packageRoot) {
149
+ return inventory.documents
150
+ .filter(({ entry }) => entry.role === 'lock')
151
+ .map(({ entry, data, path: sourcePath }) => {
152
+ const archivePath = `package/${entry.file}`
153
+ const packagedPath = join(packageRoot, entry.file)
154
+ const publishedInTarball = entries.has(archivePath)
155
+ return {
156
+ file: entry.file,
157
+ sourceSha256: sha256(sourcePath),
158
+ publishedSha256: publishedInTarball ? sha256(packagedPath) : null,
159
+ sourceName: data.name,
160
+ sourceVersion: data.version,
161
+ lockfileVersion: data.lockfileVersion,
162
+ parity: 'validated-against-manifest',
163
+ publishedInTarball,
164
+ }
165
+ })
166
+ }
167
+
168
+ function installProductionDependencies(root) {
169
+ // The root package-lock.json is source evidence only: npm-packlist omits it
170
+ // from the root of an npm package, so root ci must validate the source tree.
171
+ execFileSync('npm', CI_ARGS, { cwd: root, stdio: 'inherit' })
172
+ execFileSync('npm', CI_ARGS, {
173
+ cwd: join(root, 'src', 'plugins', 'tui'),
174
+ stdio: 'inherit',
175
+ })
176
+ }
177
+
178
+ function sha256(path) {
179
+ return createHash('sha256').update(readFileSync(path)).digest('hex')
180
+ }
181
+
182
+ export function createPackageEvidence(root, { outputDir, targetSha } = {}) {
183
+ const inventory = validateInventory(root)
184
+ const sha = resolveTargetSha(root, targetSha)
185
+ const output = createOutputDirectory(outputDir)
186
+ let workDir
187
+ try {
188
+ const tarball = packOnce(root, output.directory)
189
+ workDir = mkdtempSync(join(tmpdir(), 'pantheon-package-evidence-check-'))
190
+ const entries = validateArchiveEntries(tarball)
191
+ const packageRoot = extractSafely(tarball, workDir)
192
+ validatePayload(packageRoot, inventory)
193
+ installProductionDependencies(root)
194
+ const digest = sha256(tarball)
195
+ if (!/^[0-9a-f]{64}$/.test(digest)) fail('tarball SHA-256 is not lowercase 64-hex')
196
+ const metadata = {
197
+ version: inventory.sourceVersion,
198
+ targetSha: sha,
199
+ tarballSha256: digest,
200
+ tarball: tarball.split('/').at(-1),
201
+ lockfilePolicy: {
202
+ rootPackageLockfile: entries.has('package/package-lock.json')
203
+ ? 'published-by-npm-pack'
204
+ : 'omitted-by-npm-pack',
205
+ note: entries.has('package/package-lock.json')
206
+ ? 'npm pack published the root package-lock.json; lockfile parity is validated and recorded as source evidence, while tarball validation covers only the payload npm pack publishes.'
207
+ : 'npm pack omits the package root package-lock.json by policy; lockfile parity is validated and recorded as source evidence, while tarball validation covers only the payload npm pack publishes.',
208
+ },
209
+ lockfileEvidence: collectLockfileEvidence(inventory, entries, packageRoot),
210
+ }
211
+ writeFileSync(join(output.directory, 'metadata.json'), `${JSON.stringify(metadata, null, 2)}\n`)
212
+ writeFileSync(join(output.directory, 'version'), `${metadata.version}\n`)
213
+ writeFileSync(join(output.directory, 'target-sha'), `${metadata.targetSha}\n`)
214
+ writeFileSync(join(output.directory, 'tarball-sha256'), `${metadata.tarballSha256}\n`)
215
+ return { ...metadata, outputDir: output.directory, tarball }
216
+ } catch (error) {
217
+ rmSync(output.directory, { recursive: true, force: true })
218
+ throw error
219
+ } finally {
220
+ if (workDir) rmSync(workDir, { recursive: true, force: true })
221
+ }
222
+ }
223
+
224
+ function parseOptions(args) {
225
+ const options = {}
226
+ for (const arg of args) {
227
+ const match = /^--(output-dir|target-sha)=(.*)$/.exec(arg)
228
+ if (!match) fail(`unsupported package evidence option: ${arg}`)
229
+ options[match[1] === 'output-dir' ? 'outputDir' : 'targetSha'] = match[2]
230
+ }
231
+ return options
232
+ }
233
+
234
+ if (process.argv[1]?.endsWith('package-evidence.mjs')) {
235
+ try {
236
+ const evidence = createPackageEvidence(process.cwd(), parseOptions(process.argv.slice(2)))
237
+ console.log(JSON.stringify(evidence))
238
+ } catch (error) {
239
+ console.error(`package-evidence: ${error.message}`)
240
+ process.exit(1)
241
+ }
242
+ }
@@ -28,6 +28,15 @@ function emit(status, detail, checks = []) {
28
28
  if (status === 'FAIL') process.exitCode = 1
29
29
  }
30
30
 
31
+ function diagnostic(error) {
32
+ const message = error instanceof Error ? error.message : String(error)
33
+ const stderr =
34
+ error && typeof error === 'object' && typeof error.stderr === 'string'
35
+ ? error.stderr.trim()
36
+ : ''
37
+ return stderr === '' ? message : `${message}; stderr: ${stderr}`
38
+ }
39
+
31
40
  function ambiental(detail) {
32
41
  emit('AMBIENTAL', detail)
33
42
  }
@@ -295,7 +304,7 @@ async function main() {
295
304
  checks.push('incompatible schema: PASS')
296
305
  emit('PASS', 'offline synthetic DB checks passed; no real DB was read', checks)
297
306
  } catch (error) {
298
- emit('FAIL', error instanceof Error ? error.message : String(error), checks)
307
+ emit('FAIL', diagnostic(error), checks)
299
308
  } finally {
300
309
  rmSync(root, { recursive: true, force: true })
301
310
  }
@@ -1,80 +1,76 @@
1
1
  #!/usr/bin/env node
2
- /** Compute and optionally apply the beta version for a release PR. */
2
+ /**
3
+ * release-beta-version.mjs — beta version helpers.
4
+ *
5
+ * The beta version is committed in the manifests (package.json) and never
6
+ * computed at release time. `nextBetaVersion` advances the committed version:
7
+ *
8
+ * X.Y.Z-beta.N -> X.Y.Z-beta.(N+1) (stay on the current beta line)
9
+ * X.Y.Z -> X.Y.(Z+1)-beta.1 (start a fresh beta line)
10
+ *
11
+ * `applyBetaVersion` writes an already-decided beta version into every
12
+ * manifest and is retained for recovery tooling. The legacy PR+SHA scheme
13
+ * (`X.Y.Z-beta.<pr>.<sha7>`) is accepted by validation for backward
14
+ * compatibility only; it is no longer generated.
15
+ */
3
16
 
4
- import { readFileSync, writeFileSync } from 'node:fs'
5
- import { join } from 'node:path'
17
+ import { SEMVER_PATTERN, validateInventory, writeInventoryVersion } from './manifest-inventory.mjs'
6
18
 
7
- export const MANIFESTS = [
8
- { file: 'package.json', kind: 'json' },
9
- { file: 'pyproject.toml', kind: 'toml' },
10
- { file: 'plugin.json', kind: 'json' },
11
- { file: 'src/plugins/tui/package.json', kind: 'json' },
12
- ]
13
-
14
- const STABLE = /^(\d+)\.(\d+)\.(\d+)$/
19
+ const STABLE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/
20
+ // Current committed sequential beta line.
21
+ const BETA_LINE = /^(\d+\.\d+\.\d+)-beta\.(\d+)$/
22
+ // Legacy PR+SHA beta scheme, accepted for recovery only.
23
+ const LEGACY_BETA = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-beta\.[1-9]\d*\.[0-9a-f]{7}$/
24
+ const BETA_RELEASE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-beta\.[1-9]\d*$/
15
25
 
16
26
  export function nextStableVersion(latest, intent = 'patch') {
17
- const match = STABLE.exec(String(latest))
18
- if (!match) throw new Error(`npm latest is not a stable semver: "${latest}"`)
19
- const [major, minor, patch] = match.slice(1).map(Number)
27
+ const match = STABLE.exec(String(latest).trim())
28
+ if (!match) throw new Error(`latest is not a stable semver: "${latest}"`)
20
29
  if (!['patch', 'minor', 'major'].includes(intent)) {
21
30
  throw new Error(`release intent must be patch, minor, or major: "${intent}"`)
22
31
  }
32
+ const [major, minor, patch] = match.slice(1).map(Number)
23
33
  if (intent === 'major') return `${major + 1}.0.0`
24
34
  if (intent === 'minor') return `${major}.${minor + 1}.0`
25
35
  return `${major}.${minor}.${patch + 1}`
26
36
  }
27
37
 
28
- export function betaVersion(latest, intent, pr, sha) {
29
- if (!/^\d+$/.test(String(pr)) || Number(pr) < 1) throw new Error(`invalid PR number: "${pr}"`)
30
- const shortSha = String(sha).slice(0, 7)
31
- if (!/^[0-9a-f]{7,}$/i.test(shortSha)) throw new Error(`invalid commit SHA: "${sha}"`)
32
- return `${nextStableVersion(latest, intent)}-beta.${pr}.${shortSha}`
33
- }
38
+ /**
39
+ * Next beta version derived from the committed manifest version.
40
+ *
41
+ * @param {string} current committed version (e.g. "1.5.0-beta.2")
42
+ * @returns {string} the next sequential beta (e.g. "1.5.0-beta.3")
43
+ */
44
+ export function nextBetaVersion(current) {
45
+ const value = String(current).trim()
46
+ const line = BETA_LINE.exec(value)
47
+ if (line) return `${line[1]}-beta.${Number(line[2]) + 1}`
34
48
 
35
- function writeVersion(root, { file, kind }, version) {
36
- const path = join(root, file)
37
- const raw = readFileSync(path, 'utf8')
38
- if (kind === 'toml') {
39
- const updated = raw.replace(/^(version\s*=\s*")[^"]+("\s*)$/m, `$1${version}$2`)
40
- if (updated === raw) throw new Error(`no version field in ${file}`)
41
- writeFileSync(path, updated)
42
- return
43
- }
44
- const content = JSON.parse(raw)
45
- content.version = version
46
- writeFileSync(path, `${JSON.stringify(content, null, 2)}\n`)
47
- }
49
+ const stable = STABLE.exec(value)
50
+ if (stable) return `${nextStableVersion(value, 'patch')}-beta.1`
48
51
 
49
- export function applyBetaVersion(root, version) {
50
- for (const manifest of MANIFESTS) writeVersion(root, manifest, version)
51
- // Keep npm's lockfile metadata consistent without changing dependency data.
52
- const lockPath = join(root, 'package-lock.json')
53
- const lock = JSON.parse(readFileSync(lockPath, 'utf8'))
54
- lock.version = version
55
- if (lock.packages?.['']) lock.packages[''].version = version
56
- writeFileSync(lockPath, `${JSON.stringify(lock, null, 2)}\n`)
52
+ // Any other prerelease (e.g. 1.5.0-rc.1): beta the next patch of its base.
53
+ const base = /^(\d+\.\d+\.\d+)-[0-9A-Za-z.-]+$/.exec(value)
54
+ if (!base) throw new Error(`cannot derive a beta version from: "${current}"`)
55
+ return `${nextStableVersion(base[1], 'patch')}-beta.1`
57
56
  }
58
57
 
59
- const args = new Map(
60
- process.argv.slice(2).map((arg) => {
61
- const [key, ...value] = arg.split('=')
62
- return [key.replace(/^--/, ''), value.join('=')]
63
- }),
64
- )
65
-
66
- if (args.has('latest')) {
67
- try {
68
- const version = betaVersion(
69
- args.get('latest'),
70
- args.get('intent') || 'patch',
71
- args.get('pr'),
72
- args.get('sha'),
58
+ function validateReleaseVersion(version) {
59
+ if (typeof version !== 'string' || !SEMVER_PATTERN.test(version)) {
60
+ throw new Error(`invalid release semver: "${version}"`)
61
+ }
62
+ if (!BETA_RELEASE.test(version) && !LEGACY_BETA.test(version)) {
63
+ throw new Error(
64
+ `release version must be <base>-beta.<N> or legacy <base>-beta.<pr>.<7-char-sha>: "${version}"`,
73
65
  )
74
- if (args.has('apply')) applyBetaVersion(process.cwd(), version)
75
- console.log(version)
76
- } catch (error) {
77
- console.error(`release-beta-version: ${error.message}`)
78
- process.exit(1)
79
66
  }
80
67
  }
68
+
69
+ /** Preflight all six entries, then update metadata only. */
70
+ export function applyBetaVersion(root, version) {
71
+ validateReleaseVersion(version)
72
+ const inventory = validateInventory(root, { allowVersionDivergence: true })
73
+ writeInventoryVersion(inventory, version)
74
+ const after = validateInventory(root)
75
+ if (after.sourceVersion !== version) throw new Error('beta version post-write validation failed')
76
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * sync-artifacts.mjs — postinstall freshness sync for copy-only artifacts.
3
+ *
4
+ * Guarantees that an `npm install -g pantheon-opencode` (or npx cache
5
+ * refresh) brings an EXISTING installation up to date for everything that is
6
+ * a pure byte-compare copy: agents + routing.yml, skills, AGENTS.md,
7
+ * commands, the runtime MCP scripts, the code-mode payload and tiers.json.
8
+ *
9
+ * Deliberately NOT done here (they belong to `pantheon-opencode init` /
10
+ * `update`, which the doctor points at when versions drift):
11
+ * - opencode.json merge (config keys, plugin registration, MCP entries)
12
+ * - venv/pip setup
13
+ * - tui.json registration changes
14
+ *
15
+ * The user's opencode.json already references the installed package root for
16
+ * plugins, so after a package update those references hit the NEW code
17
+ * automatically; this sync refreshes the COPIES that would otherwise stay
18
+ * stale until the next manual init.
19
+ */
20
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs'
21
+ import { join } from 'node:path'
22
+
23
+ import {
24
+ collectSkillNames,
25
+ copyFiles,
26
+ installSkills,
27
+ ROOT,
28
+ syncDir,
29
+ writeIfChanged,
30
+ } from './install/shared.mjs'
31
+
32
+ /** Canonical MCP script sources — mirrors installOpenCode phase 2.10. */
33
+ const MCP_SCRIPTS = {
34
+ 'mcp_resources_server.py': () => join(ROOT, 'src', 'mcp', 'mcp_resources_server.py'),
35
+ 'code_mode_server.py': () => join(ROOT, 'src', 'mcp', 'code_mode_server.py'),
36
+ 'memory_mcp_server.py': () => join(ROOT, 'src', 'mcp', 'memory_mcp_server.py'),
37
+ 'mcp_persistence_server.py': () => join(ROOT, 'src', 'mcp', 'mcp_persistence_server.py'),
38
+ '_pantheon_paths.py': () => join(ROOT, 'src', 'mcp', '_pantheon_paths.py'),
39
+ 'mcp_codemap_module.py': () => join(ROOT, 'src', 'mcp', 'mcp_codemap_module.py'),
40
+ 'pantheon_vision_server.py': () => join(ROOT, 'src', 'mcp', 'pantheon_vision_server.py'),
41
+ 'eval_store.py': () => join(ROOT, 'src', 'mcp', 'eval_store.py'),
42
+ 'scrub-secrets.py': () => join(ROOT, 'scripts', 'scrub-secrets.py'),
43
+ }
44
+
45
+ /**
46
+ * Sync all copy-only artifacts from the installed package into configDir
47
+ * (the GLOBAL OpenCode config dir). Byte-compare idempotent; never throws.
48
+ * @returns {{ created: number, skipped: number, errors: string[] }}
49
+ */
50
+ export function syncCopyArtifacts(configDir) {
51
+ const result = { created: 0, skipped: 0, errors: [] }
52
+ const bump = (r) => {
53
+ result.created += r.created
54
+ result.skipped += r.skipped
55
+ }
56
+
57
+ // ── agents + routing.yml ──
58
+ try {
59
+ const dstAgents = join(configDir, 'agents')
60
+ if (existsSync(join(ROOT, 'src', 'agents'))) {
61
+ mkdirSync(dstAgents, { recursive: true })
62
+ bump(copyFiles(join(ROOT, 'src', 'agents'), dstAgents, false))
63
+ const routing = join(ROOT, 'src', 'routing.yml')
64
+ if (existsSync(routing)) {
65
+ if (
66
+ writeIfChanged(join(configDir, 'routing.yml'), readFileSync(routing, 'utf8'), false) ===
67
+ 'created'
68
+ ) {
69
+ result.created++
70
+ } else result.skipped++
71
+ }
72
+ }
73
+ } catch (err) {
74
+ result.errors.push(`agents: ${err.message}`)
75
+ }
76
+
77
+ // ── skills ──
78
+ try {
79
+ const names = collectSkillNames()
80
+ if (names.length > 0) bump(installSkills(names, configDir, false, ''))
81
+ } catch (err) {
82
+ result.errors.push(`skills: ${err.message}`)
83
+ }
84
+
85
+ // ── instructions (AGENTS.md) ──
86
+ try {
87
+ const src = join(ROOT, 'AGENTS.md')
88
+ if (existsSync(src)) {
89
+ if (
90
+ writeIfChanged(join(configDir, 'AGENTS.md'), readFileSync(src, 'utf8'), false) === 'created'
91
+ ) {
92
+ result.created++
93
+ } else result.skipped++
94
+ }
95
+ } catch (err) {
96
+ result.errors.push(`instructions: ${err.message}`)
97
+ }
98
+
99
+ // ── commands (*.md) ──
100
+ try {
101
+ const srcCmds = join(ROOT, 'commands')
102
+ if (existsSync(srcCmds)) {
103
+ bump(syncDir(srcCmds, join(configDir, 'commands'), false, false, (f) => f.endsWith('.md')))
104
+ }
105
+ } catch (err) {
106
+ result.errors.push(`commands: ${err.message}`)
107
+ }
108
+
109
+ // ── runtime payload (MCP scripts, code-mode, tiers) ──
110
+ try {
111
+ const dstScripts = join(configDir, 'scripts')
112
+ mkdirSync(dstScripts, { recursive: true })
113
+ for (const [name, srcFn] of Object.entries(MCP_SCRIPTS)) {
114
+ const src = srcFn()
115
+ if (!existsSync(src)) continue
116
+ const content = readFileSync(src, 'utf8')
117
+ const dst = join(dstScripts, name)
118
+ if (writeIfChanged(dst, content, false) === 'created') {
119
+ statSync(dst)
120
+ result.created++
121
+ } else result.skipped++
122
+ }
123
+
124
+ const req = join(ROOT, 'src', 'mcp', 'requirements-vision.txt')
125
+ if (existsSync(req)) {
126
+ if (
127
+ writeIfChanged(
128
+ join(configDir, 'requirements-vision.txt'),
129
+ readFileSync(req, 'utf8'),
130
+ false,
131
+ ) === 'created'
132
+ ) {
133
+ result.created++
134
+ } else result.skipped++
135
+ }
136
+
137
+ const srcCodeMode = join(ROOT, '.pantheon', 'code-mode')
138
+ const dstCodeMode = join(configDir, '.pantheon', 'code-mode')
139
+ if (existsSync(srcCodeMode)) {
140
+ bump(syncDir(srcCodeMode, dstCodeMode, false, false))
141
+ }
142
+
143
+ const srcTiers = join(ROOT, '.pantheon', 'tiers.json')
144
+ const dstTiers = join(configDir, '.pantheon', 'tiers.json')
145
+ if (existsSync(srcTiers)) {
146
+ if (writeIfChanged(dstTiers, readFileSync(srcTiers, 'utf8'), false) === 'created')
147
+ result.created++
148
+ else result.skipped++
149
+ }
150
+ } catch (err) {
151
+ result.errors.push(`runtime: ${err.message}`)
152
+ }
153
+
154
+ return result
155
+ }
156
+
157
+ /** Drop stale entries in dst that no longer exist in src (agents/skills dirs). */
158
+ export function pruneMissing(srcDir, dstDir) {
159
+ if (!existsSync(srcDir) || !existsSync(dstDir)) return 0
160
+ const src = new Set(readdirSync(srcDir))
161
+ let removed = 0
162
+ for (const entry of readdirSync(dstDir)) {
163
+ if (!src.has(entry)) {
164
+ rmSync(join(dstDir, entry), { recursive: true, force: true })
165
+ removed++
166
+ }
167
+ }
168
+ return removed
169
+ }