pantheon-opencode 1.6.0-beta.10 → 1.6.0-beta.12

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 (71) hide show
  1. package/AGENTS.global.md +42 -18
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +46 -0
  4. package/README.md +122 -38
  5. package/README.pt-BR.md +5 -4
  6. package/bin/pantheon-init.mjs +322 -101
  7. package/package.json +7 -6
  8. package/plugin.json +1 -1
  9. package/pyproject.toml +1 -1
  10. package/scripts/build-agents-md.mjs +128 -10
  11. package/scripts/check-plugin-v2-coverage.mjs +7 -7
  12. package/scripts/doctor.mjs +242 -19
  13. package/scripts/install/backup.mjs +648 -0
  14. package/scripts/install/cli-ui.mjs +79 -1
  15. package/scripts/install/exit-codes.mjs +26 -0
  16. package/scripts/install/file-inventory.mjs +555 -0
  17. package/scripts/install/interactive.mjs +15 -1
  18. package/scripts/install/migrate.mjs +32 -6
  19. package/scripts/install/model-picker.mjs +63 -22
  20. package/scripts/install/opencode-version.mjs +9 -1
  21. package/scripts/install/opencode.mjs +761 -63
  22. package/scripts/install/plan-artifact.mjs +517 -0
  23. package/scripts/install/plan.mjs +449 -0
  24. package/scripts/install/plugin.mjs +136 -18
  25. package/scripts/install/shared.mjs +249 -94
  26. package/scripts/install/state.mjs +63 -8
  27. package/scripts/install/strings.mjs +20 -0
  28. package/scripts/install/venv.mjs +25 -6
  29. package/scripts/install/verify.mjs +128 -0
  30. package/scripts/install/vision-prompt.mjs +13 -1
  31. package/scripts/install-mcp.mjs +1 -1
  32. package/scripts/manifest-inventory.mjs +49 -3
  33. package/scripts/mcp_persistence.py +50 -15
  34. package/scripts/node-engine-policy.mjs +74 -0
  35. package/scripts/package-evidence.mjs +6 -0
  36. package/scripts/postinstall.mjs +3 -4
  37. package/scripts/prune.mjs +9 -3
  38. package/scripts/reconcile.mjs +325 -0
  39. package/scripts/restore.mjs +222 -0
  40. package/scripts/uninstall.mjs +258 -34
  41. package/scripts/update.mjs +164 -0
  42. package/scripts/validate-node-engine-policy.mjs +13 -0
  43. package/scripts/version-check.mjs +34 -1
  44. package/scripts/versioning.mjs +9 -1
  45. package/skills-lock.json +4 -4
  46. package/src/agents/iris.md +6 -0
  47. package/src/agents/mnemosyne.md +7 -2
  48. package/src/agents/zeus.md +19 -62
  49. package/src/instructions/agent-return-format.instructions.md +17 -15
  50. package/src/instructions/memory-protocol.instructions.md +3 -2
  51. package/src/instructions/test-economy.instructions.md +21 -0
  52. package/src/instructions/zeus-anti-stall.instructions.md +6 -6
  53. package/src/instructions/zeus-council-synthesis.instructions.md +10 -11
  54. package/src/instructions/zeus-memory-operations.instructions.md +11 -6
  55. package/src/instructions/zeus-timeout-retry.instructions.md +28 -9
  56. package/src/mcp/mcp_persistence.py +50 -15
  57. package/src/pantheon/cost-command.ts +5 -2
  58. package/src/pantheon/model-agents.ts +19 -0
  59. package/src/pantheon/model-command.ts +218 -56
  60. package/src/pantheon/v2-model-overrides.ts +275 -0
  61. package/src/pantheon/v2-tools.ts +18 -29
  62. package/src/pantheon/v2-unsupported.mjs +2 -3
  63. package/src/plugin-v2.ts +13 -0
  64. package/src/plugins/tui/dist/tui-v2.js +2 -2
  65. package/src/plugins/tui/dist/tui.js +1 -1
  66. package/src/plugins/tui/package-lock.json +3 -3
  67. package/src/plugins/tui/package.json +2 -2
  68. package/src/skills/artifact-management/SKILL.md +5 -1
  69. package/src/skills/auto-continue/SKILL.md +14 -6
  70. package/src/skills/memory-bank/SKILL.md +5 -1
  71. package/src/skills/worktrees/SKILL.md +30 -11
@@ -4,8 +4,15 @@
4
4
 
5
5
  import { execSync } from 'node:child_process'
6
6
  import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs'
