pantheon-opencode 1.5.0-beta.1 → 1.5.0-beta.11

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 (83) hide show
  1. package/.pantheon/code-mode/manifest.json +21 -0
  2. package/CHANGELOG.md +1054 -0
  3. package/README.md +171 -16
  4. package/README.pt-BR.md +99 -13
  5. package/bin/pantheon-init.mjs +178 -21
  6. package/package.json +23 -15
  7. package/plugin.json +1 -1
  8. package/pyproject.toml +65 -0
  9. package/scripts/code_mode_server.py +431 -55
  10. package/scripts/cost.mjs +30 -14
  11. package/scripts/dev-tui.sh +48 -0
  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 +3 -33
  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 +537 -128
  22. package/scripts/package-evidence.mjs +242 -0
  23. package/scripts/probe-context-rehydrate.mjs +240 -0
  24. package/scripts/probe-pantheon-cost.mjs +313 -0
  25. package/scripts/release-beta-version.mjs +56 -60
  26. package/scripts/sync-artifacts.mjs +169 -0
  27. package/scripts/sync-tui.mjs +121 -53
  28. package/scripts/test-opencode-v1-v2-sandbox.sh +367 -96
  29. package/scripts/validation-policy.mjs +7 -7
  30. package/scripts/version-check.mjs +113 -108
  31. package/scripts/versioning.mjs +230 -86
  32. package/src/agents/mnemosyne.md +1 -1
  33. package/src/agents/zeus.md +2 -2
  34. package/src/mcp/code_mode_server.py +431 -55
  35. package/src/mcp/mcp_persistence_server.py +537 -128
  36. package/src/pantheon/auto-wake.ts +1 -5
  37. package/src/pantheon/background-job-board.ts +43 -1
  38. package/src/pantheon/compaction-handoff.ts +304 -0
  39. package/src/pantheon/cost-command.ts +345 -62
  40. package/src/pantheon/delegate-manager.ts +390 -8
  41. package/src/pantheon/delegation-finalize.ts +32 -0
  42. package/src/pantheon/delegation.ts +235 -18
  43. package/src/pantheon/file-persistence-salvage.ts +64 -0
  44. package/src/pantheon/file-persistence.ts +194 -45
  45. package/src/pantheon/native-probe.ts +234 -0
  46. package/src/pantheon/native-task-status.ts +139 -0
  47. package/src/pantheon/provider-capabilities.ts +221 -0
  48. package/src/pantheon/provider-stream-tracker.ts +182 -0
  49. package/src/pantheon/shared-board.ts +37 -0
  50. package/src/pantheon/task-result-guard.ts +107 -0
  51. package/src/pantheon/token-opt.ts +146 -12
  52. package/src/pantheon/tool-ceiling.ts +159 -0
  53. package/src/pantheon/v2-events.ts +18 -1
  54. package/src/plugin-v2.ts +2 -14
  55. package/src/plugin.ts +28 -17
  56. package/src/plugins/pantheon-hooks.ts +51 -0
  57. package/src/plugins/tui/dist/server.d.ts +1 -1
  58. package/src/plugins/tui/dist/server.d.ts.map +1 -1
  59. package/src/plugins/tui/dist/server.js +1 -1
  60. package/src/plugins/tui/dist/server.js.map +1 -1
  61. package/src/plugins/tui/dist/tui.d.ts +137 -27
  62. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  63. package/src/plugins/tui/dist/tui.js +459 -163
  64. package/src/plugins/tui/dist/tui.js.map +1 -1
  65. package/src/plugins/tui/dist/tui.tsx +506 -103
  66. package/src/plugins/tui/package-lock.json +2 -2
  67. package/src/plugins/tui/package.json +1 -1
  68. package/src/plugins/tui/src/index.tsx +506 -103
  69. package/.pantheon/code-mode/.gitkeep +0 -0
  70. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-1.json +0 -8
  71. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-10.json +0 -8
  72. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-11.json +0 -8
  73. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-2.json +0 -8
  74. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-3.json +0 -8
  75. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-4.json +0 -8
  76. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-5.json +0 -8
  77. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-6.json +0 -8
  78. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-7.json +0 -8
  79. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-8.json +0 -8
  80. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-9.json +0 -8
  81. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/heartbeat.json +0 -8
  82. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/session.json +0 -11
  83. package/src/pantheon/delegate-task-adapter.ts +0 -444
