pantheon-opencode 1.6.0-beta.10 → 1.6.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 (46) hide show
  1. package/AGENTS.global.md +42 -18
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +24 -0
  4. package/README.md +66 -21
  5. package/bin/pantheon-init.mjs +44 -1
  6. package/package.json +4 -4
  7. package/plugin.json +1 -1
  8. package/pyproject.toml +1 -1
  9. package/scripts/build-agents-md.mjs +60 -6
  10. package/scripts/install/backup.mjs +475 -0
  11. package/scripts/install/cli-ui.mjs +6 -1
  12. package/scripts/install/exit-codes.mjs +26 -0
  13. package/scripts/install/interactive.mjs +15 -1
  14. package/scripts/install/migrate.mjs +41 -22
  15. package/scripts/install/opencode.mjs +562 -50
  16. package/scripts/install/plan.mjs +287 -0
  17. package/scripts/install/plugin.mjs +22 -8
  18. package/scripts/install/shared.mjs +89 -55
  19. package/scripts/install/strings.mjs +20 -0
  20. package/scripts/install/venv.mjs +13 -3
  21. package/scripts/install/vision-prompt.mjs +11 -1
  22. package/scripts/mcp_persistence.py +50 -15
  23. package/scripts/prune.mjs +9 -3
  24. package/scripts/restore.mjs +222 -0
  25. package/skills-lock.json +1 -1
  26. package/src/agents/iris.md +6 -0
  27. package/src/agents/zeus.md +19 -62
  28. package/src/instructions/agent-return-format.instructions.md +17 -15
  29. package/src/instructions/memory-protocol.instructions.md +3 -2
  30. package/src/instructions/test-economy.instructions.md +21 -0
  31. package/src/instructions/zeus-anti-stall.instructions.md +6 -6
  32. package/src/instructions/zeus-council-synthesis.instructions.md +10 -11
  33. package/src/instructions/zeus-memory-operations.instructions.md +10 -6
  34. package/src/instructions/zeus-timeout-retry.instructions.md +28 -9
  35. package/src/mcp/mcp_persistence.py +50 -15
  36. package/src/pantheon/cost-command.ts +5 -2
  37. package/src/pantheon/model-agents.ts +19 -0
  38. package/src/pantheon/model-command.ts +218 -56
  39. package/src/pantheon/v2-model-overrides.ts +275 -0
  40. package/src/pantheon/v2-tools.ts +18 -29
  41. package/src/plugin-v2.ts +13 -0
  42. package/src/plugins/tui/dist/tui-v2.js +2 -2
  43. package/src/plugins/tui/dist/tui.js +1 -1
  44. package/src/plugins/tui/package-lock.json +2 -2
  45. package/src/plugins/tui/package.json +1 -1
  46. package/src/skills/auto-continue/SKILL.md +14 -6
