pantheon-opencode 1.6.0-beta.4 → 1.6.0-beta.6
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.md +42 -23
- package/CHANGELOG.md +41 -0
- package/README.md +114 -12
- package/README.pt-BR.md +15 -6
- package/bin/pantheon-init.mjs +26 -3
- package/package.json +5 -6
- package/plugin.json +1 -1
- package/pyproject.toml +6 -6
- package/scripts/{code_mode_server.py → code_mode.py} +2 -2
- package/scripts/doctor.mjs +151 -1
- package/scripts/install/config-migration.mjs +80 -2
- package/scripts/install/health-check.mjs +12 -17
- package/scripts/install/opencode-version.mjs +246 -4
- package/scripts/install/opencode.mjs +250 -119
- package/scripts/install-mcp.mjs +28 -9
- package/scripts/{mcp_persistence_server.py → mcp_persistence.py} +1 -1
- package/{src/mcp/mcp_resources_server.py → scripts/mcp_resources.py} +1 -1
- package/scripts/{memory_mcp_server.py → memory_mcp.py} +1 -1
- package/scripts/probe-context-rehydrate.mjs +2 -2
- package/scripts/run-node-tests.mjs +189 -0
- package/scripts/sync-artifacts.mjs +5 -5
- package/scripts/test-opencode-v2-sandbox.sh +21 -21
- package/skills-lock.json +2 -2
- package/src/agents/aphrodite.md +1 -1
- package/src/agents/apollo.md +7 -2
- package/src/agents/athena.md +1 -0
- package/src/agents/demeter.md +1 -1
- package/src/agents/hephaestus.md +1 -1
- package/src/agents/hermes.md +1 -1
- package/src/agents/iris.md +1 -0
- package/src/agents/nyx.md +1 -0
- package/src/agents/themis.md +1 -0
- package/src/agents/zeus.md +39 -10
- package/src/instructions/memory-protocol.instructions.md +21 -10
- package/src/instructions/zeus-anti-stall.instructions.md +19 -11
- package/src/instructions/zeus-council-synthesis.instructions.md +2 -2
- package/src/mcp/{code_mode_server.py → code_mode.py} +2 -2
- package/src/mcp/eval_store.py +2 -2
- package/src/mcp/{mcp_persistence_server.py → mcp_persistence.py} +1 -1
- package/{scripts/mcp_resources_server.py → src/mcp/mcp_resources.py} +1 -1
- package/src/mcp/{memory_mcp_server.py → memory_mcp.py} +1 -1
- package/src/pantheon/v2-unsupported.d.mts +11 -0
- package/src/pantheon/v2-unsupported.mjs +113 -0
- package/src/plugin-v2.ts +149 -53
- 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 +4 -4
- package/src/skills/context-compression/SKILL.md +1 -1
- package/opencode.json +0 -58
- /package/src/mcp/{pantheon_vision_server.py → pantheon_vision.py} +0 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Run `node --test` over an explicit file list, and FAIL when zero tests are collected.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: `node --test <glob>` exits 0 when the glob matches nothing (Node >= 22).
|
|
6
|
+
* A test script whose file set is absent from the checkout -- or absent from the published
|
|
7
|
+
* package, because the `files` allow-list omits it -- therefore reports success while
|
|
8
|
+
* collecting nothing. That is the worst possible failure mode for a release gate: it
|
|
9
|
+
* manufactures confidence.
|
|
10
|
+
*
|
|
11
|
+
* This runner closes that hole from two independent sides:
|
|
12
|
+
* 1. It expands the globs ITSELF and refuses to start the runner with zero files, so the
|
|
13
|
+
* runner is never handed an unexpanded pattern that silently matches nothing.
|
|
14
|
+
* 2. After a run that exited 0, it re-reads the collected count from the machine-readable
|
|
15
|
+
* TAP summary and fails closed when that count is 0, unparseable, or missing.
|
|
16
|
+
*
|
|
17
|
+
* Side 2 is deliberately independent of side 1: a future Node change to `--test` glob
|
|
18
|
+
* handling, a glob that expands to a directory of empty files, or an emptied test file all
|
|
19
|
+
* still get caught.
|
|
20
|
+
*
|
|
21
|
+
* Human-readable output is unchanged: the default `spec` reporter still goes to stdout, and
|
|
22
|
+
* the TAP reporter is diverted to a scratch file that is read once and deleted.
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* node scripts/run-node-tests.mjs [--label <name>] [-- <node flags...>] <glob>...
|
|
26
|
+
*
|
|
27
|
+
* Examples:
|
|
28
|
+
* node scripts/run-node-tests.mjs 'tests/*.mjs' 'tests/pantheon/*.mjs'
|
|
29
|
+
* node scripts/run-node-tests.mjs -- --experimental-test-coverage 'tests/*.mjs'
|
|
30
|
+
*/
|
|
31
|
+
import { spawnSync } from 'node:child_process'
|
|
32
|
+
import { globSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
|
33
|
+
import { tmpdir } from 'node:os'
|
|
34
|
+
import { join } from 'node:path'
|
|
35
|
+
import { fileURLToPath } from 'node:url'
|
|
36
|
+
|
|
37
|
+
const ROOT = join(fileURLToPath(new URL('.', import.meta.url)), '..')
|
|
38
|
+
|
|
39
|
+
function usage(message) {
|
|
40
|
+
console.error(`zero-test-guard: ${message}`)
|
|
41
|
+
console.error(
|
|
42
|
+
'usage: node scripts/run-node-tests.mjs [--label <name>] [-- <node flags...>] <glob>...',
|
|
43
|
+
)
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Parse `[--label <name>] [-- <node flags...>] <glob>...`. */
|
|
48
|
+
function parseArgs(argv) {
|
|
49
|
+
const separator = argv.indexOf('--')
|
|
50
|
+
const own = separator === -1 ? argv : argv.slice(0, separator)
|
|
51
|
+
const nodeFlags = separator === -1 ? [] : argv.slice(separator + 1)
|
|
52
|
+
|
|
53
|
+
let label = 'node tests'
|
|
54
|
+
const globs = []
|
|
55
|
+
for (let i = 0; i < own.length; i++) {
|
|
56
|
+
if (own[i] === '--label') {
|
|
57
|
+
const value = own[i + 1]
|
|
58
|
+
if (value === undefined) usage('--label requires a value')
|
|
59
|
+
label = value
|
|
60
|
+
i += 1
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
if (own[i].startsWith('-')) usage(`unknown option ${own[i]}`)
|
|
64
|
+
globs.push(own[i])
|
|
65
|
+
}
|
|
66
|
+
if (globs.length === 0) usage('at least one glob is required')
|
|
67
|
+
return { label, globs, nodeFlags }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Expand globs to a deduped, ordered file list.
|
|
72
|
+
* Order follows the positional order of the globs so that the suite runs in the same order
|
|
73
|
+
* as the shell used to; within a glob, matches are sorted for determinism.
|
|
74
|
+
*/
|
|
75
|
+
function expand(globs) {
|
|
76
|
+
const files = []
|
|
77
|
+
const seen = new Set()
|
|
78
|
+
for (const pattern of globs) {
|
|
79
|
+
const matches = globSync(pattern, { cwd: ROOT })
|
|
80
|
+
.filter((match) => !match.includes('node_modules') && !match.includes('__pycache__'))
|
|
81
|
+
.sort()
|
|
82
|
+
for (const match of matches) {
|
|
83
|
+
if (seen.has(match)) continue
|
|
84
|
+
seen.add(match)
|
|
85
|
+
files.push(match)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return files
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function reportMissingFiles(label, globs) {
|
|
92
|
+
console.error(`zero-test-guard: ${label} matched 0 test files — refusing to report success.`)
|
|
93
|
+
console.error(` globs: ${globs.join(' ')}`)
|
|
94
|
+
console.error(' This is the "test suite silently passes when it collects nothing" failure.')
|
|
95
|
+
console.error(' Either the test files are missing from this checkout, or they are missing')
|
|
96
|
+
console.error(' from the published package (check the "files" allow-list in package.json).')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function reportZeroCollected(label, files) {
|
|
100
|
+
console.error(`zero-test-guard: ${label} collected 0 tests — refusing to report success.`)
|
|
101
|
+
console.error(` ${files.length} file(s) matched, but not one test case was collected:`)
|
|
102
|
+
for (const file of files) console.error(` ${file}`)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Count the real test cases collected in a TAP summary.
|
|
107
|
+
*
|
|
108
|
+
* Node counts a test FILE that declares no test cases as one passing "test"
|
|
109
|
+
* (`ok 1 - tests/foo.test.mjs`), so the summary's `# tests N` is >= the file count and
|
|
110
|
+
* can never reach 0 once any file matched. Relying on it alone would let a suite whose
|
|
111
|
+
* every test file was emptied out report success.
|
|
112
|
+
*
|
|
113
|
+
* The reliable discriminator is the subtest NAME: a file-level placeholder is named with
|
|
114
|
+
* the file path that was passed to the runner, a real test case is named with its test
|
|
115
|
+
* name. So a top-level subtest whose name is one of our matched paths is a placeholder
|
|
116
|
+
* and is not counted as a collected case.
|
|
117
|
+
*
|
|
118
|
+
* Returns null when the summary cannot be read or parsed -- the caller fails closed.
|
|
119
|
+
*/
|
|
120
|
+
function countCollectedTests(tapPath, files) {
|
|
121
|
+
let tap = ''
|
|
122
|
+
try {
|
|
123
|
+
tap = readFileSync(tapPath, 'utf8')
|
|
124
|
+
} catch {
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
// The summary count is still asserted to exist: without it the stream is not TAP.
|
|
128
|
+
if (!/^#\s*tests\s+\d+\s*$/m.test(tap)) return null
|
|
129
|
+
|
|
130
|
+
const placeholders = new Set(files)
|
|
131
|
+
let collected = 0
|
|
132
|
+
for (const match of tap.matchAll(/^# Subtest: (.+)$/gm)) {
|
|
133
|
+
if (placeholders.has(match[1].trim())) continue
|
|
134
|
+
collected += 1
|
|
135
|
+
}
|
|
136
|
+
return collected
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const { label, globs, nodeFlags } = parseArgs(process.argv.slice(2))
|
|
140
|
+
const files = expand(globs)
|
|
141
|
+
|
|
142
|
+
if (files.length === 0) {
|
|
143
|
+
reportMissingFiles(label, globs)
|
|
144
|
+
process.exit(1)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const scratch = mkdtempSync(join(tmpdir(), 'pantheon-node-tests-'))
|
|
148
|
+
const tapPath = join(scratch, 'summary.tap')
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const run = spawnSync(
|
|
152
|
+
process.execPath,
|
|
153
|
+
[
|
|
154
|
+
'--test',
|
|
155
|
+
'--test-reporter=spec',
|
|
156
|
+
'--test-reporter-destination=stdout',
|
|
157
|
+
'--test-reporter=tap',
|
|
158
|
+
`--test-reporter-destination=${tapPath}`,
|
|
159
|
+
...nodeFlags,
|
|
160
|
+
...files,
|
|
161
|
+
],
|
|
162
|
+
{ cwd: ROOT, stdio: 'inherit' },
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if (run.error) {
|
|
166
|
+
console.error(`zero-test-guard: could not start the test runner: ${run.error.message}`)
|
|
167
|
+
process.exit(1)
|
|
168
|
+
}
|
|
169
|
+
if (run.signal) {
|
|
170
|
+
console.error(`zero-test-guard: the test runner was killed by ${run.signal}`)
|
|
171
|
+
process.exit(1)
|
|
172
|
+
}
|
|
173
|
+
// Real failures keep the runner's own exit code; the guard only speaks up when the
|
|
174
|
+
// runner claimed success it cannot justify.
|
|
175
|
+
if (run.status !== 0) process.exit(run.status)
|
|
176
|
+
|
|
177
|
+
const collected = countCollectedTests(tapPath, files)
|
|
178
|
+
if (collected === null) {
|
|
179
|
+
console.error('zero-test-guard: could not read the collected test count from the TAP summary.')
|
|
180
|
+
console.error(' Failing closed: an unreadable summary cannot certify that any test ran.')
|
|
181
|
+
process.exit(1)
|
|
182
|
+
}
|
|
183
|
+
if (collected === 0) {
|
|
184
|
+
reportZeroCollected(label, files)
|
|
185
|
+
process.exit(1)
|
|
186
|
+
}
|
|
187
|
+
} finally {
|
|
188
|
+
rmSync(scratch, { recursive: true, force: true })
|
|
189
|
+
}
|
|
@@ -31,13 +31,13 @@ import {
|
|
|
31
31
|
|
|
32
32
|
/** Canonical MCP script sources — mirrors installOpenCode phase 2.10. */
|
|
33
33
|
const MCP_SCRIPTS = {
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'
|
|
34
|
+
'mcp_resources.py': () => join(ROOT, 'src', 'mcp', 'mcp_resources.py'),
|
|
35
|
+
'code_mode.py': () => join(ROOT, 'src', 'mcp', 'code_mode.py'),
|
|
36
|
+
'memory_mcp.py': () => join(ROOT, 'src', 'mcp', 'memory_mcp.py'),
|
|
37
|
+
'mcp_persistence.py': () => join(ROOT, 'src', 'mcp', 'mcp_persistence.py'),
|
|
38
38
|
'_pantheon_paths.py': () => join(ROOT, 'src', 'mcp', '_pantheon_paths.py'),
|
|
39
39
|
'mcp_codemap_module.py': () => join(ROOT, 'src', 'mcp', 'mcp_codemap_module.py'),
|
|
40
|
-
'
|
|
40
|
+
'pantheon_vision.py': () => join(ROOT, 'src', 'mcp', 'pantheon_vision.py'),
|
|
41
41
|
'eval_store.py': () => join(ROOT, 'src', 'mcp', 'eval_store.py'),
|
|
42
42
|
'scrub-secrets.py': () => join(ROOT, 'scripts', 'scrub-secrets.py'),
|
|
43
43
|
}
|
|
@@ -241,11 +241,11 @@ try:
|
|
|
241
241
|
except OSError:
|
|
242
242
|
raise SystemExit(1)
|
|
243
243
|
expected = {
|
|
244
|
-
"
|
|
245
|
-
"
|
|
246
|
-
"
|
|
247
|
-
"
|
|
248
|
-
"
|
|
244
|
+
"code_mode.py",
|
|
245
|
+
"memory_mcp.py",
|
|
246
|
+
"mcp_persistence.py",
|
|
247
|
+
"mcp_resources.py",
|
|
248
|
+
"pantheon_vision.py",
|
|
249
249
|
}
|
|
250
250
|
pattern = re.compile(
|
|
251
251
|
r"spawning process \{\s*"
|
|
@@ -515,11 +515,11 @@ try:
|
|
|
515
515
|
except OSError:
|
|
516
516
|
raise SystemExit(1)
|
|
517
517
|
expected = {
|
|
518
|
-
"
|
|
519
|
-
"
|
|
520
|
-
"
|
|
521
|
-
"
|
|
522
|
-
"
|
|
518
|
+
"code_mode.py",
|
|
519
|
+
"memory_mcp.py",
|
|
520
|
+
"mcp_persistence.py",
|
|
521
|
+
"mcp_resources.py",
|
|
522
|
+
"pantheon_vision.py",
|
|
523
523
|
}
|
|
524
524
|
pattern = re.compile(
|
|
525
525
|
r"spawning process \{\s*"
|
|
@@ -642,11 +642,11 @@ config_path = Path(os.environ["CONFIG_PATH"])
|
|
|
642
642
|
runtime_dir = os.environ["RUNTIME_DIR"]
|
|
643
643
|
python_bin = os.environ["MCP_PYTHON"]
|
|
644
644
|
managed = {
|
|
645
|
-
"pantheon-code-mode": "
|
|
646
|
-
"pantheon-memory": "
|
|
647
|
-
"pantheon-persistence": "
|
|
648
|
-
"pantheon-resources": "
|
|
649
|
-
"pantheon-vision": "
|
|
645
|
+
"pantheon-code-mode": "code_mode.py",
|
|
646
|
+
"pantheon-memory": "memory_mcp.py",
|
|
647
|
+
"pantheon-persistence": "mcp_persistence.py",
|
|
648
|
+
"pantheon-resources": "mcp_resources.py",
|
|
649
|
+
"pantheon-vision": "pantheon_vision.py",
|
|
650
650
|
}
|
|
651
651
|
try:
|
|
652
652
|
config = json.loads(config_path.read_text(encoding="utf-8"))
|
|
@@ -829,7 +829,7 @@ else
|
|
|
829
829
|
fi
|
|
830
830
|
|
|
831
831
|
echo "--- Gate (b): pantheon://agents CONTENT check ---"
|
|
832
|
-
RESOURCES_SRV="$PANTHEON_GLOBAL/scripts/
|
|
832
|
+
RESOURCES_SRV="$PANTHEON_GLOBAL/scripts/mcp_resources.py"
|
|
833
833
|
if [ ! -f "$RESOURCES_SRV" ]; then
|
|
834
834
|
record_status "pantheon://agents resource" "FAIL" "resources server not found at $RESOURCES_SRV"
|
|
835
835
|
else
|
|
@@ -1013,11 +1013,11 @@ config_path = Path(os.environ["CONFIG_PATH"])
|
|
|
1013
1013
|
runtime_dir = os.environ["RUNTIME_DIR"]
|
|
1014
1014
|
python_bin = os.environ["MCP_PYTHON"]
|
|
1015
1015
|
managed = {
|
|
1016
|
-
"pantheon-code-mode": "
|
|
1017
|
-
"pantheon-memory": "
|
|
1018
|
-
"pantheon-persistence": "
|
|
1019
|
-
"pantheon-resources": "
|
|
1020
|
-
"pantheon-vision": "
|
|
1016
|
+
"pantheon-code-mode": "code_mode.py",
|
|
1017
|
+
"pantheon-memory": "memory_mcp.py",
|
|
1018
|
+
"pantheon-persistence": "mcp_persistence.py",
|
|
1019
|
+
"pantheon-resources": "mcp_resources.py",
|
|
1020
|
+
"pantheon-vision": "pantheon_vision.py",
|
|
1021
1021
|
}
|
|
1022
1022
|
|
|
1023
1023
|
try:
|
package/skills-lock.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"src/skills/agent-coordination/SKILL.md": "cfd040ef29e3dd0fd5e0e67773874facf7c06e3284898afe1fddba4456c3f1aa",
|
|
3
3
|
"src/skills/artifact-management/SKILL.md": "c60e493666bedf46e9113d0dd20106489d5c5a73905e1370fcfc1c0d71c59185",
|
|
4
|
-
"src/skills/auto-continue/SKILL.md": "
|
|
4
|
+
"src/skills/auto-continue/SKILL.md": "a7a4ec7ab35970bec36bcc286b95881b43a75e3bcdb626a17ebb9e397dd1a708",
|
|
5
5
|
"src/skills/clonedeps/SKILL.md": "3e0b43cf96da98c83699dc48db3fd58fbfda84b9022bb70d3747875c81c3e124",
|
|
6
6
|
"src/skills/code-review-checklist/SKILL.md": "0e59367c65ef4608db80a0674279f32cfc3f67dca7236f233b61a47fe506579f",
|
|
7
7
|
"src/skills/codemap/SKILL.md": "87af5271e1576e154cadff313eab16264b726e43d094520736b7a5b03b409a4a",
|
|
8
|
-
"src/skills/context-compression/SKILL.md": "
|
|
8
|
+
"src/skills/context-compression/SKILL.md": "9c65c132beb0dc7bd20335cfd7f41c589f5168a72dcf5d51740bc66f2865e027",
|
|
9
9
|
"src/skills/git-workflow-and-versioning/SKILL.md": "685a94041f1b4ce2a430301f517b89102055696267c369a7e8fb191606659276",
|
|
10
10
|
"src/skills/incremental-implementation/SKILL.md": "7065ddb45da3c6b6751de07f290e92e6cb072e949a2e3aaae13ee7762f0a70fc",
|
|
11
11
|
"src/skills/loop-engineering/SKILL.md": "f804e545821b9cca8fe0ec517307ed902446f28c2ca8ff9194c0f987cf7229e3",
|
package/src/agents/aphrodite.md
CHANGED
|
@@ -122,7 +122,7 @@ Frontend: `tdd-with-agents`, `visual-review-pipeline`, `incremental-implementati
|
|
|
122
122
|
|
|
123
123
|
|
|
124
124
|
## Session Context Retrieval
|
|
125
|
-
When dispatched by Zeus, call context_get(slug, "latest", session_id=SESSION_ID)
|
|
125
|
+
When dispatched by Zeus, call context_get(slug=slug, key="latest", session_id=SESSION_ID)
|
|
126
126
|
if session_slug is provided in dispatch metadata. Apply any "remaining_tasks",
|
|
127
127
|
"current_phase", or "gotchas" from the retrieved context.
|
|
128
128
|
If context_get returns None, proceed fresh (first phase or expired session).
|
package/src/agents/apollo.md
CHANGED
|
@@ -38,7 +38,10 @@ mcp_tools:
|
|
|
38
38
|
- Generate structured summaries (not raw dumps)
|
|
39
39
|
|
|
40
40
|
### 2. External Research
|
|
41
|
-
- Web search
|
|
41
|
+
- Web search through whatever web-search MCP the USER has configured — Pantheon
|
|
42
|
+
bundles none and does not assume one. If no search tool is available, skip
|
|
43
|
+
external search and say so; never present an unverified recollection as a
|
|
44
|
+
citation.
|
|
42
45
|
- Context7 for library documentation
|
|
43
46
|
- Read URLs with webfetch for known resource URLs
|
|
44
47
|
|
|
@@ -50,7 +53,9 @@ mcp_tools:
|
|
|
50
53
|
## TOOLS NOT AVAILABLE
|
|
51
54
|
- bash - forbidden (cannot run commands)
|
|
52
55
|
- edit - forbidden (read-only agent)
|
|
53
|
-
-
|
|
56
|
+
- Web search by default - no search MCP is bundled or pre-configured. Use one
|
|
57
|
+
ONLY if the user's own config exposes it; otherwise state that external search
|
|
58
|
+
was unavailable instead of improvising.
|
|
54
59
|
|
|
55
60
|
## MCP Security
|
|
56
61
|
- Never embed credentials in URLs (grep for token=, key=, secret=)
|
package/src/agents/athena.md
CHANGED
package/src/agents/demeter.md
CHANGED
|
@@ -111,7 +111,7 @@ Compress working context with the `context-compression` skill (L1, Pantheon-nati
|
|
|
111
111
|
|
|
112
112
|
|
|
113
113
|
## Session Context Retrieval
|
|
114
|
-
When dispatched by Zeus, call context_get(slug, "latest", session_id=SESSION_ID)
|
|
114
|
+
When dispatched by Zeus, call context_get(slug=slug, key="latest", session_id=SESSION_ID)
|
|
115
115
|
if session_slug is provided in dispatch metadata. Apply any "remaining_tasks",
|
|
116
116
|
"current_phase", or "gotchas" from the retrieved context.
|
|
117
117
|
If context_get returns None, proceed fresh (first phase or expired session).
|
package/src/agents/hephaestus.md
CHANGED
|
@@ -64,7 +64,7 @@ Compress working context with the `context-compression` skill (L1, Pantheon-nati
|
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
## Session Context Retrieval
|
|
67
|
-
When dispatched by Zeus, call context_get(slug, "latest", session_id=SESSION_ID)
|
|
67
|
+
When dispatched by Zeus, call context_get(slug=slug, key="latest", session_id=SESSION_ID)
|
|
68
68
|
if session_slug is provided in dispatch metadata. Apply any "remaining_tasks",
|
|
69
69
|
"current_phase", or "gotchas" from the retrieved context.
|
|
70
70
|
If context_get returns None, proceed fresh (first phase or expired session).
|
package/src/agents/hermes.md
CHANGED
|
@@ -270,7 +270,7 @@ Implementação: `tdd-with-agents`, `incremental-implementation`, `code-review-c
|
|
|
270
270
|
|
|
271
271
|
|
|
272
272
|
## Session Context Retrieval
|
|
273
|
-
When dispatched by Zeus, call context_get(slug, "latest", session_id=SESSION_ID)
|
|
273
|
+
When dispatched by Zeus, call context_get(slug=slug, key="latest", session_id=SESSION_ID)
|
|
274
274
|
if session_slug is provided in dispatch metadata. Apply any "remaining_tasks",
|
|
275
275
|
"current_phase", or "gotchas" from the retrieved context.
|
|
276
276
|
If context_get returns None, proceed fresh (first phase or expired session).
|
package/src/agents/iris.md
CHANGED
package/src/agents/nyx.md
CHANGED
package/src/agents/themis.md
CHANGED
package/src/agents/zeus.md
CHANGED
|
@@ -5,6 +5,7 @@ reasoning_effort: medium
|
|
|
5
5
|
permission:
|
|
6
6
|
read: allow
|
|
7
7
|
edit: deny
|
|
8
|
+
hashline_edit: deny
|
|
8
9
|
bash: deny
|
|
9
10
|
task:
|
|
10
11
|
"*": allow
|
|
@@ -32,7 +33,7 @@ skills:
|
|
|
32
33
|
## Memory Protocol
|
|
33
34
|
|
|
34
35
|
**Auto-Store:** Ao receber subtask_summary, chame `memory_store()` com summary/files_changed/tests/status. Sempre.
|
|
35
|
-
**Pre-work:** `memory_search("<feature>", top_k=3)` antes de planejar qualquer coisa.
|
|
36
|
+
**Pre-work:** `memory_search(query="<feature>", top_k=3)` antes de planejar qualquer coisa.
|
|
36
37
|
|
|
37
38
|
## Golden Rule
|
|
38
39
|
|
|
@@ -79,7 +80,7 @@ O fluxo é SEMPRE: **Planejar → Especificar → Delegar → Revisar**. Zeus nu
|
|
|
79
80
|
Antes de usar a arvore de roteamento, consulte o memory:
|
|
80
81
|
|
|
81
82
|
```
|
|
82
|
-
memory_search(task_prompt, top_k=2)
|
|
83
|
+
memory_search(query=task_prompt, top_k=2)
|
|
83
84
|
→ score > 0.85?
|
|
84
85
|
SIM → usa resultado cacheado (agent, background, pattern)
|
|
85
86
|
NAO → aplica arvore de roteamento + memory_store() pra proxima vez
|
|
@@ -90,8 +91,8 @@ memory_search(task_prompt, top_k=2)
|
|
|
90
91
|
Para padroes de delegacao recorrentes, grave no KV:
|
|
91
92
|
|
|
92
93
|
```
|
|
93
|
-
kv_store("delegation:<pattern>", "{agent: ..., background: true/false}")
|
|
94
|
-
kv_get("delegation:<pattern>") → reusa decisao sem memory_search
|
|
94
|
+
kv_store(namespace="deleg", key="delegation:<pattern>", value="{agent: ..., background: true/false}")
|
|
95
|
+
kv_get(namespace="deleg", key="delegation:<pattern>") → reusa decisao sem memory_search
|
|
95
96
|
```
|
|
96
97
|
|
|
97
98
|
### Telemetria de delegacao (Nyx P1-3)
|
|
@@ -186,12 +187,12 @@ Wave announcement obrigatorio.
|
|
|
186
187
|
Limite maximo de 2 niveis de nesting: Zeus -> subagente -> sub-subagente.
|
|
187
188
|
|
|
188
189
|
```
|
|
189
|
-
depth = kv_get("deleg:depth") ?? 0
|
|
190
|
+
depth = kv_get(namespace="deleg", key="deleg:depth") ?? 0
|
|
190
191
|
if depth >= 2 → NAO delegar, ESCALAR para o usuario
|
|
191
|
-
else → kv_store("deleg:depth", depth + 1)
|
|
192
|
+
else → kv_store(namespace="deleg", key="deleg:depth", value=String(depth + 1))
|
|
192
193
|
|
|
193
194
|
Quando subagente retornar:
|
|
194
|
-
kv_store("deleg:depth", max(0, depth - 1))
|
|
195
|
+
kv_store(namespace="deleg", key="deleg:depth", value=String(max(0, depth - 1)))
|
|
195
196
|
```
|
|
196
197
|
|
|
197
198
|
Zeus (nivel 0) -> Apollo/Hermes (nivel 1) -> sub-subagente (nivel 2 max).
|
|
@@ -220,7 +221,7 @@ Quando um subagente RECUSA ou retorna "escopo fora do meu dominio" (ex: edit: de
|
|
|
220
221
|
|
|
221
222
|
- **Causa provavel:** agente errado selecionado no roteamento (match por palavra-chave, nao por capacidade).
|
|
222
223
|
- **ACAO:** RE-ROTEAR imediatamente para o agente correto — NAO retry com prompt reformulado.
|
|
223
|
-
- Registre o caso no cache de delegacao (`kv_store("deleg:<pattern>",
|
|
224
|
+
- Registre o caso no cache de delegacao (`kv_store(namespace="deleg", key="deleg:<pattern>", value=...)` para aprendizado futuro).
|
|
224
225
|
- Recusa legitima do agente = comportamento correto do guard; o defeito esta na selecao (zeus), nao no agente.
|
|
225
226
|
|
|
226
227
|
## TODO Enforcer (Auto-Retry)
|
|
@@ -237,7 +238,7 @@ Apos task_status(wait=true), verifique:
|
|
|
237
238
|
"reviewer-only", "fora do meu dominio", ou similar negativa de dominio)?
|
|
238
239
|
SIM → NAO retry com prompt rephrased.
|
|
239
240
|
RE-ROTEAR para o agente correto (ver "Taxonomia de Recusa (P0-3)").
|
|
240
|
-
Registre o caso: kv_store("deleg:<pattern>", "{agente_correto, ...}").
|
|
241
|
+
Registre o caso: kv_store(namespace="deleg", key="deleg:<pattern>", value="{agente_correto, ...}").
|
|
241
242
|
|
|
242
243
|
FALHA REAL (timeout, crash, resposta vazia, context exceeded)?
|
|
243
244
|
SIM → retry 1x com prompt rephrased (diferente, mais especifico)
|
|
@@ -284,7 +285,35 @@ in `task-result-guard.ts` (plugin `tool.execute.after` chain): an
|
|
|
284
285
|
empty/whitespace-only result is converted into an explicit error so a silent
|
|
285
286
|
child failure is never mistaken for success. No manual retry helper is required.
|
|
286
287
|
|
|
287
|
-
### /cost — pantheon_cost tool (
|
|
288
|
+
### /cost — pantheon_cost tool (disponivel com o version gate corrigido)
|
|
289
|
+
`pantheon_cost` volta a existir assim que o version gate corrigido estiver
|
|
290
|
+
instalado **e após o usuário re-rodar `init`**: em host 2.x, `auto` resolve
|
|
291
|
+
para `v2` e registra o entry de diretorio `src/plugin-v2`, que expoe
|
|
292
|
+
exatamente 3 tools — `hashline_edit`, `pantheon_cost` e `pantheon_model`
|
|
293
|
+
(V2 nao registra as 3 goal tools; ver `docs/INSTALLATION.md` ->
|
|
294
|
+
"OpenCode V1/V2 — contrato de plugin").
|
|
295
|
+
**Caveat**: a tool so reaparece para quem re-rodar `init` depois do fix. O
|
|
296
|
+
`postinstall` nao re-executa `init`, entao ate la o config existente continua
|
|
297
|
+
resolvendo para V1 e a tool segue ausente — nesse estado, tratar `/cost` como
|
|
298
|
+
indisponivel.
|
|
299
|
+
|
|
300
|
+
Causa raiz como era na tag `1.6.0-beta.4` (registro para quem ainda tem um
|
|
301
|
+
install quebrado): o host `opencode v2.0.22` rejeita caminhos de arquivo `.ts` nos
|
|
302
|
+
arrays de plugin do config (recusa `configured plugin path must be a directory`,
|
|
303
|
+
medida em runtime nesse host; o repo so registra a parafrase `must be a
|
|
304
|
+
directory`), e a selecao de geracao caia sempre em V1 — a geracao V1 escreve
|
|
305
|
+
entradas `.ts` sob a chave singular `plugin` (`config.plugin`, o bloco de merge
|
|
306
|
+
em `scripts/install/opencode.mjs`) e o host descarta todas, logo nenhum
|
|
307
|
+
modulo Pantheon entra no processo Node. **O que mudou**: `resolveOpenCodeVersion`
|
|
308
|
+
agora tem default `'auto'` nos dois call sites e no proprio signature, e `auto`
|
|
309
|
+
consulta o host. Precedencia `auto`: flag explicito > `OPENCODE_VERSION` >
|
|
310
|
+
basename `opencode2` > probe `--version` do host (major >= 2 => v2) > soft-fail
|
|
311
|
+
para V1 com warning visivel. Precedencia e criterios do probe estao em
|
|
312
|
+
`docs/INSTALLATION.md` -> "OpenCode V1/V2 — contrato de plugin".
|
|
313
|
+
|
|
314
|
+
Comportamento da tool — **NAO VERIFICADO em runtime**: em 1.6.0-beta.4 o modulo
|
|
315
|
+
(`src/pantheon/cost-command.ts`) nunca era carregado no processo, e o fix nao foi
|
|
316
|
+
exercitado end-to-end, entao este descritivo permanece nao verificado.
|
|
288
317
|
`pantheon_cost({ days?: number })` le o `opencode.db` READ-ONLY usando o backend
|
|
289
318
|
unico `node:sqlite`; se indisponivel, retorna `UNSUPPORTED`, sem fallback. Devolve tabela markdown de custo + tokens por
|
|
290
319
|
agente nos ultimos N dias (default 7). Uso: quando Nyx pedir visibilidade de
|
|
@@ -54,15 +54,21 @@ Each agent file defines overrides in its `## 🧠 Memory Protocol` section:
|
|
|
54
54
|
|
|
55
55
|
Para otimizar decisoes de delegacao e reduzir gasto de tokens:
|
|
56
56
|
|
|
57
|
-
1. **memory_search(task_prompt, top_k=2)** antes de aplicar a arvore de roteamento
|
|
57
|
+
1. **memory_search(query=task_prompt, top_k=2)** antes de aplicar a arvore de roteamento
|
|
58
58
|
2. Se score > 0.85 → reutiliza agente + background_mode do cache
|
|
59
59
|
3. Se score ≤ 0.85 → aplica regras estaticas e memory_store() com:
|
|
60
60
|
- key: deleg:<task_type>
|
|
61
|
-
- value: {agent, background, pattern}
|
|
62
|
-
- metadata: {type: "decision", score: N}
|
|
61
|
+
- value: JSON.stringify({agent, background, pattern})
|
|
62
|
+
- metadata: JSON.stringify({type: "decision", score: N})
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
`value` E `metadata` sao JSON-encoded strings — o runtime exige que o
|
|
65
|
+
chamador serialize os dois. Objeto cru em `metadata` e rejeitado pelo
|
|
66
|
+
`BeforeValidator` do servidor com `metadata must be a JSON object encoded
|
|
67
|
+
as a string (got object). json.dumps it first. Example: metadata='{"type":
|
|
68
|
+
"decision", "score": 0.9}'`.
|
|
69
|
+
|
|
70
|
+
4. **kv_store(namespace="deleg", key="deleg:<pattern>", value=...)** para padroes recorrentes de delegacao
|
|
71
|
+
5. **kv_get(namespace="deleg", key="deleg:<pattern>")** para reusar decisoes ja tomadas
|
|
66
72
|
|
|
67
73
|
Isso elimina ~300 tokens de reasoning por delegacao quando o cache acerta.
|
|
68
74
|
|
|
@@ -76,7 +82,7 @@ After every `/pantheon` council synthesis completes, Zeus stores:
|
|
|
76
82
|
memory_store({
|
|
77
83
|
namespace: "council_decisions",
|
|
78
84
|
key: "council:<yyyy-mm-dd>:<slug>",
|
|
79
|
-
value: {
|
|
85
|
+
value: JSON.stringify({
|
|
80
86
|
question: "original question",
|
|
81
87
|
specialists: ["@agent1", "@agent2"],
|
|
82
88
|
recommendation: "final recommendation",
|
|
@@ -87,20 +93,25 @@ memory_store({
|
|
|
87
93
|
themis_audit: "approved|issues",
|
|
88
94
|
precedent_used: false,
|
|
89
95
|
timestamp: "<ISO-8601>"
|
|
90
|
-
},
|
|
91
|
-
metadata: {
|
|
96
|
+
}),
|
|
97
|
+
metadata: JSON.stringify({
|
|
92
98
|
type: "council_decision",
|
|
93
99
|
specialist_count: N,
|
|
94
100
|
model_tier_used: "premium|default|fast"
|
|
95
|
-
}
|
|
101
|
+
})
|
|
96
102
|
})
|
|
97
103
|
```
|
|
98
104
|
`value` is JSON-serialized before storing (the MCP `memory_store.value` argument is a string).
|
|
105
|
+
`metadata` works the same way — it is a JSON object encoded as a **string**, so pass
|
|
106
|
+
`JSON.stringify({...})`. A raw object is rejected with `metadata must be a JSON object
|
|
107
|
+
encoded as a string (got object). json.dumps it first. Example: metadata='{"type":
|
|
108
|
+
"decision", "score": 0.9}'` (enforced by a `BeforeValidator` in
|
|
109
|
+
`memory_mcp.py`). **Both `value` and `metadata` must be serialized by the caller.**
|
|
99
110
|
|
|
100
111
|
### Read Path (Precedent Fast-Path)
|
|
101
112
|
Before dispatching a new council, Zeus runs:
|
|
102
113
|
```
|
|
103
|
-
memory_search(question, top_k=2, namespace="council_decisions")
|
|
114
|
+
memory_search(query=question, top_k=2, namespace="council_decisions")
|
|
104
115
|
```
|
|
105
116
|
|
|
106
117
|
Result interpretation:
|
|
@@ -56,19 +56,27 @@ All session state lives in **pantheon-persistence** (namespace `checkpoint:<slug
|
|
|
56
56
|
No file I/O, no checkpoint_session.py — TTL (4h) handles cleanup automatically.
|
|
57
57
|
|
|
58
58
|
### Heartbeat Check
|
|
59
|
-
- If `context_get(slug, "heartbeat")` returns a checkin older than 300s, log a stall warning and resume
|
|
59
|
+
- If `context_get(slug=slug, key="heartbeat", session_id=SESSION_ID)` returns a checkin older than 300s, log a stall warning and resume
|
|
60
60
|
- Write heartbeat after every anti-stall recovery action:
|
|
61
61
|
```
|
|
62
|
-
context_save(slug, "heartbeat",
|
|
62
|
+
context_save(slug=slug, key="heartbeat", content=JSON.stringify({"status": "alive", "last_action": "...", "turn_count": N}), session_id=SESSION_ID)
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
### Checkpoint Auto-Save (Pré-Compactação)
|
|
66
66
|
Before ANY delegate dispatch, save a checkpoint:
|
|
67
67
|
```
|
|
68
|
-
context_save(slug, "phase:N",
|
|
69
|
-
"phase":
|
|
68
|
+
context_save(slug=slug, key="phase:N", content=JSON.stringify({
|
|
69
|
+
"phase": {"current": N, "total": M, "name": "..."},
|
|
70
|
+
"turn_count": N, "agent": "...", "summary": "..."
|
|
70
71
|
}), session_id=SESSION_ID)
|
|
71
72
|
```
|
|
73
|
+
**`content.phase` must be an OBJECT, not a bare number.** `{"phase": 1}` is rejected with
|
|
74
|
+
`context_save: content.phase must be an object (got number)`. The server reads `current`
|
|
75
|
+
and `total` (non-negative ints) and `name` (string, max 256 **bytes** — the guard
|
|
76
|
+
measures `len(value.encode())`, so multi-byte characters count as more than one). That
|
|
77
|
+
set is not closed: any other key inside `phase` is accepted and ignored, never an
|
|
78
|
+
error. The `"phase:N"` slot KEY above is unrelated to `content.phase` and stays a plain
|
|
79
|
+
string.
|
|
72
80
|
All checkpoints auto-expire after 4h (TTL=14400).
|
|
73
81
|
|
|
74
82
|
### Gatilho de Pré-Compactação (Anti-perda de estado)
|
|
@@ -78,15 +86,15 @@ o Zeus DEVE salvar o estado atual:
|
|
|
78
86
|
2. Salve heartbeat + phase atual + tarefas pendentes
|
|
79
87
|
3. Só então permita que a compactação prossiga
|
|
80
88
|
```
|
|
81
|
-
# Ao iniciar sessão:
|
|
82
|
-
result = context_save(slug, "init", session_state)
|
|
89
|
+
# Ao iniciar sessão (session_id é REQUIRED — use o id da sessão corrente):
|
|
90
|
+
result = context_save(slug=slug, key="init", content=JSON.stringify(session_state), session_id=SESSION_ID)
|
|
83
91
|
SESSION_ID = result.session_id # ← guarde para toda a sessão
|
|
84
92
|
|
|
85
93
|
# Antes de CADA delegação:
|
|
86
|
-
context_save(slug, f"pre:{agent}", current_state, session_id=SESSION_ID)
|
|
94
|
+
context_save(slug=slug, key=f"pre:{agent}", content=JSON.stringify(current_state), session_id=SESSION_ID)
|
|
87
95
|
|
|
88
96
|
# Após retorno do agente:
|
|
89
|
-
context_save(slug, f"post:{agent}", result_state, session_id=SESSION_ID)
|
|
97
|
+
context_save(slug=slug, key=f"post:{agent}", content=JSON.stringify(result_state), session_id=SESSION_ID)
|
|
90
98
|
```
|
|
91
99
|
Isso garante que o estado sobreviva à compactação — o "latest" pointer
|
|
92
100
|
sempre aponta para o checkpoint mais recente, mesmo após compactação.
|
|
@@ -95,9 +103,9 @@ sempre aponta para o checkpoint mais recente, mesmo após compactação.
|
|
|
95
103
|
### Context Retrieval
|
|
96
104
|
Next-phase agents retrieve previous context via:
|
|
97
105
|
```
|
|
98
|
-
context_get(slug,
|
|
99
|
-
context_get(slug, "phase:3")
|
|
100
|
-
context_list(slug)
|
|
106
|
+
context_get(slug=slug, session_id=SESSION_ID) # most recent checkpoint ("latest" is the default key)
|
|
107
|
+
context_get(slug=slug, key="phase:3", session_id=SESSION_ID) # specific phase
|
|
108
|
+
context_list(slug=slug, session_id=SESSION_ID) # all checkpoints
|
|
101
109
|
```
|
|
102
110
|
|
|
103
111
|
### Long-Session Progress
|