pantheon-opencode 1.4.3 → 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.
Files changed (118) hide show
  1. package/.pantheon/code-mode/README.md +93 -0
  2. package/.pantheon/code-mode/checkpoint-session.py +7 -0
  3. package/.pantheon/code-mode/checkpoint-session.sh +11 -0
  4. package/.pantheon/code-mode/checkpoint_session.py +481 -0
  5. package/.pantheon/code-mode/compress-inline.py +450 -0
  6. package/.pantheon/code-mode/eval-llm-judge.py +255 -0
  7. package/.pantheon/code-mode/eval-monte-carlo.py +368 -0
  8. package/.pantheon/code-mode/eval-run.py +197 -0
  9. package/.pantheon/code-mode/eval-static.py +321 -0
  10. package/.pantheon/code-mode/example-sync.sh +5 -0
  11. package/.pantheon/code-mode/manifest.json +21 -0
  12. package/.pantheon/code-mode/session-end-save.py +186 -0
  13. package/.pantheon/code-mode/session-end-save.sh +18 -0
  14. package/.pantheon/code-mode/test-allowed.sh +5 -0
  15. package/.pantheon/code-mode/test-auto-continue-checkpoint.sh +179 -0
  16. package/.pantheon/code-mode/test-env.sh +7 -0
  17. package/.pantheon/code-mode/test-prlimit.sh +9 -0
  18. package/.pantheon/code-mode/test-timeout.sh +6 -0
  19. package/CHANGELOG.md +1026 -0
  20. package/README.md +292 -1080
  21. package/README.pt-BR.md +308 -0
  22. package/bin/pantheon-init.mjs +207 -31
  23. package/opencode.json +1 -0
  24. package/package.json +34 -14
  25. package/plugin.json +15 -0
  26. package/pyproject.toml +65 -0
  27. package/scripts/_pantheon_paths.py +48 -7
  28. package/scripts/ci-validate-yaml.py +7 -2
  29. package/scripts/code_mode_server.py +451 -44
  30. package/scripts/cost.mjs +30 -14
  31. package/scripts/doctor.mjs +306 -129
  32. package/scripts/generate-preset-docs.mjs +127 -66
  33. package/scripts/generate-prompts.sh +2 -2
  34. package/scripts/install/cli-ui.mjs +23 -10
  35. package/scripts/install/config-migration.mjs +399 -0
  36. package/scripts/install/health-check.mjs +3 -3
  37. package/scripts/install/model-picker.mjs +72 -23
  38. package/scripts/install/opencode-version.mjs +47 -0
  39. package/scripts/install/opencode.mjs +396 -121
  40. package/scripts/install/plugin.mjs +89 -49
  41. package/scripts/install/shared.mjs +55 -159
  42. package/scripts/install/strings.mjs +129 -0
  43. package/scripts/install-mcp.mjs +45 -232
  44. package/scripts/manifest-inventory.mjs +410 -0
  45. package/scripts/mcp_persistence_server.py +798 -60
  46. package/scripts/mcp_resources_server.py +144 -40
  47. package/scripts/memory_mcp_server.py +2 -2
  48. package/scripts/package-evidence.mjs +242 -0
  49. package/scripts/probe-context-rehydrate.mjs +240 -0
  50. package/scripts/probe-pantheon-cost.mjs +313 -0
  51. package/scripts/recover-zenodo-deposition.mjs +146 -0
  52. package/scripts/release-beta-version.mjs +56 -60
  53. package/scripts/sync-artifacts.mjs +169 -0
  54. package/scripts/sync-tui.mjs +135 -42
  55. package/scripts/test-opencode-v1-v2-sandbox.sh +862 -0
  56. package/scripts/uninstall.mjs +298 -954
  57. package/scripts/validate-zenodo-release.mjs +154 -0
  58. package/scripts/validation-policy.mjs +7 -7
  59. package/scripts/version-check.mjs +113 -108
  60. package/scripts/versioning.mjs +230 -86
  61. package/scripts/zenodo-artifact.mjs +21 -0
  62. package/scripts/zenodo-release-state.mjs +53 -0
  63. package/src/agents/mnemosyne.md +1 -1
  64. package/src/agents/zeus.md +27 -16
  65. package/src/mcp/_pantheon_paths.py +48 -7
  66. package/src/mcp/code_mode_server.py +451 -44
  67. package/src/mcp/eval_store.py +1 -1
  68. package/src/mcp/mcp_persistence_server.py +798 -60
  69. package/src/mcp/mcp_resources_server.py +144 -40
  70. package/src/mcp/pantheon_vision_server.py +1 -1
  71. package/src/mcp/toon_codec.py +491 -0
  72. package/src/pantheon/auto-wake.ts +1 -5
  73. package/src/pantheon/background-job-board.ts +110 -2
  74. package/src/pantheon/compaction-handoff.ts +304 -0
  75. package/src/pantheon/cost-command.ts +356 -69
  76. package/src/pantheon/delegate-manager.ts +766 -0
  77. package/src/pantheon/delegation-classifier.ts +307 -0
  78. package/src/pantheon/delegation-enforce.ts +13 -12
  79. package/src/pantheon/delegation-finalize.ts +406 -0
  80. package/src/pantheon/delegation-notify.ts +212 -0
  81. package/src/pantheon/delegation-toast.ts +73 -0
  82. package/src/pantheon/delegation.ts +1337 -0
  83. package/src/pantheon/file-persistence-salvage.ts +64 -0
  84. package/src/pantheon/file-persistence.ts +194 -45
  85. package/src/pantheon/goal-loop.ts +2 -2
  86. package/src/pantheon/goal-store.ts +1 -1
  87. package/src/pantheon/hashline/tool.ts +2 -2
  88. package/src/pantheon/model-command.ts +2 -1
  89. package/src/pantheon/native-probe.ts +234 -0
  90. package/src/pantheon/native-task-status.ts +139 -0
  91. package/src/pantheon/permission-eval.ts +284 -0
  92. package/src/pantheon/provider-capabilities.ts +221 -0
  93. package/src/pantheon/provider-stream-tracker.ts +182 -0
  94. package/src/pantheon/shared-board.ts +37 -0
  95. package/src/pantheon/step-cap.ts +110 -0
  96. package/src/pantheon/task-result-guard.ts +107 -0
  97. package/src/pantheon/token-opt.ts +689 -0
  98. package/src/pantheon/tool-ceiling.ts +159 -0
  99. package/src/pantheon/toon-codec.ts +481 -0
  100. package/src/pantheon/tui-version-check.ts +1 -3
  101. package/src/pantheon/v2-bridge.ts +129 -0
  102. package/src/pantheon/v2-events.ts +148 -0
  103. package/src/pantheon/v2-hooks.ts +287 -0
  104. package/src/pantheon/v2-tool-definitions.ts +331 -0
  105. package/src/pantheon/zeus-delegate-with-retry.ts +406 -0
  106. package/src/plugin-v2/index.ts +21 -0
  107. package/src/plugin-v2.ts +824 -0
  108. package/src/plugin.ts +261 -51
  109. package/src/plugins/pantheon-hooks.ts +51 -0
  110. package/src/plugins/tui/dist/tui.d.ts +116 -24
  111. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  112. package/src/plugins/tui/dist/tui.js +416 -149
  113. package/src/plugins/tui/dist/tui.tsx +456 -87
  114. package/src/plugins/tui/package-lock.json +51 -51
  115. package/src/plugins/tui/package.json +7 -7
  116. package/src/plugins/tui/src/index.tsx +462 -95
  117. package/src/skills/README.md +0 -2
  118. package/src/pantheon/tool-context.ts +0 -41