@@ -0,0 +1,410 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The only authoritative list of versioned manifests and lockfiles.
4
+ *
5
+ * Keep this list deliberately boring: release/version scripts import it rather
6
+ * than maintaining their own, subtly different lists.
7
+ */
8
+ import { execFileSync } from 'node:child_process'
9
+ import { randomBytes } from 'node:crypto'
10
+ import {
11
+ closeSync,
12
+ existsSync,
13
+ fchmodSync,
14
+ fsyncSync,
15
+ openSync,
16
+ readFileSync,
17
+ renameSync,
18
+ statSync,
19
+ unlinkSync,
20
+ writeFileSync,
21
+ } from 'node:fs'
22
+ import { basename, dirname, join } from 'node:path'
23
+
24
+ export const SEMVER_PATTERN =
25
+ /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/
26
+
27
+ export const MANIFEST_INVENTORY = Object.freeze([
28
+ Object.freeze({
29
+ id: 'root-manifest',
30
+ role: 'manifest',
31
+ file: 'package.json',
32
+ kind: 'json',
33
+ name: 'pantheon-opencode',
34
+ valuePath: ['name'],
35
+ }),
36
+ Object.freeze({
37
+ id: 'plugin-manifest',
38
+ role: 'manifest',
39
+ file: 'plugin.json',
40
+ kind: 'json',
41
+ name: 'pantheon',
42
+ valuePath: ['name'],
43
+ }),
44
+ Object.freeze({
45
+ id: 'python-manifest',
46
+ role: 'manifest',
47
+ file: 'pyproject.toml',
48
+ kind: 'toml',
49
+ name: 'pantheon',
50
+ valuePath: ['project', 'name'],
51
+ }),
52
+ Object.freeze({
53
+ id: 'tui-manifest',
54
+ role: 'manifest',
55
+ file: 'src/plugins/tui/package.json',
56
+ kind: 'json',
57
+ name: 'pantheon-tui',
58
+ valuePath: ['name'],
59
+ }),
60
+ Object.freeze({
61
+ id: 'root-lock',
62
+ role: 'lock',
63
+ file: 'package-lock.json',
64
+ kind: 'json',
65
+ name: 'pantheon-opencode',
66
+ packageFile: 'package.json',
67
+ }),
68
+ Object.freeze({
69
+ id: 'tui-lock',
70
+ role: 'lock',
71
+ file: 'src/plugins/tui/package-lock.json',
72
+ kind: 'json',
73
+ name: 'pantheon-tui',
74
+ packageFile: 'src/plugins/tui/package.json',
75
+ }),
76
+ ])
77
+
78
+ export const VERSION_MANIFEST_INVENTORY = Object.freeze(
79
+ MANIFEST_INVENTORY.filter(({ role }) => role === 'manifest'),
80
+ )
81
+
82
+ export const LOCK_INVENTORY = Object.freeze(
83
+ MANIFEST_INVENTORY.filter(({ role }) => role === 'lock'),
84
+ )
85
+
86
+ function readToml(file, text) {
87
+ const result = execFileSync(
88
+ 'python3',
89
+ ['-c', 'import json,sys,tomllib; print(json.dumps(tomllib.loads(sys.stdin.read())))'],
90
+ { input: text, encoding: 'utf8' },
91
+ )
92
+ let parsed
93
+ try {
94
+ parsed = JSON.parse(result)
95
+ } catch (error) {
96
+ throw new Error(`invalid TOML in ${file}: parser returned invalid JSON (${error.message})`)
97
+ }
98
+ if (!parsed || typeof parsed !== 'object') throw new Error(`invalid TOML in ${file}`)
99
+ return parsed
100
+ }
101
+
102
+ function readDocument(root, entry) {
103
+ const path = join(root, entry.file)
104
+ if (!existsSync(path)) throw new Error(`manifest inventory file is missing: ${entry.file}`)
105
+ const text = readFileSync(path, 'utf8')
106
+ if (entry.kind === 'toml') return { entry, path, text, data: readToml(entry.file, text) }
107
+ let data
108
+ try {
109
+ data = JSON.parse(text)
110
+ } catch (error) {
111
+ throw new Error(`invalid JSON in ${entry.file}: ${error.message}`)
112
+ }
113
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
114
+ throw new Error(`invalid JSON document in ${entry.file}`)
115
+ }
116
+ return { entry, path, text, data }
117
+ }
118
+
119
+ function valueAt(data, path) {
120
+ return path.reduce((value, key) => value?.[key], data)
121
+ }
122
+
123
+ function assertSemver(version, label) {
124
+ if (typeof version !== 'string' || !SEMVER_PATTERN.test(version)) {
125
+ throw new Error(`${label} must be a valid semver`)
126
+ }
127
+ }
128
+
129
+ function manifestVersion(document) {
130
+ const { entry, data } = document
131
+ const version =
132
+ entry.role === 'manifest'
133
+ ? (data.version ?? valueAt(data, ['project', 'version']))
134
+ : data.version
135
+ if (typeof version !== 'string' || version.length === 0) {
136
+ throw new Error(`${entry.file} is missing version`)
137
+ }
138
+ assertSemver(version, `${entry.file} version`)
139
+ return version
140
+ }
141
+
142
+ function manifestName(document) {
143
+ const { entry, data } = document
144
+ const name = entry.role === 'manifest' ? valueAt(data, entry.valuePath) : data.name
145
+ if (name !== entry.name) {
146
+ throw new Error(`${entry.file} name must be ${entry.name}`)
147
+ }
148
+ return name
149
+ }
150
+
151
+ function validateLock(document, manifestsByFile, allowVersionDivergence) {
152
+ const { entry, data } = document
153
+ if (!Number.isInteger(data.lockfileVersion) || data.lockfileVersion < 1) {
154
+ throw new Error(`${entry.file} has an invalid lockfileVersion`)
155
+ }
156
+ if (data.name !== entry.name) throw new Error(`${entry.file} name must be ${entry.name}`)
157
+ assertSemver(data.version, `${entry.file} version`)
158
+ const manifest = manifestsByFile.get(entry.packageFile)
159
+ if (!manifest) throw new Error(`${entry.file} has no inventory manifest partner`)
160
+ const manifestVersionValue = manifestVersion(manifest)
161
+ if (!allowVersionDivergence && data.version !== manifestVersionValue) {
162
+ throw new Error(`${entry.file} version diverges from ${entry.packageFile}`)
163
+ }
164
+ const rootPackage = data.packages?.['']
165
+ if (!rootPackage || typeof rootPackage !== 'object' || Array.isArray(rootPackage)) {
166
+ throw new Error(`${entry.file} is missing packages[""]`)
167
+ }
168
+ if (rootPackage.name !== entry.name) {
169
+ throw new Error(`${entry.file} packages[""] name must be ${entry.name}`)
170
+ }
171
+ assertSemver(rootPackage.version, `${entry.file} packages[""] version`)
172
+ if (data.version !== rootPackage.version) {
173
+ throw new Error(`${entry.file} top-level version diverges from packages[""]`)
174
+ }
175
+ if (!allowVersionDivergence && rootPackage.version !== manifestVersionValue) {
176
+ throw new Error(`${entry.file} packages[""] version diverges from ${entry.packageFile}`)
177
+ }
178
+ }
179
+
180
+ /** Read and validate every entry before a caller is allowed to write anything. */
181
+ export function validateInventory(root, { allowVersionDivergence = false } = {}) {
182
+ const documents = MANIFEST_INVENTORY.map((entry) => readDocument(root, entry))
183
+ const manifestsByFile = new Map(
184
+ documents
185
+ .filter(({ entry }) => entry.role === 'manifest')
186
+ .map((document) => [document.entry.file, document]),
187
+ )
188
+ const versions = documents
189
+ .filter(({ entry }) => entry.role === 'manifest')
190
+ .map((document) => ({ file: document.entry.file, version: manifestVersion(document) }))
191
+ documents.filter(({ entry }) => entry.role === 'manifest').forEach(manifestName)
192
+ const sourceVersion = versions[0].version
193
+ if (!allowVersionDivergence) {
194
+ for (const item of versions) {
195
+ if (item.version !== sourceVersion)
196
+ throw new Error(`${item.file} version diverges from package.json`)
197
+ }
198
+ }
199
+ documents
200
+ .filter(({ entry }) => entry.role === 'lock')
201
+ .forEach((document) => {
202
+ manifestName({
203
+ ...document,
204
+ entry: { ...document.entry, role: 'manifest', valuePath: ['name'] },
205
+ })
206
+ validateLock(document, manifestsByFile, allowVersionDivergence)
207
+ })
208
+ return { documents, sourceVersion, versions }
209
+ }
210
+
211
+ export function entryVersion(document) {
212
+ return document.entry.role === 'manifest' ? manifestVersion(document) : document.data.version
213
+ }
214
+
215
+ function skipJsonWhitespace(text, offset) {
216
+ let cursor = offset
217
+ while (cursor < text.length && /\s/.test(text[cursor])) cursor += 1
218
+ return cursor
219
+ }
220
+
221
+ function jsonStringEnd(text, start) {
222
+ if (text[start] !== '"') throw new Error('JSON string expected')
223
+ for (let cursor = start + 1; cursor < text.length; cursor += 1) {
224
+ if (text[cursor] === '\\') {
225
+ cursor += 1
226
+ continue
227
+ }
228
+ if (text[cursor] === '"') return cursor
229
+ }
230
+ throw new Error('unterminated JSON string')
231
+ }
232
+
233
+ function jsonValueEnd(text, start, path, targetPath, ranges) {
234
+ const cursor = skipJsonWhitespace(text, start)
235
+ if (
236
+ path.length === targetPath.length &&
237
+ path.every((part, index) => part === targetPath[index])
238
+ ) {
239
+ const end = jsonStringEnd(text, cursor)
240
+ ranges.push({ start: cursor + 1, end })
241
+ return end + 1
242
+ }
243
+
244
+ if (text[cursor] === '"') return jsonStringEnd(text, cursor) + 1
245
+ if (text[cursor] === '{') {
246
+ let next = skipJsonWhitespace(text, cursor + 1)
247
+ if (text[next] === '}') return next + 1
248
+ while (next < text.length) {
249
+ const keyEnd = jsonStringEnd(text, next)
250
+ const key = JSON.parse(text.slice(next, keyEnd + 1))
251
+ next = skipJsonWhitespace(text, keyEnd + 1)
252
+ if (text[next] !== ':') throw new Error('JSON object colon expected')
253
+ next = jsonValueEnd(text, next + 1, [...path, key], targetPath, ranges)
254
+ next = skipJsonWhitespace(text, next)
255
+ if (text[next] === '}') return next + 1
256
+ if (text[next] !== ',') throw new Error('JSON object separator expected')
257
+ next = skipJsonWhitespace(text, next + 1)
258
+ }
259
+ }
260
+ if (text[cursor] === '[') {
261
+ let next = skipJsonWhitespace(text, cursor + 1)
262
+ let index = 0
263
+ if (text[next] === ']') return next + 1
264
+ while (next < text.length) {
265
+ next = jsonValueEnd(text, next, [...path, String(index)], targetPath, ranges)
266
+ index += 1
267
+ next = skipJsonWhitespace(text, next)
268
+ if (text[next] === ']') return next + 1
269
+ if (text[next] !== ',') throw new Error('JSON array separator expected')
270
+ next = skipJsonWhitespace(text, next + 1)
271
+ }
272
+ }
273
+
274
+ let next = cursor
275
+ while (next < text.length && !',]}'.includes(text[next])) next += 1
276
+ return next
277
+ }
278
+
279
+ function replaceJsonStringValues(text, paths, version, file) {
280
+ const ranges = []
281
+ for (const path of paths) jsonValueEnd(text, 0, [], path, ranges)
282
+ if (ranges.length !== paths.length)
283
+ throw new Error(`${file} is missing writable version metadata`)
284
+
285
+ let updated = text
286
+ for (const range of ranges.sort((left, right) => right.start - left.start)) {
287
+ updated = `${updated.slice(0, range.start)}${version}${updated.slice(range.end)}`
288
+ }
289
+ return updated
290
+ }
291
+
292
+ function replaceTomlProjectVersion(text, version, file) {
293
+ const projectHeader = /^\[project\]\s*$/m.exec(text)
294
+ if (!projectHeader) throw new Error(`${file} is missing [project] metadata`)
295
+ const projectStart = projectHeader.index + projectHeader[0].length
296
+ const nextTable = text.slice(projectStart).search(/^\s*\[/m)
297
+ const projectEnd = nextTable === -1 ? text.length : projectStart + nextTable
298
+ const project = text.slice(projectStart, projectEnd)
299
+ const versionPattern = /^(\s*version\s*=\s*)(["'])([^"'\r\n]+)\2([^\r\n]*)$/m
300
+ const match = versionPattern.exec(project)
301
+ if (!match) throw new Error(`${file} is missing project version metadata`)
302
+ const replacement = `${match[1]}${match[2]}${version}${match[2]}${match[4]}`
303
+ return (
304
+ text.slice(0, projectStart) +
305
+ project.replace(versionPattern, replacement) +
306
+ text.slice(projectEnd)
307
+ )
308
+ }
309
+
310
+ function prepareDocumentVersion(document, version) {
311
+ const { entry, text } = document
312
+ if (entry.kind === 'toml') return replaceTomlProjectVersion(text, version, entry.file)
313
+ const paths = entry.role === 'lock' ? [['version'], ['packages', '', 'version']] : [['version']]
314
+ return replaceJsonStringValues(text, paths, version, entry.file)
315
+ }
316
+
317
+ /** Prepare all version metadata without changing any file. */
318
+ export function prepareInventoryVersion(inventory, version) {
319
+ assertSemver(version, 'new version')
320
+ if (!inventory || !Array.isArray(inventory.documents))
321
+ throw new Error('validated inventory is required')
322
+ return inventory.documents.map((document) => ({
323
+ path: document.path,
324
+ content: prepareDocumentVersion(document, version),
325
+ }))
326
+ }
327
+
328
+ function temporaryPath(path, label) {
329
+ return join(
330
+ dirname(path),
331
+ `.${basename(path)}.${label}.${process.pid}.${randomBytes(8).toString('hex')}`,
332
+ )
333
+ }
334
+
335
+ function cleanup(paths) {
336
+ for (const path of paths) {
337
+ try {
338
+ unlinkSync(path)
339
+ } catch (error) {
340
+ if (error.code !== 'ENOENT') throw error
341
+ }
342
+ }
343
+ }
344
+
345
+ /** Commit prepared files as one rollback-safe temp+rename transaction. */
346
+ export function commitPreparedUpdates(updates) {
347
+ const unique = new Set()
348
+ const prepared = []
349
+ try {
350
+ for (const { path, content } of updates) {
351
+ if (unique.has(path)) throw new Error(`duplicate prepared update: ${path}`)
352
+ unique.add(path)
353
+ if (typeof content !== 'string') throw new Error(`prepared content is not text: ${path}`)
354
+ const temp = temporaryPath(path, 'tmp')
355
+ try {
356
+ const mode = statSync(path).mode & 0o777
357
+ writeFileSync(temp, content, { encoding: 'utf8', flag: 'wx', mode })
358
+ const fd = openSync(temp, 'r+')
359
+ try {
360
+ fchmodSync(fd, mode)
361
+ fsyncSync(fd)
362
+ } finally {
363
+ closeSync(fd)
364
+ }
365
+ prepared.push({
366
+ path,
367
+ temp,
368
+ backup: temporaryPath(path, 'backup'),
369
+ moved: false,
370
+ installed: false,
371
+ })
372
+ } catch (error) {
373
+ cleanup([temp])
374
+ throw error
375
+ }
376
+ }
377
+ } catch (error) {
378
+ cleanup(prepared.map(({ temp }) => temp))
379
+ throw error
380
+ }
381
+
382
+ try {
383
+ for (const item of prepared) {
384
+ renameSync(item.path, item.backup)
385
+ item.moved = true
386
+ renameSync(item.temp, item.path)
387
+ item.installed = true
388
+ }
389
+ } catch (error) {
390
+ for (const item of [...prepared].reverse()) {
391
+ try {
392
+ if (item.installed) unlinkSync(item.path)
393
+ if (item.moved) renameSync(item.backup, item.path)
394
+ } catch {
395
+ // Preserve the original commit error; a failed rollback is still
396
+ // surfaced by the post-write validation performed by the caller.
397
+ }
398
+ }
399
+ cleanup(prepared.map(({ temp }) => temp))
400
+ cleanup(prepared.map(({ backup }) => backup))
401
+ throw error
402
+ }
403
+
404
+ cleanup(prepared.map(({ backup }) => backup))
405
+ }
406
+
407
+ /** Update only version metadata in the already validated inventory. */
408
+ export function writeInventoryVersion(inventory, version) {
409
+ commitPreparedUpdates(prepareInventoryVersion(inventory, version))
410
+ }