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.
- package/AGENTS.global.md +42 -18
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +24 -0
- package/README.md +66 -21
- package/bin/pantheon-init.mjs +44 -1
- package/package.json +4 -4
- package/plugin.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/build-agents-md.mjs +60 -6
- package/scripts/install/backup.mjs +475 -0
- package/scripts/install/cli-ui.mjs +6 -1
- package/scripts/install/exit-codes.mjs +26 -0
- package/scripts/install/interactive.mjs +15 -1
- package/scripts/install/migrate.mjs +41 -22
- package/scripts/install/opencode.mjs +562 -50
- package/scripts/install/plan.mjs +287 -0
- package/scripts/install/plugin.mjs +22 -8
- package/scripts/install/shared.mjs +89 -55
- package/scripts/install/strings.mjs +20 -0
- package/scripts/install/venv.mjs +13 -3
- package/scripts/install/vision-prompt.mjs +11 -1
- package/scripts/mcp_persistence.py +50 -15
- package/scripts/prune.mjs +9 -3
- package/scripts/restore.mjs +222 -0
- package/skills-lock.json +1 -1
- package/src/agents/iris.md +6 -0
- package/src/agents/zeus.md +19 -62
- package/src/instructions/agent-return-format.instructions.md +17 -15
- package/src/instructions/memory-protocol.instructions.md +3 -2
- package/src/instructions/test-economy.instructions.md +21 -0
- package/src/instructions/zeus-anti-stall.instructions.md +6 -6
- package/src/instructions/zeus-council-synthesis.instructions.md +10 -11
- package/src/instructions/zeus-memory-operations.instructions.md +10 -6
- package/src/instructions/zeus-timeout-retry.instructions.md +28 -9
- package/src/mcp/mcp_persistence.py +50 -15
- package/src/pantheon/cost-command.ts +5 -2
- package/src/pantheon/model-agents.ts +19 -0
- package/src/pantheon/model-command.ts +218 -56
- package/src/pantheon/v2-model-overrides.ts +275 -0
- package/src/pantheon/v2-tools.ts +18 -29
- package/src/plugin-v2.ts +13 -0
- package/src/plugins/tui/dist/tui-v2.js +2 -2
- package/src/plugins/tui/dist/tui.js +1 -1
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
- 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}`,
|
package/scripts/install/venv.mjs
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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)
|
|
918
|
-
raise ValueError("tail
|
|
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)
|
|
921
|
-
raise ValueError("tail entries
|
|
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(
|
|
924
|
-
|
|
925
|
-
|
|
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
|
-
|
|
1024
|
-
|
|
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
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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
|
|
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
|
|
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
|
|
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": "
|
|
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",
|
package/src/agents/iris.md
CHANGED
|
@@ -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
|
package/src/agents/zeus.md
CHANGED
|
@@ -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,
|
|
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 |
|
|
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
|
-
|
|
|
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 `
|
|
71
|
-
-
|
|
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
|
-
-
|
|
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
|
|
81
|
-
**complete Markdown plan in the final
|
|
82
|
-
|
|
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 `
|
|
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 `
|
|
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.
|
|
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
|
-
##
|
|
141
|
+
## Delegação e concorrência (condicional ao runtime)
|
|
142
142
|
|
|
143
|
-
|
|
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
|
-
|
|
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
|
|
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.
|