@@ -6,45 +6,46 @@
6
6
  * recommend Analyze commits and suggest next version bump type
7
7
  * apply [type] Bump manifests + move [Unreleased] → [vX.Y.Z] in CHANGELOG
8
8
  * type: patch | minor | major | auto (default: auto)
9
+ * --beta: bump the committed beta line instead
10
+ * beta Alias for `apply --beta`
9
11
  * changelog [ver] (Internal) Insert a versioned section into CHANGELOG
10
12
  * Normally called by `apply`; can be run standalone.
11
13
  * status Show current version, latest tag, and pending bump type
12
14
  *
13
- * Design: the release signal is "package.json version > latest git tag".
14
- * Developers (or AI agents) call `apply` to bump + update CHANGELOG, then push.
15
- * The auto-release workflow detects the version bump and creates the release.
16
- * No version bumping ever happens inside GitHub Actions.
15
+ * Design: the release signal is "package.json version > latest git tag" for
16
+ * stable, and "package.json version is a committed X.Y.Z-beta.N" for beta.
17
+ * Developers (or AI agents) call `apply` (or `apply --beta` / `beta`) to bump
18
+ * the manifests and promote CHANGELOG.md, then push. The dispatch-only release
19
+ * workflow reads the committed version and creates the release. No version
20
+ * bumping ever happens inside GitHub Actions.
17
21
  */
18
22
 
19
- import { execSync } from 'node:child_process'
20
- import { readFileSync, writeFileSync } from 'node:fs'
21
- import { dirname, join } from 'node:path'
23
+ import { execFileSync } from 'node:child_process'
24
+ import { existsSync, readFileSync } from 'node:fs'
25
+ import { basename, dirname, join } from 'node:path'
22
26
  import { fileURLToPath } from 'node:url'
27
+ import {
28
+ commitPreparedUpdates,
29
+ prepareInventoryVersion,
30
+ SEMVER_PATTERN,
31
+ validateInventory,
32
+ } from './manifest-inventory.mjs'
33
+ import { nextBetaVersion } from './release-beta-version.mjs'
23
34
  import { collectEntries, groupCommits, parseCommitLine, renderChangelog } from './release-notes.mjs'
24
35
 
25
36
  const __dirname = dirname(fileURLToPath(import.meta.url))
26
37
  const ROOT = join(__dirname, '..')
27
38
 
28
- const MANIFEST_FILES = [
29
- // 'platform/forge.json' removed in v1.0 — directory no longer exists
30
- 'pyproject.toml',
31
- 'package.json',
32
- 'plugin.json',
33
- 'src/plugins/tui/package.json',
34
- ]
35
-
36
39
  const CHANGELOG_PATH = join(ROOT, 'CHANGELOG.md')
40
+ const ZENODO_PATH = join(ROOT, '.zenodo.json')
41
+ const CITATION_PATH = join(ROOT, 'CITATION.cff')
37
42
 