7
- import { isAbsolute, join, resolve } from 'node:path'
7
+ import { dirname, isAbsolute, join, resolve } from 'node:path'
8
+ import { ACTIONS, recordChange, recordRemovalTree, removeReviewedTree, snapshot } from './plan.mjs'
8
9
  import { ROOT, writeIfChanged } from './shared.mjs'
10
+ import {
11
+ assertNoSymlinkAncestors,
12
+ assertPathWithin,
13
+ assertSafeRemoval,
14
+ verifyPathRemoved,
15
+ } from './verify.mjs'
9
16
 
10
17
  /**
11
18
  * Copy a plugin's runtime payload (the root TUI loader, bundled dist/*,
@@ -17,18 +24,38 @@ import { ROOT, writeIfChanged } from './shared.mjs'
17
24
  * @param {object} [options]
18
25
  * @param {boolean} [options.dryRun=false] - If true, don't write files
19
26
  * @param {'v1'|'v2'} [options.version='v1'] - Host plugin generation
27
+ * @param {(change: object) => void} [options.recorder] - optional change recorder for preview
28
+ * @param {Set<string>|null} [options.reviewedRemovals] - paths approved by the current plan
29
+ * @param {Map<string, object>|null} [options.reviewedSnapshots] - pre-apply path snapshots
20
30
  * @returns {{ created: number, skipped: number }}
21
31
  */