@@ -47,6 +47,16 @@ const TABLE = {
47
47
  prereqMissing:
48
48
  'runtime prerequisites missing — install the tools above, or re-run with --no-mcp to install without Python MCP servers',
49
49
  v2Migrating: 'Migrating config to V2 native format...',
50
+ nothingApplied: 'Nothing applied.',
51
+ planOnlyNoYes: 'Plan only — re-run with --yes to apply.',
52
+ driftHeader: 'State changed since preview — aborting before any write:',
53
+ driftAbort: 'No files were written. Re-run to re-review.',
54
+ applyFailed: (msg) => `❌ Apply failed: ${msg}`,
55
+ alreadyApplied: 'Paths already applied — no automatic rollback:',
56
+ restoreRecoveryHint: (dir) => `Recover with: pantheon-opencode restore (backup at ${dir})`,
57
+ restoreNoBackup:
58
+ 'No backup was created (the failure occurred before the first write); the target is unchanged.',
59
+ dryRunNothingWritten: 'Preview only — no files were written.',
50
60
  // summary (printSummary)
51
61
  summaryInstalled: (target) => `OpenCode installed in ${target}`,
52
62
  summaryWithErrors: (target) => `Installation finished WITH ERRORS in ${target}`,
@@ -95,6 +105,16 @@ const TABLE = {
95
105
  prereqMissing:
96
106
  'pré-requisitos do runtime ausentes — instale as ferramentas acima, ou rode de novo com --no-mcp para instalar sem os MCPs Python',
97
107
  v2Migrating: 'Migrando config para o formato nativo V2...',
108
+ nothingApplied: 'Nada foi aplicado.',
109
+ planOnlyNoYes: 'Apenas o plano — rode de novo com --yes para aplicar.',
110
+ driftHeader: 'O estado mudou desde o preview — abortando antes de qualquer escrita:',
111
+ driftAbort: 'Nenhum arquivo foi escrito. Rode de novo para revisar.',
112
+ applyFailed: (msg) => `❌ Falha ao aplicar: ${msg}`,
113
+ alreadyApplied: 'Caminhos já aplicados — sem rollback automático:',
114
+ restoreRecoveryHint: (dir) => `Recupere com: pantheon-opencode restore (backup em ${dir})`,
115
+ restoreNoBackup:
116
+ 'Nenhum backup foi criado (a falha ocorreu antes da primeira escrita); o alvo está intacto.',
117
+ dryRunNothingWritten: 'Somente prévia — nenhum arquivo foi escrito.',
98
118
  // summary (printSummary)
99
119
  summaryInstalled: (target) => `OpenCode instalado em ${target}`,
100
120
  summaryWithErrors: (target) => `Instalação concluída COM ERROS em ${target}`,
@@ -14,6 +14,8 @@ import { existsSync, rmSync } from 'node:fs'
14
14
  import { dirname, join } from 'node:path'
15
15
  import { fileURLToPath } from 'node:url'
16
16
 
17
+ import { ACTIONS, recordChange } from './plan.mjs'
18
+
17
19
  const __dirname = dirname(fileURLToPath(import.meta.url))
18
20
  const ROOT = join(__dirname, '..', '..')
19
21
 
@@ -40,10 +42,13 @@ export function venvPythonPath(target) {
40
42
  * Set up a Python virtual environment and install MCP dependencies.
41
43
  *
42
44
  * @param {string} target - Target installation directory (e.g. ~/.config/opencode)
43
- * @param {{ dryRun?: boolean, skipInstall?: boolean, force?: boolean }} [options]
45
+ * @param {{ dryRun?: boolean, skipInstall?: boolean, force?: boolean, recorder?: (change: object) => void }} [options]
44
46
  * @returns {{ venvPath: string, python: string }}
45
47
  */
46
- export function setupVenv(target, { dryRun = false, skipInstall = false, force = false } = {}) {
48
+ export function setupVenv(
49
+ target,
50
+ { dryRun = false, skipInstall = false, force = false, recorder = null } = {},
51
+ ) {
47
52
  const venvPath = join(target, '.venv')
48
53
  const pythonBin = venvPythonPath(target)
49
54
 
@@ -57,7 +62,8 @@ export function setupVenv(target, { dryRun = false, skipInstall = false, force =
57
62
  }
58
63
 
59
64
  // Step 1b: Create venv if not exists
60
- if (!existsSync(pythonBin)) {
65
+ const venvExists = existsSync(pythonBin)
66
+ if (!venvExists) {
61
67
  if (!dryRun) {
62
68
  console.log(' Creating .venv...')
63
69
  const result = spawnSync('python3', ['-m', 'venv', venvPath], {
@@ -69,8 +75,12 @@ export function setupVenv(target, { dryRun = false, skipInstall = false, force =
69
75
  }
70
76
  }
71
77
  console.log(' ✅ .venv created')
78
+ // A single synthetic entry for the venv directory — never enumerate its
79
+ // thousands of files. Unscored: the install summary does not count it.
80
+ recordChange({ path: venvPath, action: ACTIONS.CREATE, bytes: 0, scored: false }, recorder)
72
81
  } else {
73
82
  console.log(' ⏭️ .venv already exists')
83
+ recordChange({ path: venvPath, action: ACTIONS.UNCHANGED, bytes: 0, scored: false }, recorder)
74
84
  }
75
85
 
76
86
  // Step 2: Install MCP dependencies
@@ -16,7 +16,7 @@
16
16
  * The write mirrors the `/pantheon-vision` command: atomic, `.bak` backup,
17
17
  * and a MERGE — existing keys are never overwritten.
18
18
  */
19
- import { existsSync, mkdirSync, readFileSync } from 'node:fs'
19
+ import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs'
20
20
  import { homedir } from 'node:os'
21
21
  import { dirname, join } from 'node:path'
22
22
  import { writeConfigWithBackup } from './shared.mjs'
@@ -81,6 +81,16 @@ export function mergeVisionModel(
81
81
  const status = writeConfigWithBackup(configPath, content, false, {
82
82
  expectedContent: existingRaw,
83
83
  beforeRecheck,
84
+ // This file is GLOBAL and lives outside the config dir, so the target-scoped
85
+ // exact backup set never captures it. Keep its own `<file>.bak` recovery.
86
+ beforeSwap: (existing) => {
87
+ if (existing === null) return
88
+ try {
89
+ copyFileSync(configPath, `${configPath}.bak`)
90
+ } catch {
91
+ // Best-effort: a backup failure must not block the write.
92
+ }
93
+ },
84
94
  })
85
95
  return { path: configPath, wrote: status === 'created', reason: status }
86
96
  }
@@ -914,15 +914,35 @@ def _validate_delegations(delegations: object) -> None:
914
914
 
915
915
  def _validate_tail(tail: object, payload_bytes: int | None = None) -> list[str]:
916
916
  """Validate bounded serialized tail lines."""
917
- if not isinstance(tail, list) or len(tail) > MAX_TAIL_ITEMS:
918
- raise ValueError("tail exceeds its size limit")
917
+ if not isinstance(tail, list):
918
+ raise ValueError("tail must be an array")
919
+ if len(tail) > MAX_TAIL_ITEMS:
920
+ raise ValueError(
921
+ f"tail has {len(tail)} items; maximum is {MAX_TAIL_ITEMS}; "
922
+ "remove tail entries or omit tail"
923
+ )
919
924
  for line in tail:
920
- if not isinstance(line, str) or len(line.encode()) > MAX_TAIL_LINE_LENGTH:
921
- raise ValueError("tail entries exceed their size limit")
925
+ if not isinstance(line, str):
926
+ raise ValueError("tail entries must be strings")
927
+ line_bytes = len(line.encode())
928
+ if line_bytes > MAX_TAIL_LINE_LENGTH:
929
+ raise ValueError(
930
+ f"tail entries are {line_bytes:,} UTF-8 bytes; maximum is "
931
+ f"{MAX_TAIL_LINE_LENGTH:,}; remove or shorten tail entries, "
932
+ "or omit tail"
933
+ )
922
934
  if payload_bytes is not None and payload_bytes > MAX_CONTEXT_CONTENT_BYTES:
923
- raise ValueError("tail payload exceeds its size limit")
924
- if sum(len(line.encode()) for line in tail) > MAX_CONTEXT_CONTENT_BYTES:
925
- raise ValueError("tail payload exceeds its size limit")
935
+ raise ValueError(
936
+ f"tail payload is {payload_bytes:,} UTF-8 bytes; maximum is "
937
+ f"{MAX_CONTEXT_CONTENT_BYTES:,}; remove tail entries or omit tail"
938
+ )
939
+ tail_bytes = sum(len(line.encode()) for line in tail)
940
+ if tail_bytes > MAX_CONTEXT_CONTENT_BYTES:
941
+ raise ValueError(
942
+ f"tail contains {tail_bytes:,} UTF-8 bytes across strings; maximum "
943
+ f"is {MAX_CONTEXT_CONTENT_BYTES:,} bytes; remove tail entries or "
944
+ "omit tail"
945
+ )
926
946
  return tail
927
947
 
928
948
 
@@ -1020,8 +1040,14 @@ def _collect_context_content_errors(
1020
1040
  if not isinstance(content, str):
1021
1041
  errors.shape(prefix, "a string", content)
1022
1042
  return
1023
- if len(content.encode()) > MAX_CONTEXT_CONTENT_BYTES:
1024
- errors.add(prefix, f"exceeds the {MAX_CONTEXT_CONTENT_BYTES}-byte limit")
1043
+ content_bytes = len(content.encode())
1044
+ if content_bytes > MAX_CONTEXT_CONTENT_BYTES:
1045
+ errors.add(
1046
+ prefix,
1047
+ f"is {content_bytes:,} UTF-8 bytes; maximum is "
1048
+ f"{MAX_CONTEXT_CONTENT_BYTES:,} bytes. Reduce content or omit "
1049
+ "optional tail if it contributes",
1050
+ )
1025
1051
  return
1026
1052
  try:
1027
1053
  parsed = json.loads(content)
@@ -1030,11 +1056,10 @@ def _collect_context_content_errors(
1030
1056
  # it as a checkpoint. It is still bounded above.
1031
1057
  return
1032
1058
  if isinstance(parsed, list) and key == "tail":
1033
- if len(parsed) > MAX_TAIL_ITEMS or any(
1034
- not isinstance(line, str) or len(line.encode()) > MAX_TAIL_LINE_LENGTH
1035
- for line in parsed
1036
- ):
1037
- errors.add(f"{prefix}.tail", "exceeds its size limit")
1059
+ try:
1060
+ _validate_tail(parsed)
1061
+ except ValueError as exc:
1062
+ errors.reroot(prefix, str(exc))
1038
1063
  return
1039
1064
  if not isinstance(parsed, dict):
1040
1065
  # The message already names 'content': do not prepend the path again.
@@ -1164,6 +1189,11 @@ def _upsert_context_entry(
1164
1189
  name="context_save",
1165
1190
  description="Save a context checkpoint for a session/phase. "
1166
1191
  "Stores structured JSON in persistence KV with a key-specific TTL. "
1192
+ "The serialized content is limited to 65,536 UTF-8 bytes. An included "
1193
+ "tail may contain at most 100 strings, each at most 4,096 UTF-8 bytes, "
1194
+ "with at most 65,536 UTF-8 bytes across all tail strings. Oversized "
1195
+ "content or tail is rejected, never truncated; remove or omit optional "
1196
+ "tail data when applicable. "
1167
1197
  "content is a JSON STRING; when it parses to an object, 'goal', 'phase', "
1168
1198
  "'delegations' and 'heartbeat' must each be JSON OBJECTS and 'tail' must "
1169
1199
  "be a JSON ARRAY. 'phase' is an object {current,total,name} — NOT a phase "
@@ -1180,7 +1210,12 @@ async def context_save(
1180
1210
  str,
1181
1211
  Field(
1182
1212
  description=(
1183
- "JSON checkpoint string. When it parses to an object, 'goal', "
1213
+ "JSON checkpoint string, limited to 65,536 UTF-8 bytes. An "
1214
+ "included 'tail' may contain at most 100 strings, each at "
1215
+ "most 4,096 UTF-8 bytes, with at most 65,536 UTF-8 bytes "
1216
+ "across all tail strings. Oversized input is rejected, never "
1217
+ "truncated; remove or omit optional 'tail' when applicable. "
1218
+ "When it parses to an object, 'goal', "
1184
1219
  "'phase', 'delegations' and 'heartbeat' must be objects and "
1185
1220
  "'tail' must be an array; all are optional. 'phase' is an "
1186
1221
  "object {current,total,name}, not a counter. Example: "
package/scripts/prune.mjs CHANGED
@@ -9,7 +9,10 @@
9
9
  * SAFETY: prune NEVER touches the active config, the active venv, the package
10
10
  * repo, or the installed package. It only:
11
11
  * - lists old global `pantheon` installs (suggests `npm uninstall -g pantheon`)
12
- * - removes stale `opencode.json.bak*` backups (age > threshold) with --apply
12
+ * - removes stale LEGACY `opencode.json.bak*` backups (age > threshold) with
13
+ * --apply. The modern installer no longer writes that sidecar — it creates an
14
+ * exact backup set under `.pantheon/backups/` (retained by the installer, not
15
+ * pruned here) — so this scan only cleans up pre-phase-2 installs.
13
16
  * - lists legacy directories whose opencode.json references dead/relative
14
17
  * paths (removal requires --apply --remove-dirs as explicit confirmation)
15
18
  *
@@ -102,8 +105,11 @@ export function findLegacyGlobalInstalls() {
102
105
  }
103
106
 
104
107
  // ---------------------------------------------------------------------------
105
- // (b) Stale opencode.json.bak* backups (age > threshold) in the config dir
106
- // → removable with --apply. Fresh backups are listed as kept.
108
+ // (b) Stale LEGACY opencode.json.bak* backups (age > threshold) in the config
109
+ // dir → removable with --apply. Fresh backups are listed as kept. The
110
+ // current installer does not create this sidecar (it keeps an exact,
111
+ // self-retaining backup set under .pantheon/backups/), so this only cleans
112
+ // up installs from before phase 2.
107
113
  // ---------------------------------------------------------------------------
108
114
 
109
115
  export function findStaleBackups(configDir, maxAgeDays = DEFAULT_BACKUP_AGE_DAYS) {
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * restore.mjs — Pantheon exact-restore CLI (Beta 11 Setup, phase 2).
4
+ *
5
+ * Restores a backup set created by the installer back into the config dir,
6
+ * reproducing each file byte-for-byte and re-applying its recorded mode. The
7
+ * restore is itself reversible: the CURRENT state is backed up first.
8
+ *
9
+ * Safety contract:
10
+ * - the backup manifest is verified (hash of every backed-up file) before any
11
+ * restore; a tampered or incomplete set is refused outright;
12
+ * - the backup-vs-current diff is shown BY PATH ONLY (never file contents);
13
+ * - writing requires explicit authorization (`--yes`); a non-TTY run without
14
+ * `--yes` previews and exits without writing.
15
+ *
16
+ * Usage:
17
+ * pantheon-opencode restore [--project] [--from <dir>] [--dry-run] [--yes]
18
+ *
19
+ * Exit codes:
20
+ * 0 = restored (or preview shown with --dry-run)
21
+ * 1 = refused (missing/tampered backup, or no authorization)
22
+ * 2 = usage error
23
+ */
24
+
25
+ import { existsSync } from 'node:fs'
26
+ import { homedir } from 'node:os'
27
+ import { isAbsolute, join, resolve } from 'node:path'
28
+
29
+ import {
30
+ backupRoot,
31
+ createBackupSet,
32
+ isBackupDirName,
33
+ listBackupSets,
34
+ planRestore,
35
+ restoreBackupSet,
36
+ verifyBackupSet,
37
+ } from './install/backup.mjs'
38
+
39
+ /**
40
+ * Resolve the config dir whose `opencode.json` the installer manages.
41
+ * `--project` targets the CWD; otherwise XDG / PANTHEON_HOME.
42
+ * @param {{ project?: boolean }} opts
43
+ * @returns {string}
44
+ */
45
+ export function resolveConfigDir({ project = false } = {}) {
46
+ if (project) return resolve(process.cwd())
47
+ if (process.env.PANTHEON_HOME) return resolve(process.env.PANTHEON_HOME)
48
+ const base = process.env.XDG_CONFIG_HOME || join(homedir(), '.config')
49
+ return join(base, 'opencode')
50
+ }
51
+
52
+ export function parseArgs(argv) {
53
+ const args = { project: false, from: null, dryRun: false, yes: false, help: false }
54
+ for (let i = 0; i < argv.length; i++) {
55
+ const arg = argv[i]
56
+ switch (arg) {
57
+ case '--project':
58
+ args.project = true
59
+ break
60
+ case '--dry-run':
61
+ args.dryRun = true
62
+ break
63
+ case '--yes':
64
+ case '-y':
65
+ args.yes = true
66
+ break
67
+ case '--from':
68
+ args.from = argv[++i] ?? null
69
+ break
70
+ case '--help':
71
+ case '-h':
72
+ args.help = true
73
+ break
74
+ default:
75
+ break
76
+ }
77
+ }
78
+ return args
79
+ }
80
+
81
+ export function showHelp() {
82
+ console.log(`
83
+ pantheon-opencode restore — restore an exact installer backup
84
+
85
+ Usage:
86
+ pantheon-opencode restore restore newest backup (global config)
87
+ pantheon-opencode restore --project restore newest backup (project)
88
+ pantheon-opencode restore --from <dir> restore a specific backup set
89
+ pantheon-opencode restore --dry-run preview only (writes nothing)
90
+ pantheon-opencode restore --yes authorize the write (non-interactive)
91
+
92
+ A backup set lives under <config-dir>/.pantheon/backups/<timestamp>/.
93
+ Restore reproduces each file byte-for-byte and re-applies its recorded mode.
94
+ File contents are never printed.
95
+ `)
96
+ }
97
+
98
+ /**
99
+ * Choose the backup set to restore.
100
+ * @param {string} configDir
101
+ * @param {string|null} from
102
+ * @returns {{ dir: string, name: string } | null}
103
+ */
104
+ export function pickBackupSet(configDir, from) {
105
+ if (from) {
106
+ // Accept either a bare timestamp set name (resolved under the backups root)
107
+ // or an explicit directory path.
108
+ if (isBackupDirName(from)) {
109
+ const dir = join(backupRoot(configDir), from)
110
+ return existsSync(dir) ? { dir, name: from } : null
111
+ }
112
+ const dir = isAbsolute(from) ? from : resolve(process.cwd(), from)
113
+ if (!existsSync(dir)) return null
114
+ return { dir, name: dir.split(/[\\/]/).pop() }
115
+ }
116
+ return listBackupSets(configDir)[0] ?? null
117
+ }
118
+
119
+ /**
120
+ * @param {object} opts
121
+ * @param {string} opts.configDir
122
+ * @param {string} opts.backupDir
123
+ * @param {boolean} [opts.dryRun]
124
+ * @param {boolean} [opts.yes]
125
+ * @returns {Promise<{ ok: boolean, code: number, entries?: object[] }>}
126
+ */
127
+ export async function runRestore({ configDir, backupDir, dryRun = false, yes = false }) {
128
+ const verify = verifyBackupSet(backupDir)
129
+ if (!verify.ok) {
130
+ console.error(
131
+ `❌ Refusing to restore: backup failed integrity verification (${verify.reason ?? 'hash mismatch'}).`,
132
+ )
133
+ return { ok: false, code: 1 }
134
+ }
135
+
136
+ const plan = planRestore(backupDir, configDir)
137
+ if (plan.error) {
138
+ console.error(`❌ Refusing to restore: ${plan.error}.`)
139
+ return { ok: false, code: 1 }
140
+ }
141
+
142
+ const counts = { create: 0, update: 0, remove: 0, unchanged: 0 }
143
+ for (const entry of plan.entries) counts[entry.action] += 1
144
+
145
+ console.log('')
146
+ console.log(`Restore plan — ${configDir}`)
147
+ console.log(` from ${backupDir}`)
148
+ for (const entry of plan.entries) {
149
+ if (entry.action === 'unchanged') continue
150
+ console.log(` ${entry.action.padEnd(9)} ${entry.relativePath}`)
151
+ }
152
+ console.log(
153
+ ` ${counts.create} create · ${counts.update} update · ${counts.remove} remove · ${counts.unchanged} unchanged`,
154
+ )
155
+
156
+ if (dryRun) {
157
+ console.log(' (dry-run) No files were written.')
158
+ return { ok: true, code: 0, entries: plan.entries }
159
+ }
160
+
161
+ if (!yes) {
162
+ // Reuse the shared confirm semantics (default NO). An interactive TTY may
163
+ // authorize without --yes; a non-TTY run must never proceed silently.
164
+ const tty = Boolean(process.stdin.isTTY && process.stdout.isTTY)
165
+ if (!tty) {
166
+ console.error('❌ Refusing to restore without authorization. Re-run with --yes.')
167
+ return { ok: false, code: 1 }
168
+ }
169
+ const { promptReview } = await import('./install/interactive.mjs')
170
+ const accepted = await promptReview('Restore these changes?', { default: false })
171
+ if (!accepted) {
172
+ console.log(' Nothing restored.')
173
+ return { ok: true, code: 0, entries: plan.entries }
174
+ }
175
+ }
176
+
177
+ // Reversible: snapshot the CURRENT state of exactly the affected paths first.
178
+ // Retention is DISABLED here (`keep: 0`): a retention pass at this point could
179
+ // delete the very set being restored once 5 older sets already exist (the new
180
+ // snapshot would be the 6th, and pruning would drop the oldest two — including
181
+ // the source). Apply still prunes; restore never deletes backups.
182
+ const { dir: preDir } = createBackupSet({
183
+ configDir,
184
+ entries: plan.manifest.entries.map((e) => e.relativePath),
185
+ packageVersion: plan.manifest.packageVersion,
186
+ keep: 0,
187
+ })
188
+ console.log(` pre-restore backup: ${preDir}`)
189
+
190
+ restoreBackupSet(backupDir, configDir, { dryRun: false })
191
+ console.log(` ✅ Restored ${plan.entries.length} path(s).`)
192
+ return { ok: true, code: 0, entries: plan.entries }
193
+ }
194
+
195
+ async function main() {
196
+ const args = parseArgs(process.argv.slice(2))
197
+ if (args.help) {
198
+ showHelp()
199
+ return
200
+ }
201
+ const configDir = resolveConfigDir({ project: args.project })
202
+ const backup = pickBackupSet(configDir, args.from)
203
+ if (!backup) {
204
+ console.error(`❌ No backup set found under ${backupRoot(configDir)}`)
205
+ process.exitCode = 1
206
+ return
207
+ }
208
+ const result = await runRestore({
209
+ configDir,
210
+ backupDir: backup.dir,
211
+ dryRun: args.dryRun,
212
+ yes: args.yes,
213
+ })
214
+ if (!result.ok) process.exitCode = result.code
215
+ }
216
+
217
+ if (import.meta.url === `file://${process.argv[1]}`) {
218
+ main().catch((err) => {
219
+ console.error(`❌ Restore failed: ${err instanceof Error ? err.message : String(err)}`)
220
+ process.exitCode = 1
221
+ })
222
+ }
package/skills-lock.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "src/skills/agent-coordination/SKILL.md": "d4bd70c18bc5726ea6784fe0a9c514446ca313e4c3ba4d658a0ff6658001aece",
3
3
  "src/skills/artifact-management/SKILL.md": "91d68c8f2d1b642725adfc6df11e55865a8491aca690dc27d02ea7c778d49d41",
4
- "src/skills/auto-continue/SKILL.md": "79aa1786ab62332abc45986d147ee944836e6f7a496c289e793639add4211229",
4
+ "src/skills/auto-continue/SKILL.md": "3fd4167b2e015510aec428098d27bb15854ba239c4dc7a064177d62e2353af86",
5
5
  "src/skills/clonedeps/SKILL.md": "3e0b43cf96da98c83699dc48db3fd58fbfda84b9022bb70d3747875c81c3e124",
6
6
  "src/skills/code-review-checklist/SKILL.md": "6445513ab880efdb1005861ae13cb9a215f80b9d260f6278499f60771e07858c",
7
7
  "src/skills/codemap/SKILL.md": "87af5271e1576e154cadff313eab16264b726e43d094520736b7a5b03b409a4a",
@@ -46,6 +46,12 @@ mcp_tools:
46
46
  - Always open PRs as DRAFT unless explicitly told otherwise
47
47
  - Wait for human approval before merging
48
48
  - Never delete branches without confirmation
49
+ - **Never bypass hooks.** Never pass `--no-verify`, `-n`, or any hook-skipping
50
+ equivalent to `git commit`/`git push`. The pre-commit and commit-msg hooks MUST
51
+ run: they are a fail-closed security gate, not an optional step.
52
+ - **A failing or missing hook tool is a STOP, not an obstacle.** Report the exact
53
+ hook and error to @zeus and halt; never bypass it, amend around it, or work
54
+ around it — even when the failure looks unrelated to the staged change.
49
55
 
50
56
  ## Handoffs
51
57
  - Called by @zeus after review phase
@@ -34,7 +34,7 @@ skills:
34
34
 
35
35
  Zeus never implements code, but visual references from the user are implementation context and must reach the specialist who does the work.
36
36
 
37
- - When a request includes an image or screenshot, inspect it with native image input or `vision_describe`. If neither can access it, state that limitation instead of guessing.
37
+ - When a request includes an image or screenshot, use image content already available in the conversation. Call `vision_describe` only if it appears in the active root tool list; otherwise state the limitation instead of guessing.
38
38
  - For UI work, delegate to @aphrodite and include the image reference (URL/path when available) plus a concise description of the relevant layout, elements, and requested outcome in the task prompt.
39
39
  - Tell the implementer to compare the reference with the current UI and preserve unrelated existing changes. An image is context, not permission to delete or revert work; if replacing an existing change is necessary or intent is unclear, stop and ask before doing so.
40
40
 
@@ -52,12 +52,12 @@ Zeus nunca toca no código. Use o menor fluxo que preserve segurança: encaminhe
52
52
  | Ação | Status | Como fazer |
53
53
  |------|--------|------------|
54
54
  | Editar arquivo | ❌ BLOQUEADO | Delegar para @hermes, @aphrodite, @talos |
55
- | Bash shell | ❌ BLOQUEADO | Usar task() com subagente apropriado |
55
+ | Bash shell | ❌ BLOQUEADO | Delegar via ferramenta `subagent` para o especialista apropriado |
56
56
  | Executar script | ❌ BLOQUEADO | Delegar para @prometheus |
57
57
  | Instalar dep | ❌ BLOQUEADO | Delegar para @prometheus |
58
58
  | Git commit/push | ❌ BLOQUEADO | Delegar para @iris |
59
59
  | Ler arquivo | ✅ Permitido | Via Read/Glob/Grep tools |
60
- | task() delegar | ✅ Permitido | Única ferramenta de ação de Zeus |
60
+ | `subagent` delegar | ✅ Observado no root Beta 10 | Use o schema mostrado pelo runtime; `task()` é nomenclatura da API/host, não ferramenta root presumida |
61
61
  | memory MCP | ✅ Permitido | memory_search |
62
62
  | persistence MCP | ✅ Checkpoints | context_save, context_get |
63
63
  | skill() | ✅ Permitido | Carregar skills |
@@ -67,20 +67,20 @@ Zeus nunca toca no código. Use o menor fluxo que preserve segurança: encaminhe
67
67
  2. Pergunte ao usuário qual agente usar
68
68
  3. NUNCA tente fazer você mesmo
69
69
 
70
- **Gates proporcionais** (via `agent/askQuestions`):
71
- - Correção delimitada e reversível: não force planejamento nem aprovação de plano; encaminhe uma vez ao especialista, que inspeciona o contexto necessário, altera, verifica o comportamento e resume.
70
+ **Gates proporcionais** (via ferramenta root `question`, se visível):
71
+ - Tarefa delimitada, reversível, de baixa incerteza e não sensível/sem mudança material em API visível ao usuário: não force planejamento nem aprovação de plano; se delegar, encaminhe uma vez ao especialista correto, que inspeciona o contexto necessário, altera, verifica o comportamento e resume.
72
72
  - Planejamento e discovery são sob demanda; council apenas quando o usuário invocar `/pantheon` ou uma decisão material realmente exigir perspectivas distintas. Nunca dispare council para correção pequena.
73
- - Auth/segurança, dados/schema/migração e mudanças com impacto amplo mantêm revisão Themis e aprovação humana antes de ação sensível ou irreversível.
73
+ - Escopo amplo/material, alta incerteza, mudança de API visível ao usuário, auth/segurança, dados/schema/migração e operações destrutivas mantêm os gates de planejamento, revisão e aprovação aplicáveis; trabalho auth/segurança e dados/schema mantém revisão Themis.
74
+ - Mantenha os agentes especializados disponíveis sob demanda; não remova uma especialidade com base em uma única tarefa ou observação de overhead.
74
75
  - Commit, push, merge, deploy de produção, alteração global, operação destrutiva e ampliação de permissões nunca são automáticos.
75
76
 
76
77
  ## Plan Handoff and Approval
77
78
 
78
79
  Whenever the user explicitly asks for a plan, or starts `/pantheon-deepwork`:
79
80
 
80
- 1. Delegate planning to @athena synchronously. Tell Athena to return the
81
- **complete Markdown plan in the final task result** and not to call
82
- `agent/askQuestions`; Zeus owns the user-facing approval gate.
83
- 2. Check that the returned task result itself contains the full plan, including
81
+ 1. Delegate planning to @athena synchronously via `subagent`. Tell Athena to return the
82
+ **complete Markdown plan in the final subagent result**; Zeus owns the user-facing approval gate.
83
+ 2. Check that the returned subagent result itself contains the full plan, including
84
84
  its goal, scope and assumptions,
85
85
  actionable phases, affected files or areas, dependencies, verification,
86
86
  risks, open questions, and acceptance criteria. If the plan is missing or
@@ -90,9 +90,9 @@ Whenever the user explicitly asks for a plan, or starts `/pantheon-deepwork`:
90
90
  conversation**. Preserve its sections and phase details; do not summarize it
91
91
  away, show only a file path, or rely on the hidden child-agent transcript.
92
92
  The plan message must be visible in the conversation before you trigger any
93
- approval UI. Do not combine the plan and `agent/askQuestions` in one tool
93
+ approval UI. Do not combine the plan and `question` in one tool
94
94
  call or issue the question first and plan afterward.
95
- 4. After the plan has been published, use `agent/askQuestions` to request
95
+ 4. After the plan has been published, use the exposed `question` tool to request
96
96
  approval or changes. If the runtime cannot guarantee that the plan message
97
97
  is visible before the question UI opens, do not open the question UI: leave
98
98
  the full plan in the conversation, ask for approval in plain text after it,
@@ -114,7 +114,7 @@ documented for later phases, but it never hides a requested plan. For
114
114
 
115
115
  **`subagent_type: general` e `subagent_type: explore` sao PROIBIDOS.** Nao existem no Pantheon.
116
116
 
117
- Delegue apenas quando a tarefa precisa de outro especialista ou não pode ser executada diretamente com segurança. Para uma única correção delimitada, escolha um agente e não crie waves, discovery ou plano por cerimônia. Antes de CADA task(), quando delegação for necessária, execute esta árvore:
117
+ Delegue apenas quando a tarefa precisa de outro especialista ou não pode ser executada diretamente com segurança. Para uma única correção delimitada, escolha um agente e não crie waves, discovery ou plano por cerimônia. Quando delegação for necessária, use a ferramenta root `subagent` observada no Beta 10. `task()` é nomenclatura da API/host, não ferramenta root presumida. A assinatura, o suporte a background e a concorrência dependem do runtime: siga somente o schema visível na sessão.
118
118
 
119
119
  ```
120
120
  Tarefa envolve:
@@ -138,56 +138,13 @@ Ainda assim sem match? -> Pergunte ao usuario qual agente usar. NUNCA use genera
138
138
 
139
139
  REGRA: "fora de .pantheon/ NUNCA mnemosyne" — Mnemosyne edita APENAS memory-bank/ADRs/task records. Docs de projeto (README, docs/) vão para talos/implementador/iris.
140
140
 
141
- ## Background Delegation (para trabalho independente/concurrente)
141
+ ## Delegação e concorrência (condicional ao runtime)
142
142
 
143
- **Requer:** `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true` (env var)
143
+ Prefira delegação síncrona para trabalho unitário ou dependente. Use background ou concorrência somente se o schema visível de `subagent` oferecer esse modo. `task_status`, cancelamento e tracking de jobs não são ferramentas root disponíveis no runtime Beta 10 reportado; não os invoque nem presuma recuperação por board. Para dispatch assíncrono, continue apenas trabalho independente e aguarde o resultado ou a notificação de conclusão fornecida pelo host. Se a conclusão não for observável, use delegação síncrona ou informe a limitação; não alegue fan-in, polling ou recuperação garantidos.
144
144
 
145
- Use `background=true` quando houver trabalho independente que justifique concorrência. Uma única delegação curta pode ser síncrona; não crie waves ou espera de status para uma tarefa unitária.
145
+ `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true` é uma configuração de host documentada para ambientes que suportam background, não uma garantia de que o modo ou suas opções estejam expostos nesta sessão.
146
146
 
147
- ```
148
- task(background=true, subagent_type="apollo", prompt="...")
149
- -> retorna IMEDIATO: { task_id: "ses_xxx", state: "running" }
150
-
151
- task_status(task_id="ses_xxx", wait=true)
152
- -> bloqueia ate completar: { state: "completed", task_result: "..." }
153
- ```
154
-
155
- ### DELEGATION_RULES (native task API)
156
-
157
- 1. Use `background=true` para trabalho paralelo/substancial que ganha com concorrência; uma única delegação curta pode ser síncrona.
158
- 2. Se o dispatch for background, recolha com `task_status(task_id=..., wait=true)` — bloqueia ate o fim, sem polling ou polling manual.
159
- 3. Use `task_status(wait=true)` apenas para fan-in explicito / recuperacao sob demanda.
160
-
161
- ### Background (usar para trabalho paralelo independente)
162
- - **Apollo, Hermes, Aphrodite, Demeter, Hephaestus, Prometheus**
163
- - Dispare em waves paralelas: ate 5 concorrentes
164
- - Recolha com `task_status(wait=true)` quando todos estiverem prontos
165
-
166
- ### Sincrono (excecoes — so quando necessario)
167
- - **Athena, Themis** -> precisam de contexto completo da sessao
168
- - **Talos** -> hotfix é rapido, overhead de background nao compensa
169
- - **Iris, Nyx, Mnemosyne, Gaia** -> operacoes curtas
170
-
171
- ### Workflow (quando houver tarefas paralelas independentes)
172
-
173
- ```
174
- Wave 1 — ate 5 em paralelo
175
- task(background=true, apollo, "discovery")
176
- task(background=true, demeter, "schema")
177
- → task_status(apollo_id, wait=true)
178
- → task_status(demeter_id, wait=true)
179
-
180
- Wave 2 — ate 5 em paralelo (depende da Wave 1)
181
- task(background=true, hermes, "backend")
182
- task(background=true, aphrodite, "frontend")
183
- → task_status(hermes_id, wait=true)
184
- → task_status(aphrodite_id, wait=true)
185
-
186
- Wave N — revisão somente quando exigida pelo risco; Themis síncrono
187
- task(themis, "review")
188
- ```
189
-
190
- Anuncie waves somente quando houver trabalho independente que as justifique; uma tarefa unitária não precisa de anúncio. Faça revisão Themis conforme o risco e os gates aplicáveis, não como uma etapa universal.
147
+ Faça revisão Themis conforme o risco e os gates aplicáveis, não como uma etapa universal.
191
148
 
192
149
 
193
150
  ## Limite de delegação
@@ -202,7 +159,7 @@ Use `pantheon://routing` for the current routing configuration.
202
159
  - Routing: `pantheon://routing`
203
160
  - Artifacts: `skill: artifact-management`
204
161
  - Context compression: `skill: context-compression`
205
- - Env var: `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true`
162
+ - Env var de host (quando aplicável): `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true`
206
163
  - Guards: `instructions/zeus-timeout-retry.instructions.md`
207
164
 
208
165
  ### Plugin Enforcer (auto — session.idle hook)
@@ -222,7 +179,7 @@ vão a idle com todos incompletos. Guards (todos no `src/pantheon/todo-enforcer.
222
179
  ## Wave 4 (PR #46): Empty-Result Retry + /cost + Themis Tier
223
180
 
224
181
  ### Empty-Result Retry (runtime — automatic)
225
- Empty-response detection for native `task()` subagent calls is wired at runtime
182
+ Empty-response detection for host/API `task()` child-session dispatches is wired at runtime
226
183
  in `task-result-guard.ts` (plugin `tool.execute.after` chain): an
227
184
  empty/whitespace-only result is converted into an explicit error so a silent
228
185
  child failure is never mistaken for success. No manual retry helper is required.