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.
Files changed (51) hide show
  1. package/AGENTS.md +42 -23
  2. package/CHANGELOG.md +41 -0
  3. package/README.md +114 -12
  4. package/README.pt-BR.md +15 -6
  5. package/bin/pantheon-init.mjs +26 -3
  6. package/package.json +5 -6
  7. package/plugin.json +1 -1
  8. package/pyproject.toml +6 -6
  9. package/scripts/{code_mode_server.py → code_mode.py} +2 -2
  10. package/scripts/doctor.mjs +151 -1
  11. package/scripts/install/config-migration.mjs +80 -2
  12. package/scripts/install/health-check.mjs +12 -17
  13. package/scripts/install/opencode-version.mjs +246 -4
  14. package/scripts/install/opencode.mjs +250 -119
  15. package/scripts/install-mcp.mjs +28 -9
  16. package/scripts/{mcp_persistence_server.py → mcp_persistence.py} +1 -1
  17. package/{src/mcp/mcp_resources_server.py → scripts/mcp_resources.py} +1 -1
  18. package/scripts/{memory_mcp_server.py → memory_mcp.py} +1 -1
  19. package/scripts/probe-context-rehydrate.mjs +2 -2
  20. package/scripts/run-node-tests.mjs +189 -0
  21. package/scripts/sync-artifacts.mjs +5 -5
  22. package/scripts/test-opencode-v2-sandbox.sh +21 -21
  23. package/skills-lock.json +2 -2
  24. package/src/agents/aphrodite.md +1 -1
  25. package/src/agents/apollo.md +7 -2
  26. package/src/agents/athena.md +1 -0
  27. package/src/agents/demeter.md +1 -1
  28. package/src/agents/hephaestus.md +1 -1
  29. package/src/agents/hermes.md +1 -1
  30. package/src/agents/iris.md +1 -0
  31. package/src/agents/nyx.md +1 -0
  32. package/src/agents/themis.md +1 -0
  33. package/src/agents/zeus.md +39 -10
  34. package/src/instructions/memory-protocol.instructions.md +21 -10
  35. package/src/instructions/zeus-anti-stall.instructions.md +19 -11
  36. package/src/instructions/zeus-council-synthesis.instructions.md +2 -2
  37. package/src/mcp/{code_mode_server.py → code_mode.py} +2 -2
  38. package/src/mcp/eval_store.py +2 -2
  39. package/src/mcp/{mcp_persistence_server.py → mcp_persistence.py} +1 -1
  40. package/{scripts/mcp_resources_server.py → src/mcp/mcp_resources.py} +1 -1
  41. package/src/mcp/{memory_mcp_server.py → memory_mcp.py} +1 -1
  42. package/src/pantheon/v2-unsupported.d.mts +11 -0
  43. package/src/pantheon/v2-unsupported.mjs +113 -0
  44. package/src/plugin-v2.ts +149 -53
  45. package/src/plugins/tui/dist/tui.js +1 -1
  46. package/src/plugins/tui/package-lock.json +2 -2
  47. package/src/plugins/tui/package.json +1 -1
  48. package/src/skills/auto-continue/SKILL.md +4 -4
  49. package/src/skills/context-compression/SKILL.md +1 -1
  50. package/opencode.json +0 -58
  51. /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
- 'mcp_resources_server.py': () => join(ROOT, 'src', 'mcp', 'mcp_resources_server.py'),
35
- 'code_mode_server.py': () => join(ROOT, 'src', 'mcp', 'code_mode_server.py'),
36
- 'memory_mcp_server.py': () => join(ROOT, 'src', 'mcp', 'memory_mcp_server.py'),
37
- 'mcp_persistence_server.py': () => join(ROOT, 'src', 'mcp', 'mcp_persistence_server.py'),
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
- 'pantheon_vision_server.py': () => join(ROOT, 'src', 'mcp', 'pantheon_vision_server.py'),
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
- "code_mode_server.py",
245
- "memory_mcp_server.py",
246
- "mcp_persistence_server.py",
247
- "mcp_resources_server.py",
248
- "pantheon_vision_server.py",
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
- "code_mode_server.py",
519
- "memory_mcp_server.py",
520
- "mcp_persistence_server.py",
521
- "mcp_resources_server.py",
522
- "pantheon_vision_server.py",
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": "code_mode_server.py",
646
- "pantheon-memory": "memory_mcp_server.py",
647
- "pantheon-persistence": "mcp_persistence_server.py",
648
- "pantheon-resources": "mcp_resources_server.py",
649
- "pantheon-vision": "pantheon_vision_server.py",
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/mcp_resources_server.py"
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": "code_mode_server.py",
1017
- "pantheon-memory": "memory_mcp_server.py",
1018
- "pantheon-persistence": "mcp_persistence_server.py",
1019
- "pantheon-resources": "mcp_resources_server.py",
1020
- "pantheon-vision": "pantheon_vision_server.py",
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": "4fe256d44ee9828d622a0d8b63cfc6bb39a317189a9ccf19494a6efd4527a7a9",
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": "ebc721c715c1fa20e4ebf5177007fde94e001540b16cef4316393a665633c16f",
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",
@@ -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).
@@ -38,7 +38,10 @@ mcp_tools:
38
38
  - Generate structured summaries (not raw dumps)
39
39
 
40
40
  ### 2. External Research
41
- - Web search via bifrost MCP tools for documentation, blog posts, GitHub repos
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
- - bifrost - use bifrost MCP tools for search
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=)
@@ -15,6 +15,7 @@ skills:
15
15
  - memory-bank
16
16
  permission:
17
17
  edit: deny
18
+ hashline_edit: deny
18
19
  bash: deny
19
20
  read: allow
20
21
  grep: allow
@@ -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).
@@ -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).
@@ -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).
@@ -11,6 +11,7 @@ permission:
11
11
  gh *: allow
12
12
  webfetch: allow
13
13
  edit: deny
14
+ hashline_edit: deny
14
15
  task:
15
16
  "*": deny
16
17
  temperature: 0.2
package/src/agents/nyx.md CHANGED
@@ -9,6 +9,7 @@ permission:
9
9
  bash: allow
10
10
  webfetch: allow
11
11
  edit: deny
12
+ hashline_edit: deny
12
13
  task:
13
14
  "*": deny
14
15
  temperature: 0.1
@@ -8,6 +8,7 @@ permission:
8
8
  grep: allow
9
9
  bash: allow
10
10
  edit: deny
11
+ hashline_edit: deny
11
12
  task:
12
13
  "*": deny
13
14
  temperature: 0.1
@@ -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>", ...)` para aprendizado futuro).
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 (WIRED no plugin)
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
- 4. **kv_store("deleg:<pattern>", ...)** para padroes recorrentes de delegacao
65
- 5. **kv_get("deleg:<pattern>")** para reusar decisoes ja tomadas
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", json({"status": "alive", "last_action": "...", "turn_count": N}))
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", json({
69
- "phase": N, "turn_count": N, "agent": "...", "summary": "..."
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, "latest") # most recent checkpoint
99
- context_get(slug, "phase:3") # specific phase
100
- context_list(slug) # all checkpoints
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