pantheon-opencode 1.5.0-beta.1 → 1.5.0-beta.10
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/.pantheon/code-mode/manifest.json +21 -0
- package/CHANGELOG.md +1026 -0
- package/README.md +171 -16
- package/README.pt-BR.md +99 -13
- package/bin/pantheon-init.mjs +178 -21
- package/package.json +23 -14
- package/plugin.json +1 -1
- package/pyproject.toml +65 -0
- package/scripts/code_mode_server.py +431 -55
- package/scripts/cost.mjs +30 -14
- package/scripts/doctor.mjs +178 -9
- package/scripts/install/cli-ui.mjs +17 -11
- package/scripts/install/config-migration.mjs +4 -6
- package/scripts/install/opencode.mjs +137 -35
- package/scripts/install/plugin.mjs +3 -33
- package/scripts/install/shared.mjs +43 -2
- package/scripts/install/strings.mjs +129 -0
- package/scripts/install-mcp.mjs +1 -0
- package/scripts/manifest-inventory.mjs +410 -0
- package/scripts/mcp_persistence_server.py +537 -128
- package/scripts/package-evidence.mjs +242 -0
- package/scripts/probe-context-rehydrate.mjs +240 -0
- package/scripts/probe-pantheon-cost.mjs +313 -0
- package/scripts/release-beta-version.mjs +56 -60
- package/scripts/sync-artifacts.mjs +169 -0
- package/scripts/sync-tui.mjs +121 -53
- package/scripts/test-opencode-v1-v2-sandbox.sh +367 -96
- package/scripts/validation-policy.mjs +7 -7
- package/scripts/version-check.mjs +113 -108
- package/scripts/versioning.mjs +230 -86
- package/src/agents/mnemosyne.md +1 -1
- package/src/agents/zeus.md +2 -2
- package/src/mcp/code_mode_server.py +431 -55
- package/src/mcp/mcp_persistence_server.py +537 -128
- package/src/pantheon/auto-wake.ts +1 -5
- package/src/pantheon/background-job-board.ts +43 -1
- package/src/pantheon/compaction-handoff.ts +304 -0
- package/src/pantheon/cost-command.ts +345 -62
- package/src/pantheon/delegate-manager.ts +390 -8
- package/src/pantheon/delegation-finalize.ts +32 -0
- package/src/pantheon/delegation.ts +235 -18
- package/src/pantheon/file-persistence-salvage.ts +64 -0
- package/src/pantheon/file-persistence.ts +194 -45
- package/src/pantheon/native-probe.ts +234 -0
- package/src/pantheon/native-task-status.ts +139 -0
- package/src/pantheon/provider-capabilities.ts +221 -0
- package/src/pantheon/provider-stream-tracker.ts +182 -0
- package/src/pantheon/shared-board.ts +37 -0
- package/src/pantheon/task-result-guard.ts +107 -0
- package/src/pantheon/token-opt.ts +146 -12
- package/src/pantheon/tool-ceiling.ts +159 -0
- package/src/pantheon/v2-events.ts +18 -1
- package/src/plugin-v2.ts +2 -14
- package/src/plugin.ts +28 -17
- package/src/plugins/pantheon-hooks.ts +51 -0
- package/src/plugins/tui/dist/server.d.ts +1 -1
- package/src/plugins/tui/dist/server.d.ts.map +1 -1
- package/src/plugins/tui/dist/server.js +1 -1
- package/src/plugins/tui/dist/server.js.map +1 -1
- package/src/plugins/tui/dist/tui.d.ts +116 -27
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +414 -150
- package/src/plugins/tui/dist/tui.js.map +1 -1
- package/src/plugins/tui/dist/tui.tsx +447 -84
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +447 -84
- package/.pantheon/code-mode/.gitkeep +0 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-1.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-10.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-11.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-2.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-3.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-4.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-5.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-6.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-7.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-8.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-9.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/heartbeat.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/session.json +0 -11
- package/src/pantheon/delegate-task-adapter.ts +0 -444
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Offline/ambiental probe for pantheon_cost.
|
|
4
|
+
*
|
|
5
|
+
* The probe never reads or writes a user's real database. Without --fixture it
|
|
6
|
+
* reports AMBIENTAL because a real dependency/database is intentionally not
|
|
7
|
+
* exercised. With --fixture it imports the installed command and creates a
|
|
8
|
+
* temporary synthetic opencode database. A missing fixture runtime is also
|
|
9
|
+
* AMBIENTAL rather than a false PASS.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs'
|
|
12
|
+
import { tmpdir } from 'node:os'
|
|
13
|
+
import { join } from 'node:path'
|
|
14
|
+
|
|
15
|
+
const requestedVersion = valueFor('--version')
|
|
16
|
+
const jsonOutput = process.argv.includes('--json')
|
|
17
|
+
const fixtureMode = process.argv.includes('--fixture')
|
|
18
|
+
|
|
19
|
+
function valueFor(name) {
|
|
20
|
+
const index = process.argv.indexOf(name)
|
|
21
|
+
return index >= 0 ? process.argv[index + 1] : undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function emit(status, detail, checks = []) {
|
|
25
|
+
const result = { status, version: requestedVersion, detail, checks }
|
|
26
|
+
if (jsonOutput) process.stdout.write(JSON.stringify(result))
|
|
27
|
+
else process.stdout.write(`${status}: ${detail}\n`)
|
|
28
|
+
if (status === 'FAIL') process.exitCode = 1
|
|
29
|
+
}
|
|
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
|
+
|
|
40
|
+
function ambiental(detail) {
|
|
41
|
+
emit('AMBIENTAL', detail)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function setEnvironment(values) {
|
|
45
|
+
const previous = new Map()
|
|
46
|
+
for (const [key, value] of Object.entries(values)) {
|
|
47
|
+
previous.set(key, process.env[key])
|
|
48
|
+
if (value === undefined) delete process.env[key]
|
|
49
|
+
else process.env[key] = value
|
|
50
|
+
}
|
|
51
|
+
return () => {
|
|
52
|
+
for (const [key, value] of previous) {
|
|
53
|
+
if (value === undefined) delete process.env[key]
|
|
54
|
+
else process.env[key] = value
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createSyntheticDb(DatabaseSync, path, rows) {
|
|
60
|
+
const db = new DatabaseSync(path)
|
|
61
|
+
db.exec('CREATE TABLE message (data TEXT NOT NULL, time_created INTEGER NOT NULL)')
|
|
62
|
+
const insert = db.prepare('INSERT INTO message (data, time_created) VALUES (?, ?)')
|
|
63
|
+
for (const data of rows) insert.run(JSON.stringify(data), Date.now())
|
|
64
|
+
db.close()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createIncompatibleDb(DatabaseSync, path) {
|
|
68
|
+
const db = new DatabaseSync(path)
|
|
69
|
+
db.exec('CREATE TABLE not_message (value TEXT NOT NULL)')
|
|
70
|
+
db.close()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function requireOutput(output, fragment, label) {
|
|
74
|
+
if (!output.includes(fragment)) throw new Error(`${label}: missing ${fragment}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function assertTokensOnly(output, expectedRows, total) {
|
|
78
|
+
requireOutput(output, 'Tokens In | Tokens Out | Tokens Total', 'tokens-only header')
|
|
79
|
+
for (const row of expectedRows) {
|
|
80
|
+
requireOutput(
|
|
81
|
+
output,
|
|
82
|
+
`| ${row.agent} | ${row.phase} | ${row.input} | ${row.output} | ${row.total} |`,
|
|
83
|
+
'token row',
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
requireOutput(
|
|
87
|
+
output,
|
|
88
|
+
`| **Total** | — | **${total.input}** | **${total.output}** | **${total.total}** |`,
|
|
89
|
+
'token total',
|
|
90
|
+
)
|
|
91
|
+
if (/Cost \(USD\)|costUsd|\$\d|\bUSD\b/i.test(output)) {
|
|
92
|
+
throw new Error('tokens-only output contains a monetary value')
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function main() {
|
|
97
|
+
if (requestedVersion !== 'v1' && requestedVersion !== 'v2') {
|
|
98
|
+
emit('FAIL', 'usage: --version v1|v2 [--fixture] [--json]')
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!fixtureMode) {
|
|
103
|
+
ambiental(
|
|
104
|
+
'real pantheon_cost dependency/database not exercised; use --fixture for synthetic checks',
|
|
105
|
+
)
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const source = new URL('../src/pantheon/cost-command.ts', import.meta.url)
|
|
110
|
+
if (!existsSync(source)) {
|
|
111
|
+
ambiental(
|
|
112
|
+
'pantheon_cost dependency is unavailable: installed package lacks src/pantheon/cost-command.ts',
|
|
113
|
+
)
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let DatabaseSync
|
|
118
|
+
let createCostCommand
|
|
119
|
+
try {
|
|
120
|
+
;({ DatabaseSync } = await import('node:sqlite'))
|
|
121
|
+
;({ createCostCommand } = await import(source.href))
|
|
122
|
+
} catch (error) {
|
|
123
|
+
ambiental(
|
|
124
|
+
`pantheon_cost fixture runtime unavailable: ${error instanceof Error ? error.message : String(error)}`,
|
|
125
|
+
)
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const root = mkdtempSync(join(tmpdir(), 'pantheon-cost-probe-'))
|
|
130
|
+
const dataHome = join(root, 'data')
|
|
131
|
+
const opencodeDir = join(dataHome, 'opencode')
|
|
132
|
+
const selectedDb = join(opencodeDir, requestedVersion === 'v2' ? 'opencode-v2.db' : 'opencode.db')
|
|
133
|
+
const otherDb = join(root, 'other.db')
|
|
134
|
+
const envDb = join(root, 'env.db')
|
|
135
|
+
const explicitDb = join(root, 'explicit.db')
|
|
136
|
+
const incompatibleDb = join(root, 'incompatible.db')
|
|
137
|
+
const checks = []
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
mkdirSync(opencodeDir, { recursive: true })
|
|
141
|
+
// These rows intentionally include a monetary-looking field to prove it is
|
|
142
|
+
// ignored; only input/output/total tokens may appear in the report.
|
|
143
|
+
const selectedRows =
|
|
144
|
+
requestedVersion === 'v2'
|
|
145
|
+
? [
|
|
146
|
+
{
|
|
147
|
+
role: 'assistant',
|
|
148
|
+
agent: 'aphrodite',
|
|
149
|
+
phase: 'plan',
|
|
150
|
+
cost: 999.99,
|
|
151
|
+
tokens: { input: 7, output: 8 },
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
role: 'assistant',
|
|
155
|
+
agent: 'aphrodite',
|
|
156
|
+
phase: 'plan',
|
|
157
|
+
cost: 999.99,
|
|
158
|
+
tokens: { input: 1, output: 2 },
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
role: 'assistant',
|
|
162
|
+
agent: 'zeus',
|
|
163
|
+
phase: 'audit',
|
|
164
|
+
cost: 999.99,
|
|
165
|
+
tokens: { input: 4, output: 6 },
|
|
166
|
+
},
|
|
167
|
+
]
|
|
168
|
+
: [
|
|
169
|
+
{
|
|
170
|
+
role: 'assistant',
|
|
171
|
+
agent: 'hermes',
|
|
172
|
+
phase: 'plan',
|
|
173
|
+
cost: 999.99,
|
|
174
|
+
tokens: { input: 10, output: 20 },
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
role: 'assistant',
|
|
178
|
+
agent: 'hermes',
|
|
179
|
+
phase: 'plan',
|
|
180
|
+
cost: 999.99,
|
|
181
|
+
tokens: { input: 3, output: 4 },
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
role: 'assistant',
|
|
185
|
+
agent: 'athena',
|
|
186
|
+
phase: 'review',
|
|
187
|
+
cost: 999.99,
|
|
188
|
+
tokens: { input: 2, output: 5 },
|
|
189
|
+
},
|
|
190
|
+
]
|
|
191
|
+
createSyntheticDb(DatabaseSync, selectedDb, selectedRows)
|
|
192
|
+
createSyntheticDb(DatabaseSync, otherDb, [
|
|
193
|
+
{
|
|
194
|
+
role: 'assistant',
|
|
195
|
+
agent: 'wrong-version',
|
|
196
|
+
phase: 'other',
|
|
197
|
+
tokens: { input: 90, output: 90 },
|
|
198
|
+
},
|
|
199
|
+
])
|
|
200
|
+
createSyntheticDb(DatabaseSync, envDb, [
|
|
201
|
+
{ role: 'assistant', agent: 'env-agent', phase: 'override', tokens: { input: 1, output: 2 } },
|
|
202
|
+
])
|
|
203
|
+
createSyntheticDb(DatabaseSync, explicitDb, [
|
|
204
|
+
{
|
|
205
|
+
role: 'assistant',
|
|
206
|
+
agent: 'explicit-agent',
|
|
207
|
+
phase: 'explicit',
|
|
208
|
+
tokens: { input: 9, output: 1 },
|
|
209
|
+
},
|
|
210
|
+
])
|
|
211
|
+
createIncompatibleDb(DatabaseSync, incompatibleDb)
|
|
212
|
+
|
|
213
|
+
const restore = setEnvironment({
|
|
214
|
+
XDG_DATA_HOME: dataHome,
|
|
215
|
+
PANTHEON_OPENCODE_VERSION: requestedVersion,
|
|
216
|
+
PANTHEON_COST_DB: undefined,
|
|
217
|
+
})
|
|
218
|
+
try {
|
|
219
|
+
const selectedOutput = await createCostCommand().pantheon_cost.execute(
|
|
220
|
+
{},
|
|
221
|
+
{ sessionID: 'cost-probe' },
|
|
222
|
+
)
|
|
223
|
+
assertTokensOnly(
|
|
224
|
+
selectedOutput,
|
|
225
|
+
requestedVersion === 'v2'
|
|
226
|
+
? [
|
|
227
|
+
{ agent: 'aphrodite', phase: 'plan', input: 8, output: 10, total: 18 },
|
|
228
|
+
{ agent: 'zeus', phase: 'audit', input: 4, output: 6, total: 10 },
|
|
229
|
+
]
|
|
230
|
+
: [
|
|
231
|
+
{ agent: 'hermes', phase: 'plan', input: 13, output: 24, total: 37 },
|
|
232
|
+
{ agent: 'athena', phase: 'review', input: 2, output: 5, total: 7 },
|
|
233
|
+
],
|
|
234
|
+
requestedVersion === 'v2'
|
|
235
|
+
? { input: 12, output: 16, total: 28 }
|
|
236
|
+
: { input: 15, output: 29, total: 44 },
|
|
237
|
+
)
|
|
238
|
+
checks.push('version-selected DB: PASS')
|
|
239
|
+
} finally {
|
|
240
|
+
restore()
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const envRestore = setEnvironment({
|
|
244
|
+
XDG_DATA_HOME: dataHome,
|
|
245
|
+
PANTHEON_OPENCODE_VERSION: requestedVersion,
|
|
246
|
+
PANTHEON_COST_DB: envDb,
|
|
247
|
+
})
|
|
248
|
+
try {
|
|
249
|
+
const output = await createCostCommand().pantheon_cost.execute(
|
|
250
|
+
{},
|
|
251
|
+
{ sessionID: 'cost-probe' },
|
|
252
|
+
)
|
|
253
|
+
requireOutput(output, '| env-agent | override | 1 | 2 | 3 |', 'PANTHEON_COST_DB')
|
|
254
|
+
if (output.includes('wrong-version') || output.includes('env-agent') === false) {
|
|
255
|
+
throw new Error('PANTHEON_COST_DB did not override the version-selected database')
|
|
256
|
+
}
|
|
257
|
+
checks.push('PANTHEON_COST_DB override: PASS')
|
|
258
|
+
} finally {
|
|
259
|
+
envRestore()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const explicitRestore = setEnvironment({
|
|
263
|
+
XDG_DATA_HOME: dataHome,
|
|
264
|
+
PANTHEON_OPENCODE_VERSION: requestedVersion,
|
|
265
|
+
PANTHEON_COST_DB: envDb,
|
|
266
|
+
})
|
|
267
|
+
try {
|
|
268
|
+
const output = await createCostCommand({ dbPath: explicitDb }).pantheon_cost.execute(
|
|
269
|
+
{},
|
|
270
|
+
{ sessionID: 'cost-probe' },
|
|
271
|
+
)
|
|
272
|
+
requireOutput(output, '| explicit-agent | explicit | 9 | 1 | 10 |', 'explicit dbPath')
|
|
273
|
+
if (output.includes('env-agent')) throw new Error('explicit dbPath did not take precedence')
|
|
274
|
+
checks.push('explicit dbPath precedence: PASS')
|
|
275
|
+
} finally {
|
|
276
|
+
explicitRestore()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const missingRestore = setEnvironment({
|
|
280
|
+
XDG_DATA_HOME: join(root, 'missing-data'),
|
|
281
|
+
PANTHEON_OPENCODE_VERSION: requestedVersion,
|
|
282
|
+
PANTHEON_COST_DB: undefined,
|
|
283
|
+
})
|
|
284
|
+
try {
|
|
285
|
+
const output = await createCostCommand().pantheon_cost.execute(
|
|
286
|
+
{},
|
|
287
|
+
{ sessionID: 'cost-probe' },
|
|
288
|
+
)
|
|
289
|
+
requireOutput(output, 'not found', 'missing DB')
|
|
290
|
+
requireOutput(
|
|
291
|
+
output,
|
|
292
|
+
requestedVersion === 'v2' ? 'opencode-v2.db' : 'opencode.db',
|
|
293
|
+
'selected missing DB path',
|
|
294
|
+
)
|
|
295
|
+
checks.push('missing DB: PASS')
|
|
296
|
+
} finally {
|
|
297
|
+
missingRestore()
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const incompatibleOutput = await createCostCommand({
|
|
301
|
+
dbPath: incompatibleDb,
|
|
302
|
+
}).pantheon_cost.execute({}, { sessionID: 'cost-probe' })
|
|
303
|
+
requireOutput(incompatibleOutput, 'incompatible opencode.db schema', 'incompatible schema')
|
|
304
|
+
checks.push('incompatible schema: PASS')
|
|
305
|
+
emit('PASS', 'offline synthetic DB checks passed; no real DB was read', checks)
|
|
306
|
+
} catch (error) {
|
|
307
|
+
emit('FAIL', diagnostic(error), checks)
|
|
308
|
+
} finally {
|
|
309
|
+
rmSync(root, { recursive: true, force: true })
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
await main()
|
|
@@ -1,80 +1,76 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
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 {
|
|
5
|
-
import { join } from 'node:path'
|
|
17
|
+
import { SEMVER_PATTERN, validateInventory, writeInventoryVersion } from './manifest-inventory.mjs'
|
|
6
18
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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(`
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
}
|