22
- export function copyPluginFiles(srcDir, dstDir, { dryRun = false, version = 'v1' } = {}) {
32
+ export function copyPluginFiles(
33
+ srcDir,
34
+ dstDir,
35
+ {
36
+ dryRun = false,
37
+ version = 'v1',
38
+ recorder = null,
39
+ reviewedRemovals = null,
40
+ reviewedSnapshots = null,
41
+ } = {},
42
+ ) {
23
43
  const result = { created: 0, skipped: 0 }
44
+ const distDir = join(dstDir, 'dist')
24
45
 
25
- if (!dryRun) mkdirSync(dstDir, { recursive: true })
26
- if (!dryRun) mkdirSync(join(dstDir, 'dist'), { recursive: true })
46
+ if (!dryRun) {
47
+ assertNoSymlinkAncestors(dstDir)
48
+ mkdirSync(dstDir, { recursive: true })
49
+ assertNoSymlinkAncestors(dstDir)
50
+ assertPathWithin(dstDir, distDir)
51
+ assertNoSymlinkAncestors(distDir)
52
+ mkdirSync(distDir, { recursive: true })
53
+ assertNoSymlinkAncestors(distDir)
54
+ }
27
55
 
28
56
  // Drop legacy raw copies from previous installs (root index.tsx + dist/tui.tsx).
29
- if (!dryRun) {
30
- rmSync(join(dstDir, 'index.tsx'), { force: true })
31
- rmSync(join(dstDir, 'dist', 'tui.tsx'), { force: true })
57
+ for (const legacyPath of [join(dstDir, 'index.tsx'), join(distDir, 'tui.tsx')]) {
58
+ removeLegacyFile(legacyPath, dstDir, { dryRun, recorder, reviewedRemovals, reviewedSnapshots })
32
59
  }
33
60
 
34
61
  // Copy dist/* files
@@ -36,7 +63,9 @@ export function copyPluginFiles(srcDir, dstDir, { dryRun = false, version = 'v1'
36
63
  if (existsSync(distSrc)) {
37
64
  for (const f of readdirSync(distSrc)) {
38
65
  const c = readFileSync(join(distSrc, f), 'utf8')
39
- const status = writeIfChanged(join(dstDir, 'dist', f), c, dryRun)
66
+ const destination = join(distDir, f)
67
+ assertPathWithin(dstDir, destination)
68
+ const status = writeIfChanged(destination, c, dryRun, { recorder })
40
69
  if (status === 'created') result.created++
41
70
  else result.skipped++
42
71
  }
@@ -53,6 +82,7 @@ export function copyPluginFiles(srcDir, dstDir, { dryRun = false, version = 'v1'
53
82
  join(dstDir, 'package.json'),
54
83
  `${JSON.stringify(packageJson, null, 2)}\n`,
55
84
  dryRun,
85
+ { recorder },
56
86
  )
57
87
  if (pkgStatus === 'created') result.created++
58
88
  else result.skipped++
@@ -65,6 +95,7 @@ export function copyPluginFiles(srcDir, dstDir, { dryRun = false, version = 'v1'
65
95
  join(dstDir, 'tui.ts'),
66
96
  readFileSync(tuiEntrySrc, 'utf8'),
67
97
  dryRun,
98
+ { recorder },
68
99
  )
69
100
  if (entryStatus === 'created') result.created++
70
101
  else result.skipped++
@@ -77,11 +108,17 @@ export function copyPluginFiles(srcDir, dstDir, { dryRun = false, version = 'v1'
77
108
  v2ServerDestination,
78
109
  readFileSync(v2ServerEntry, 'utf8'),
79
110
  dryRun,
111
+ { recorder },
80
112
  )
81
113
  if (entryStatus === 'created') result.created++
82
114
  else result.skipped++
83
- } else if (!dryRun) {
84
- rmSync(v2ServerDestination, { force: true })
115
+ } else {
116
+ removeLegacyFile(v2ServerDestination, dstDir, {
117
+ dryRun,
118
+ recorder,
119
+ reviewedRemovals,
120
+ reviewedSnapshots,
121
+ })
85
122
  }
86
123
 
87
124
  // Copy the lockfile so dependency installation is reproducible. A missing
@@ -92,6 +129,7 @@ export function copyPluginFiles(srcDir, dstDir, { dryRun = false, version = 'v1'
92
129
  join(dstDir, 'package-lock.json'),
93
130
  readFileSync(lockSrc, 'utf8'),
94
131
  dryRun,
132
+ { recorder },
95
133
  )
96
134
  if (lockStatus === 'created') result.created++
97
135
  else result.skipped++
@@ -100,6 +138,49 @@ export function copyPluginFiles(srcDir, dstDir, { dryRun = false, version = 'v1'
100
138
  return result
101
139
  }
102
140
 
141
+ function removeLegacyFile(
142
+ filePath,
143
+ rootDir,
144
+ { dryRun, recorder, reviewedRemovals, reviewedSnapshots },
145
+ ) {
146
+ assertSafeRemoval(rootDir, filePath)
147
+ const present = existsSync(filePath)
148
+ if (dryRun) {
149
+ recordChange(
150
+ {
151
+ path: filePath,
152
+ action: present ? ACTIONS.REMOVE : ACTIONS.UNCHANGED,
153
+ bytes: 0,
154
+ scored: false,
155
+ },
156
+ recorder,
157
+ )
158
+ return
159
+ }
160
+ if (!present) return
161
+
162
+ const normalizedPath = resolve(filePath)
163
+ if (reviewedRemovals && !reviewedRemovals.has(normalizedPath)) {
164
+ throw new Error(`Refusing unreviewed plugin removal: ${filePath}`)
165
+ }
166
+ const expected = reviewedSnapshots?.get(normalizedPath)
167
+ if (reviewedSnapshots && (!expected || !sameSnapshot(snapshot(filePath), expected))) {
168
+ throw new Error(`Plugin removal target drifted after review: ${filePath}`)
169
+ }
170
+ rmSync(filePath, { force: true })
171
+ verifyPathRemoved(filePath)
172
+ recordChange({ path: filePath, action: ACTIONS.REMOVE, bytes: 0, scored: false }, recorder)
173
+ }
174
+
175
+ function sameSnapshot(left, right) {
176
+ return (
177
+ left.exists === right.exists &&
178
+ left.sha256 === right.sha256 &&
179
+ left.size === right.size &&
180
+ left.mode === right.mode
181
+ )
182
+ }
183
+
103
184
  /**
104
185
  * Install a plugin from source to destination.
105
186
  * @param {string} srcDir - Source plugin directory (e.g. ROOT/src/plugins/tui)
@@ -107,22 +188,55 @@ export function copyPluginFiles(srcDir, dstDir, { dryRun = false, version = 'v1'
107
188
  * @param {object} [options]
108
189
  * @param {boolean} [options.dryRun=false] - If true, don't write files
109
190
  * @param {boolean} [options.clean=false] - If true, remove existing before install
191
+ * @param {(change: object) => void} [options.recorder] - optional change recorder for preview
110
192
  * @returns {{ created: number, skipped: number, errors: number }}
111
193
  */
112
194
  export function installPlugin(
113
195
  srcDir,
114
196
  dstDir,
115
- { dryRun = false, clean = false, version = 'v1' } = {},
197
+ {
198
+ dryRun = false,
199
+ clean = false,
200
+ version = 'v1',
201
+ recorder = null,
202
+ reviewedRemovals = null,
203
+ reviewedSnapshots = null,
204
+ } = {},
116
205
  ) {
117
206
  const result = { created: 0, skipped: 0, errors: 0 }
207
+ const cleanTargetExisted = clean && existsSync(dstDir)
118
208
 
119
- if (clean && existsSync(dstDir) && !dryRun) {
120
- rmSync(dstDir, { recursive: true, force: true })
209
+ assertNoSymlinkAncestors(dstDir)
210
+ if (cleanTargetExisted) {
211
+ assertSafeRemoval(dirname(dstDir), dstDir)
212
+ if (dryRun) {
213
+ recordRemovalTree(dstDir, recorder)
214
+ } else {
215
+ removeReviewedTree(dstDir, reviewedRemovals, reviewedSnapshots)
216
+ }
121
217
  }
122
218
 
123
- const copyResult = copyPluginFiles(srcDir, dstDir, { dryRun, version })
219
+ const copyResult = copyPluginFiles(srcDir, dstDir, {
220
+ dryRun,
221
+ version,
222
+ recorder,
223
+ reviewedRemovals,
224
+ reviewedSnapshots,
225
+ })
124
226
  result.created += copyResult.created
125
227
  result.skipped += copyResult.skipped
228
+ if (cleanTargetExisted) {
229
+ // The clean pass removes these directories before rebuilding their files;
230
+ // keep the net plan truthful while the separate removal review still lists
231
+ // every path actually cleared.
232
+ recordChange({ path: dstDir, action: ACTIONS.UNCHANGED, bytes: 0, scored: false }, recorder)
233
+ if (existsSync(join(srcDir, 'dist'))) {
234
+ recordChange(
235
+ { path: join(dstDir, 'dist'), action: ACTIONS.UNCHANGED, bytes: 0, scored: false },
236
+ recorder,
237
+ )
238
+ }
239
+ }
126
240
 
127
241
  // Install plugin dependencies (@opentui/core, @opentui/solid, solid-js).
128
242
  // Lockfile installation is the only accepted dependency path.
@@ -150,9 +264,10 @@ export function installPluginDependencies(pluginDir, { dryRun = false } = {}) {
150
264
  * @param {string} pluginId - Plugin identifier string
151
265
  * @param {object} [options]
152
266
  * @param {boolean} [options.dryRun=false]
267
+ * @param {(change: object) => void} [options.recorder] - optional change recorder for preview
153
268
  * @returns {'created'|'skipped'}
154
269
  */
155
- export function registerPlugin(tuiJsonPath, pluginId, { dryRun = false } = {}) {
270
+ export function registerPlugin(tuiJsonPath, pluginId, { dryRun = false, recorder = null } = {}) {
156
271
  let tuiConfig = { plugin: [] }
157
272
  if (existsSync(tuiJsonPath)) {
158
273
  try {
@@ -168,7 +283,7 @@ export function registerPlugin(tuiJsonPath, pluginId, { dryRun = false } = {}) {
168
283
  tuiConfig.plugin.push(pluginId)
169
284
  }
170
285
  const tuiContent = `${JSON.stringify(tuiConfig, null, 2)}\n`
171
- return writeIfChanged(tuiJsonPath, tuiContent, dryRun)
286
+ return writeIfChanged(tuiJsonPath, tuiContent, dryRun, { recorder })
172
287
  }
173
288
 
174
289
  /**
@@ -256,12 +371,14 @@ export function staleTuiRefs(pluginArray, knownPantheonPaths = []) {
256
371
  * @param {string} pluginId - Plugin to unregister
257
372
  * @param {object} [options]
258
373
  * @param {boolean} [options.dryRun=false]
374
+ * @param {string[]} [options.knownPantheonPaths=[]]
375
+ * @param {(change: object) => void} [options.recorder] - optional change recorder for preview
259
376
  * @returns {'created'|'skipped'}
260
377
  */
261
378
  export function unregisterPlugin(
262
379
  tuiJsonPath,
263
380
  pluginId,
264
- { dryRun = false, knownPantheonPaths = [] } = {},
381
+ { dryRun = false, knownPantheonPaths = [], recorder = null } = {},
265
382
  ) {
266
383
  if (!existsSync(tuiJsonPath)) {
267
384
  // Nothing to clean — and never create an empty tui.json in a location
@@ -292,5 +409,6 @@ export function unregisterPlugin(
292
409
  return !isPantheonTuiRef(ref, knownPantheonPaths)
293
410
  })
294
411
  const tuiContent = `${JSON.stringify(tuiConfig, null, 2)}\n`
295
- return writeIfChanged(tuiJsonPath, tuiContent, dryRun)
412
+ // Stale-ref cleanup is not part of the install summary, so record it unscored.
413
+ return writeIfChanged(tuiJsonPath, tuiContent, dryRun, { recorder, scored: false })
296
414
  }