38
43
  // ---------------------------------------------------------------------------
39
44
  // Git helpers
40
45
  // ---------------------------------------------------------------------------
41
46
 
42
- function run(cmd) {
43
- try {
44
- return execSync(cmd, { cwd: ROOT, encoding: 'utf-8' }).trim()
45
- } catch {
46
- return ''
47
- }
47
+ function runGit(args) {
48
+ return execFileSync('git', args, { cwd: ROOT, encoding: 'utf8' }).trim()
48
49
  }
49
50
 
50
51
  function getLatestTag() {
@@ -52,12 +53,15 @@ function getLatestTag() {
52
53
  // The loose glob `v[0-9]*.[0-9]*.[0-9]*` ALSO matches v1.2.0-beta.9.*
53
54
  // (the trailing `*` swallows the -beta suffix), which would poison the
54
55
  // version gate — so filter strictly before sorting.
55
- const tag = run("git tag -l 'v*' | grep -E '^v[0-9]+\\.[0-9]+\\.[0-9]+$' | sort -V | tail -1")
56
- return tag || 'v0.0.0'
56
+ const tags = runGit(['tag', '-l', 'v*'])
57
+ .split('\n')
58
+ .filter((tag) => /^v\d+\.\d+\.\d+$/.test(tag))
59
+ tags.sort((left, right) => compareStable(left.slice(1), right.slice(1)))
60
+ return tags.at(-1) || 'v0.0.0'
57
61
  }
58
62
 
59
63
  function getCurrentVersion() {
60
- return JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8')).version
64
+ return validateInventory(ROOT).sourceVersion
61
65
  }
62
66
 
63
67
  // ---------------------------------------------------------------------------
@@ -65,7 +69,10 @@ function getCurrentVersion() {
65
69
  // ---------------------------------------------------------------------------
66
70
 
67
71
  function bumpVersion(version, type) {
68
- const [major, minor, patch] = version.split('.').map(Number)
72
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?$/.exec(version)
73
+ if (!match) throw new Error(`cannot bump invalid semver: ${version}`)
74
+ if (!['major', 'minor', 'patch'].includes(type)) throw new Error(`invalid bump type: ${type}`)
75
+ const [major, minor, patch] = match.slice(1).map(Number)
69
76
  switch (type) {
70
77
  case 'major':
71
78
  return `${major + 1}.0.0`
@@ -76,8 +83,17 @@ function bumpVersion(version, type) {
76
83
  }
77
84
  }
78
85
 
86
+ function compareStable(left, right) {
87
+ const a = left.split('.').map(Number)
88
+ const b = right.split('.').map(Number)
89
+ for (let index = 0; index < 3; index += 1) {
90
+ if (a[index] !== b[index]) return a[index] - b[index]
91
+ }
92
+ return 0
93
+ }
94
+
79
95
  function analyzeConventionalCommits(since) {
80
- const log = run(`git log ${since}..HEAD --format="%s"`)
96
+ const log = runGit(['log', `${since}..HEAD`, '--format=%s'])
81
97
  if (!log) return 'patch'
82
98
  let bump = 'patch'
83
99
  for (const msg of log.split('\n').filter(Boolean)) {
@@ -91,27 +107,94 @@ function analyzeConventionalCommits(since) {
91
107
  // Manifest updater
92
108
  // ---------------------------------------------------------------------------
93
109
 
94
- function updateManifests(newVersion) {
95
- for (const file of MANIFEST_FILES) {
96
- const path = join(ROOT, file)
97
- try {
98
- const raw = readFileSync(path, 'utf-8')
99
- if (file.endsWith('.toml')) {
100
- // TOML: replace version = "X.Y.Z"
101
- const updated = raw.replace(/^(version\s*=\s*")[^"]+(")/m, `$1${newVersion}$2`)
102
- writeFileSync(path, updated)
103
- console.log(` ✓ ${file} → ${newVersion}`)
104
- } else {
105
- // JSON
106
- const content = JSON.parse(raw)
107
- content.version = newVersion
108
- writeFileSync(path, `${JSON.stringify(content, null, 2)}\n`)
109
- console.log(` ✓ ${file} → ${newVersion}`)
110
+ function prepareManifests(newVersion) {
111
+ if (!SEMVER_PATTERN.test(newVersion)) throw new Error(`invalid version: ${newVersion}`)
112
+ const inventory = validateInventory(ROOT, { allowVersionDivergence: true })
113
+ return { inventory, updates: prepareInventoryVersion(inventory, newVersion) }
114
+ }
115
+
116
+ /**
117
+ * Serialize `.zenodo.json` the way Biome 2.x formats it: objects stay expanded
118
+ * (matching `JSON.stringify(value, null, 2)`), while arrays containing only
119
+ * primitives are collapsed onto one line when they fit within the line width.
120
+ * A plain `JSON.stringify` emits the `keywords` array multiline, which Biome
121
+ * collapses — reintroducing a lint failure on the next release bump.
122
+ *
123
+ * @param {unknown} value parsed JSON document
124
+ * @param {number} [lineWidth] formatter line width (matches biome.json)
125
+ * @returns {string} Biome-compatible JSON with a trailing newline
126
+ */
127
+ function formatZenodoJson(value, lineWidth = 100) {
128
+ const indent = (depth) => ' '.repeat(depth)
129
+ const render = (node, depth, column) => {
130
+ if (Array.isArray(node)) {
131
+ if (node.length === 0) return '[]'
132
+ const primitiveOnly = node.every((item) => item === null || typeof item !== 'object')
133
+ if (primitiveOnly) {
134
+ const inline = `[${node.map((item) => JSON.stringify(item)).join(', ')}]`
135
+ if (column + inline.length <= lineWidth) return inline
110
136
  }
111
- } catch {
112
- console.log(` ⚠ ${file} not found — skipped`)
137
+ const body = node
138
+ .map((item) => `${indent(depth + 1)}${render(item, depth + 1, indent(depth + 1).length)}`)
139
+ .join(',\n')
140
+ return `[\n${body}\n${indent(depth)}]`
141
+ }
142
+ if (node !== null && typeof node === 'object') {
143
+ const entries = Object.entries(node)
144
+ if (entries.length === 0) return '{}'
145
+ const body = entries
146
+ .map(([key, item]) => {
147
+ const keyPrefix = `${indent(depth + 1)}${JSON.stringify(key)}: `
148
+ return `${keyPrefix}${render(item, depth + 1, keyPrefix.length)}`
149
+ })
150
+ .join(',\n')
151
+ return `{\n${body}\n${indent(depth)}}`
113
152
  }
153
+ return JSON.stringify(node)
114
154
  }
155
+ return `${render(value, 0, 0)}\n`
156
+ }
157
+
158
+ /**
159
+ * Keep the Zenodo deposition metadata (.zenodo.json) and the citation file
160
+ * (CITATION.cff) aligned with the version and date being released. Zenodo's
161
+ * release validator hard-fails when CITATION.cff version differs from the
162
+ * release tag, so these must be bumped together with the manifests.
163
+ */
164
+ function prepareReleaseMetadata(newVersion, dateStr) {
165
+ if (!SEMVER_PATTERN.test(newVersion)) throw new Error(`invalid version: ${newVersion}`)
166
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) throw new Error(`invalid date: ${dateStr}`)
167
+ const tagUrl = `https://github.com/ils15/pantheon-opencode/releases/tag/v${newVersion}`
168
+ const updates = []
169
+
170
+ if (existsSync(ZENODO_PATH)) {
171
+ const zenodo = JSON.parse(readFileSync(ZENODO_PATH, 'utf8'))
172
+ const zenodoPrevious = typeof zenodo.version === 'string' ? zenodo.version : null
173
+ if (zenodoPrevious && zenodoPrevious !== newVersion && typeof zenodo.description === 'string') {
174
+ zenodo.description = zenodo.description.replaceAll(zenodoPrevious, newVersion)
175
+ }
176
+ zenodo.version = newVersion
177
+ zenodo.publication_date = dateStr
178
+ zenodo.url = tagUrl
179
+ updates.push({ path: ZENODO_PATH, content: formatZenodoJson(zenodo) })
180
+ }
181
+
182
+ if (existsSync(CITATION_PATH)) {
183
+ const citationRaw = readFileSync(CITATION_PATH, 'utf8')
184
+ const citationMatch = /^version:\s*["']?([^"'\s]+)["']?\s*$/m.exec(citationRaw)
185
+ const citationPrevious = citationMatch ? citationMatch[1] : null
186
+ let citation = citationRaw
187
+ if (citationPrevious && citationPrevious !== newVersion) {
188
+ citation = citation.replaceAll(`Pantheon v${citationPrevious}`, `Pantheon v${newVersion}`)
189
+ }
190
+ citation = citation
191
+ .replace(/^version:.*$/m, `version: ${newVersion}`)
192
+ .replace(/^date-released:.*$/m, `date-released: ${dateStr}`)
193
+ .replace(/^url:.*$/m, `url: ${tagUrl}`)
194
+ updates.push({ path: CITATION_PATH, content: citation })
195
+ }
196
+
197
+ return updates
115
198
  }
116
199
 
117
200
  // ---------------------------------------------------------------------------
@@ -181,15 +264,17 @@ function stripEmptySections(body) {
181
264
  return keep.join('\n').replace(/\n{3,}/g, '\n\n')
182
265
  }
183
266
 
184
- function promoteUnreleased(newVersion, dateStr, notesBody = null) {
267
+ function prepareUnreleased(newVersion, dateStr, notesBody = null) {
268
+ if (!SEMVER_PATTERN.test(newVersion)) throw new Error(`invalid changelog version: ${newVersion}`)
269
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) throw new Error(`invalid changelog date: ${dateStr}`)
270
+ if (notesBody !== null && typeof notesBody !== 'string') {
271
+ throw new Error('generated release notes must be text')
272
+ }
185
273
  const content = readFileSync(CHANGELOG_PATH, 'utf-8')
186
274
 
187
275
  const unreleasedHeader = '## [Unreleased]'
188
276
  const idx = content.indexOf(unreleasedHeader)
189
- if (idx === -1) {
190
- console.log(' ⚠ [Unreleased] section not found in CHANGELOG — skipping')
191
- return false
192
- }
277
+ if (idx === -1) throw new Error('CHANGELOG.md is missing the [Unreleased] section')
193
278
 
194
279
  // Find the end of [Unreleased]: next ## header or end of file
195
280
  const afterHeader = idx + unreleasedHeader.length
@@ -211,15 +296,10 @@ function promoteUnreleased(newVersion, dateStr, notesBody = null) {
211
296
  .map((l) => l.trim())
212
297
  .filter((l) => l && !l.startsWith('###') && !l.startsWith('## '))
213
298
 
214
- if (notesBody === null && realLines.length === 0) {
215
- console.log(' ℹ [Unreleased] is empty — CHANGELOG not modified')
216
- return false
217
- }
299
+ if (notesBody === null && realLines.length === 0) return { changed: false, content }
218
300
 
219
301
  if (notesBody !== null && realLines.length > 0) {
220
- console.log(
221
- ' ⚠ --notes replaces the manual [Unreleased] content — review the diff before committing',
222
- )
302
+ console.log(' --notes replaces the manual [Unreleased] content')
223
303
  }
224
304
 
225
305
  // Strip lines that are just empty subsections (### X or ## 🆕/🐞/⚠️/✅
@@ -239,13 +319,35 @@ function promoteUnreleased(newVersion, dateStr, notesBody = null) {
239
319
 
240
320
  const updated = `${before + unreleasedHeader + newTemplate}\n\n${newVersionHeader}${writtenBody}${after}`
241
321
 
242
- writeFileSync(CHANGELOG_PATH, updated)
243
- console.log(
244
- notesBody !== null
245
- ? ` ✓ CHANGELOG: [Unreleased] → [v${newVersion}] (notes generated from commits)`
246
- : ` ✓ CHANGELOG: [Unreleased] → [v${newVersion}]`,
247
- )
248
- return true
322
+ if (!updated.includes('## [Unreleased]') || !updated.includes(newVersionHeader)) {
323
+ throw new Error('prepared CHANGELOG.md failed structural validation')
324
+ }
325
+ return { changed: true, content: updated, notesGenerated: notesBody !== null }
326
+ }
327
+
328
+ function contentHasUnreleased() {
329
+ return readFileSync(CHANGELOG_PATH, 'utf8').includes('## [Unreleased]')
330
+ }
331
+
332
+ function commitPreparedRelease(manifestPlan, changelogPlan, metadataUpdates = []) {
333
+ const updates = [...manifestPlan.updates, ...metadataUpdates]
334
+ if (changelogPlan.changed) {
335
+ updates.push({ path: CHANGELOG_PATH, content: changelogPlan.content })
336
+ }
337
+ commitPreparedUpdates(updates)
338
+ for (const document of manifestPlan.inventory.documents) {
339
+ console.log(` ✓ ${document.entry.file} → ${manifestPlan.version}`)
340
+ }
341
+ for (const { path } of metadataUpdates) {
342
+ console.log(` ✓ ${basename(path)} → ${manifestPlan.version}`)
343
+ }
344
+ if (changelogPlan.changed) {
345
+ console.log(
346
+ changelogPlan.notesGenerated
347
+ ? ` ✓ CHANGELOG: [Unreleased] → [v${manifestPlan.version}] (notes generated from commits)`
348
+ : ` ✓ CHANGELOG: [Unreleased] → [v${manifestPlan.version}]`,
349
+ )
350
+ }
249
351
  }
250
352
 
251
353
  // ---------------------------------------------------------------------------
@@ -282,9 +384,35 @@ switch (command) {
282
384
  break
283
385
  }
284
386
 
387
+ case 'beta':
285
388
  case 'apply': {
286
389
  const args = process.argv.slice(3)
287
390
  const useNotes = args.includes('--notes')
391
+ const useBeta = command === 'beta' || args.includes('--beta')
392
+
393
+ // Beta: advance the committed X.Y.Z-beta.N line and promote the
394
+ // [Unreleased] CHANGELOG section into [vX.Y.Z-beta.N], exactly like the
395
+ // stable path. The committed manifest is the single source of truth — no
396
+ // npm lookup, no git lookup, and no runtime version computation.
397
+ if (useBeta) {
398
+ if (!contentHasUnreleased())
399
+ throw new Error('CHANGELOG.md is missing the [Unreleased] section')
400
+ const current = getCurrentVersion()
401
+ const newVersion = nextBetaVersion(current)
402
+ const date = new Date().toISOString().slice(0, 10)
403
+ console.log(`Bumping ${current} → ${newVersion} (beta)`)
404
+ const changelogPlan = prepareUnreleased(newVersion, date)
405
+ const manifestPlan = prepareManifests(newVersion)
406
+ commitPreparedRelease(
407
+ { ...manifestPlan, version: newVersion },
408
+ changelogPlan,
409
+ prepareReleaseMetadata(newVersion, date),
410
+ )
411
+ console.log(`\nDone. Commit the version inventory and CHANGELOG as v${newVersion}.`)
412
+ console.log(`Tag v${newVersion} will be created by the release workflow after merge to main.`)
413
+ break
414
+ }
415
+
288
416
  const type = args.find((a) => !a.startsWith('--')) || 'auto'
289
417
  const latestTag = getLatestTag()
290
418
  const current = getCurrentVersion()
@@ -298,23 +426,25 @@ switch (command) {
298
426
  const since = latestTag === 'v0.0.0' ? null : latestTag
299
427
  const raw = collectEntries({ since, draft: since === null })
300
428
  const body = renderChangelog(groupCommits(raw.map(parseCommitLine)))
301
- if (!body) {
302
- console.log(' ⚠ --notes: no groupable commits — manual [Unreleased] content used')
303
- return null
304
- }
305
- console.log(` ℹ --notes: release notes generated from ${raw.length} commits`)
429
+ if (!body) return null
430
+ console.log(` --notes generated from ${raw.length} commits`)
306
431
  return body
307
432
  }
308
433
 
309
- // If package.json is already ahead of the latest tag, someone bumped
310
- // without tagging — warn and use the current version as-is. Tags are
311
- // workflow-owned, so there is nothing to create here.
434
+ if (!contentHasUnreleased()) throw new Error('CHANGELOG.md is missing the [Unreleased] section')
312
435
  if (current !== latestVer) {
313
- console.log(`⚠ package.json (${current}) already ahead of latest tag (${latestTag}).`)
314
- console.log(` Syncing all manifests to ${current} and promoting CHANGELOG.`)
315
- updateManifests(current)
436
+ console.log(
437
+ `package.json (${current}) is ahead of latest tag (${latestTag}); synchronizing inventory.`,
438
+ )
316
439
  const date = new Date().toISOString().slice(0, 10)
317
- promoteUnreleased(current, date, generateNotes())
440
+ const notesBody = generateNotes()
441
+ const changelogPlan = prepareUnreleased(current, date, notesBody)
442
+ const manifestPlan = prepareManifests(current)
443
+ commitPreparedRelease(
444
+ { ...manifestPlan, version: current },
445
+ changelogPlan,
446
+ prepareReleaseMetadata(current, date),
447
+ )
318
448
  console.log(`Tag v${current} will be created by the release workflow after merge to main.`)
319
449
  break
320
450
  }
@@ -324,13 +454,15 @@ switch (command) {
324
454
  const date = new Date().toISOString().slice(0, 10)
325
455
 
326
456
  console.log(`Bumping ${current} → ${newVersion} (${bumpType})`)
327
- updateManifests(newVersion)
328
- promoteUnreleased(newVersion, date, generateNotes())
329
- console.log(`\nDone. Commit with: git add -A && git commit -m "chore(release): v${newVersion}"`)
330
-
331
- // Tags are owned by the release workflow — never create them locally.
332
- // A local tag on a pre-merge commit drifts from the merged main state
333
- // (the untagged-v1.2.1 root cause) and would desync the version gate.
457
+ const notesBody = generateNotes()
458
+ const changelogPlan = prepareUnreleased(newVersion, date, notesBody)
459
+ const manifestPlan = prepareManifests(newVersion)
460
+ commitPreparedRelease(
461
+ { ...manifestPlan, version: newVersion },
462
+ changelogPlan,
463
+ prepareReleaseMetadata(newVersion, date),
464
+ )
465
+ console.log(`\nDone. Commit the version inventory and CHANGELOG as v${newVersion}.`)
334
466
  console.log(`Tag v${newVersion} will be created by the release workflow after merge to main.`)
335
467
  break
336
468
  }
@@ -339,7 +471,11 @@ switch (command) {
339
471
  case 'changelog': {
340
472
  const version = arg || getCurrentVersion()
341
473
  const date = new Date().toISOString().slice(0, 10)
342
- promoteUnreleased(version, date)
474
+ const changelogPlan = prepareUnreleased(version, date)
475
+ if (changelogPlan.changed) {
476
+ commitPreparedUpdates([{ path: CHANGELOG_PATH, content: changelogPlan.content }])
477
+ console.log(` ✓ CHANGELOG: [Unreleased] → [v${version}]`)
478
+ }
343
479
  break
344
480
  }
345
481
 
@@ -353,11 +489,19 @@ Commands:
353
489
  type: patch | minor | major | auto (default: auto)
354
490
  --notes: pre-fill the promoted entry with release
355
491
  notes generated from conventional commits
492
+ --beta: advance the committed X.Y.Z-beta.N line and
493
+ promote [Unreleased] → [vX.Y.Z-beta.N] in CHANGELOG
494
+ beta Alias for 'apply --beta'
356
495
  changelog [version] Promote [Unreleased] → [vX.Y.Z] without bumping
357
496
 
358
- Release flow:
497
+ Release flow (stable):
359
498
  1. node scripts/versioning.mjs apply [minor]
360
- 2. git add -A && git commit -m "chore(release): vX.Y.Z"
361
- 3. git push ← CI passes → auto-release fires because pkg > tag
499
+ 2. Commit the version inventory and CHANGELOG.
500
+ 3. Push after review; CI and the dispatch-only release workflow create the tag.
501
+
502
+ Release flow (beta):
503
+ 1. node scripts/versioning.mjs apply --beta (or: node scripts/versioning.mjs beta)
504
+ 2. Commit the version inventory and CHANGELOG.
505
+ 3. Dispatch the release workflow with release_channel=beta.
362
506
  `)
363
507
  }
@@ -0,0 +1,21 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { readFileSync } from 'node:fs'
3
+
4
+ export function sha256File(path) {
5
+ return createHash('sha256').update(readFileSync(path)).digest('hex')
6
+ }
7
+
8
+ export function findZenodoFile(record, filename, checksum) {
9
+ if (!record || typeof record !== 'object' || !Array.isArray(record.files)) {
10
+ throw new Error('Zenodo record did not contain a files array.')
11
+ }
12
+ const matches = record.files.filter((file) => file?.filename === filename)
13
+ if (matches.length > 1) throw new Error(`Zenodo record contains duplicate file: ${filename}.`)
14
+ if (matches.length === 0) return null
15
+ const file = matches[0]
16
+ const expected = `sha256:${checksum}`
17
+ if (file.checksum !== expected) {
18
+ throw new Error(`Zenodo checksum mismatch for ${filename}.`)
19
+ }
20
+ return file
21
+ }
@@ -0,0 +1,53 @@
1
+ const DOI_PATTERN = /^10\.\d{4,9}\/\S+$/
2
+ const MARKER_PATTERN =
3
+ /<!-- zenodo-deposition:(?:id=(\d+);)?(?:doi=([^;\s]+);)?state=(pending|created|published)(?:;claim=([^\s]+))? -->/
4
+
5
+ export function extractZenodoDoi(response) {
6
+ if (!response || typeof response !== 'object') return null
7
+ const candidates = [response.doi, response.metadata?.doi, response.links?.doi]
8
+ for (const candidate of candidates) {
9
+ if (typeof candidate === 'string' && DOI_PATTERN.test(candidate.trim())) return candidate.trim()
10
+ }
11
+ return null
12
+ }
13
+
14
+ export function validateZenodoResponse(response, { requireDoi = false } = {}) {
15
+ if (!response || typeof response !== 'object' || Array.isArray(response)) {
16
+ throw new Error('Zenodo response must be a JSON object.')
17
+ }
18
+ if (!Number.isSafeInteger(response.id) || response.id <= 0) {
19
+ throw new Error('Zenodo response did not contain a valid deposition id.')
20
+ }
21
+ const doi = extractZenodoDoi(response)
22
+ if (requireDoi && !doi) throw new Error('Zenodo response did not contain a valid DOI.')
23
+ return { id: response.id, doi }
24
+ }
25
+
26
+ export function parseZenodoMarker(body) {
27
+ const text = String(body ?? '')
28
+ const match = text.match(MARKER_PATTERN)
29
+ if (!match) {
30
+ const legacy = text.match(/<!-- zenodo-deposition-id:(\d+) -->/)
31
+ if (!legacy) return null
32
+ const id = Number(legacy[1])
33
+ if (!Number.isSafeInteger(id) || id <= 0)
34
+ throw new Error('Zenodo marker has an invalid deposition id.')
35
+ return { id, doi: null, state: 'created', claim: null }
36
+ }
37
+ const id = match[1] ? Number(match[1]) : null
38
+ if (match[1] && (!Number.isSafeInteger(id) || id <= 0))
39
+ throw new Error('Zenodo marker has an invalid deposition id.')
40
+ if (match[2] && !DOI_PATTERN.test(match[2])) throw new Error('Zenodo marker has an invalid DOI.')
41
+ return { id, doi: match[2] ?? null, state: match[3], claim: match[4] ?? null }
42
+ }
43
+
44
+ export function zenodoMarker({ id = null, doi = null, state, claim = null }) {
45
+ if (!['pending', 'created', 'published'].includes(state))
46
+ throw new Error('Zenodo marker state is invalid.')
47
+ if (id !== null && (!Number.isSafeInteger(id) || id <= 0))
48
+ throw new Error('Zenodo marker id is invalid.')
49
+ if (doi !== null && !DOI_PATTERN.test(doi)) throw new Error('Zenodo marker DOI is invalid.')
50
+ if (state !== 'pending' && id === null)
51
+ throw new Error('Created or published Zenodo markers require an id.')
52
+ return `<!-- zenodo-deposition:${id === null ? '' : `id=${id};`}${doi === null ? '' : `doi=${doi};`}state=${state}${claim === null ? '' : `;claim=${claim}`} -->`
53
+ }
@@ -34,7 +34,7 @@ permission:
34
34
  bash: deny
35
35
  read: allow
36
36
  grep: allow
37
- edit: deny
37
+ edit: allow
38
38
  task:
39
39
  "*": deny
40
40
  ---
@@ -144,23 +144,33 @@ task_status(task_id="ses_xxx", wait=true)
144
144
  -> bloqueia ate completar: { state: "completed", task_result: "..." }
145
145
  ```
146
146
 
147
- ### Pantheon Delegation — BackgroundJobBoard (plugin)
147
+ ### Pantheon Delegation Tools (plugin) — 3-tool API
148
148
 
149
- O plugin Pantheon mantém um **BackgroundJobBoard** persistente
150
- (`.pantheon/board/state.json`) que rastreia trabalho em voo e o carrega para o
151
- contexto de compactação. O toolset de delegação do plugin
152
- (`pantheon_delegate`, `pantheon_delegation_read`, `pantheon_delegation_list`)
153
- foi **removido em 1.4.3** — dispatch e coleta usam a API nativa
154
- `task(background=true)` / `task_status(wait=true)`.
149
+ O plugin Pantheon adiciona 3 tools de delegacao gerenciada pelo BackgroundJobBoard:
155
150
 
151
+ ```
152
+ pantheon_delegate({prompt, agent, description?, read_only?})
153
+ -> cria sessao filha, registra no board, retorna alias: [apo-1]
154
+ pantheon_delegation_read({id: "apo-1"})
155
+ -> BLOQUEIA ate o job terminar, retorna o report markdown, marca reconciled
156
+ -> durante a espera, mostra o que o agente esta fazendo: seccao final "## Agent Activity"
157
+ (amostra as messages do child a cada ~2s; tool calls + args truncados / texto)
158
+ pantheon_delegation_list()
159
+ -> lista jobs da sessao, com [unread] para jobs terminados nao lidos
160
+ -> jobs RUNNING mostram "last activity:" (ultima acao visivel do child)
161
+ -> fail-open: sem suporte a messages, formato atual inalterado
162
+ ```
163
+
164
+ - **Read-only enforcement:** agentes read-only (apollo, gaia — `read_only_agents` no routing.yml) tem `edit`, `write`, `bash`, `task` NEGADOS dentro da sessao delegada (o guard de `tool.execute.before` THROW com mensagem acionavel). Use `read_only: true` no delegate para qualquer agente de investigacao.
165
+ - **Depth-2 hard-enforced:** sessao read-only NAO cria subagentes (`task` bloqueado) — investigacao nunca vira arvore.
156
166
  - **Compactacao:** jobs em voo (running + unread terminal) sao injetados no contexto de compactacao via `experimental.session.compacting` — delegacao em andamento nao se perde ao compactar.
157
167
 
158
- ### DELEGATION_RULES (native task path)
168
+ ### DELEGATION_RULES (plugin path)
159
169
 
160
- 1. **NUNCA** polle `task_status` para checar se um job terminou — use `wait=true` para fan-in explicito.
161
- 2. Voce NAO recebe notificacao injetada no chat — politica do usuario: ZERO `task-notification` no transcript. Visibilidade de conclusao: marcador `[unread]` no board, toasts TUI (pantheon-hooks) e carry-forward de compactacao.
162
- 3. Use `task_status({task_id, wait: true})` APENAS para fan-in explicito / recuperacao de resultado sob demanda (bloqueia ate o fim).
163
- 4. `task_status` sem `wait` e para diagnostico, nao para polling.
170
+ 1. **NUNCA** polle `pantheon_delegation_list` para checar se um job terminou.
171
+ 2. Voce NAO recebe notificacao injetada no chat — politica do usuario: ZERO `task-notification` no transcript. Visibilidade de conclusao: marcador `[unread]` em `pantheon_delegation_list`, `pantheon_delegation_read({id})`, toasts TUI (pantheon-hooks) e carry-forward de compactacao.
172
+ 3. Use `pantheon_delegation_read({id})` APENAS para fan-in explicito / recuperacao de resultado sob demanda (bloqueia ate o fim).
173
+ 4. `pantheon_delegation_list` e para diagnostico, nao para polling.
164
174
 
165
175
  ### Background (sempre usar)
166
176
  - **Apollo, Hermes, Aphrodite, Demeter, Hephaestus, Prometheus**
@@ -314,12 +324,13 @@ out = await guard.maybeRetry(result, async () => { ...task() de novo... })
314
324
  Regra: retry 1x APENAS em `empty-mode1`/`empty-mode2`. Resultado com conteudo
315
325
  nunca redispara. Apos 1 retry vazio → escalate (mesma regra do TODO Enforcer).
316
326
 
317
- > **Waves DEVEM usar `dispatch-guard`** (`src/pantheon/dispatch-guard.ts`) — encapsula `task → classify → retry 1x → escalate` com cap duro 1; se ainda vazio throw `ZeusEscalationError` (escale: tente outro agente, simplifique, manual).
318
- > Uso alto-nível: `import { createDispatchGuard } from './pantheon/dispatch-guard.ts'` → `await guard.maybeRetry(result, () => task(...))`.
327
+ > **Waves DEVEM usar `zeusDelegateWithRetry`** (`src/pantheon/zeus-delegate-with-retry.ts`) — encapsula `delegate → waitForTerminal → classify → retry 1x → escalate` com cap duro 1; se ainda vazio throw `ZeusEscalationError` (escale: tente outro agente, simplifique, manual).
328
+ > Uso alto-nível: `import { zeusDelegateWithRetry } from './pantheon/zeus-delegate-with-retry.ts'` → `await zeusDelegateWithRetry({ board, client, sessionID, agent, prompt })`.
329
+ > Uso baixo-nível: `createZeusRetryHelper().executeWithRetry(first, () => secondRead())` — classifica empty e decide retry/escalate (mesmo cap 1).
319
330
 
320
331
  ### /cost — pantheon_cost tool (WIRED no plugin)
321
- `pantheon_cost({ days?: number })` le o `opencode.db` READ-ONLY (node:sqlite;
322
- fallback `scripts/cost.mjs`) e devolve tabela markdown de custo + tokens por
332
+ `pantheon_cost({ days?: number })` le o `opencode.db` READ-ONLY usando o backend
333
+ unico `node:sqlite`; se indisponivel, retorna `UNSUPPORTED`, sem fallback. Devolve tabela markdown de custo + tokens por
323
334
  agente nos ultimos N dias (default 7). Uso: quando Nyx pedir visibilidade de
324
335
  custo, ou antes de escalar tier — decida se o batch valeu o preco.
325
336