pantheon-opencode 1.5.0-beta.8 → 1.5.0
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 +14 -63
- package/CHANGELOG.md +320 -0
- package/README.md +14 -27
- package/README.pt-BR.md +5 -6
- package/commands/pantheon.md +1 -35
- package/package.json +1 -2
- package/plugin.json +1 -1
- package/pyproject.toml +2 -5
- package/scripts/doctor.mjs +32 -3
- package/scripts/install/plugin.mjs +9 -12
- package/scripts/mcp_persistence_server.py +1 -1
- package/scripts/sync-tui.mjs +8 -11
- package/src/agents/apollo.md +1 -0
- package/src/agents/gaia.md +1 -0
- package/src/agents/zeus.md +9 -52
- package/src/instructions/memory-protocol.instructions.md +6 -3
- package/src/instructions/zeus-anti-stall.instructions.md +2 -4
- package/src/instructions/zeus-council-synthesis.instructions.md +3 -28
- package/src/instructions/zeus-timeout-retry.instructions.md +3 -28
- package/src/mcp/mcp_persistence_server.py +1 -1
- package/src/mcp/toon_codec.py +2 -4
- package/src/pantheon/background-job-board.ts +4 -4
- package/src/pantheon/{delegation-compaction.ts → compaction-context.ts} +10 -100
- package/src/pantheon/cost-command.ts +1 -2
- package/src/pantheon/delegation-enforce.ts +59 -14
- package/src/pantheon/goal-loop.ts +1 -1
- package/src/pantheon/goal-store.ts +1 -1
- package/src/pantheon/idle-continuation.ts +4 -6
- package/src/pantheon/model-command.ts +1 -2
- package/src/pantheon/native-task-status.ts +2 -2
- package/src/pantheon/presets.d.mts +0 -20
- package/src/pantheon/presets.mjs +0 -64
- package/src/pantheon/session-guard.ts +2 -2
- package/src/pantheon/session-id.ts +30 -0
- package/src/pantheon/task-result-guard.ts +2 -3
- package/src/pantheon/todo-enforcer.ts +3 -3
- package/src/pantheon/tool-context.ts +23 -0
- package/src/pantheon/v2-bridge.ts +1 -4
- package/src/plugin-v2.ts +8 -21
- package/src/plugin.ts +61 -245
- package/src/plugins/pantheon-hooks.ts +1 -1
- package/src/plugins/tui/dist/tui.d.ts +229 -77
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +509 -259
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +2 -2
- package/src/plugins/tui/src/index.tsx +753 -279
- package/src/routing.yml +0 -16
- package/src/skills/context-compression/SKILL.md +1 -1
- package/scripts/generate-preset-docs.mjs +0 -300
- package/scripts/generate-prompts.sh +0 -222
- package/scripts/hash_verify.py +0 -192
- package/scripts/install/component.mjs +0 -24
- package/scripts/install/migrate-config.mjs +0 -302
- package/scripts/test-context-compression.sh +0 -169
- package/src/pantheon/auto-wake.ts +0 -85
- package/src/pantheon/chat-reminders.ts +0 -9
- package/src/pantheon/comment-checker.ts +0 -48
- package/src/pantheon/compaction-assert.ts +0 -160
- package/src/pantheon/compaction-handoff.ts +0 -304
- package/src/pantheon/cost-tracker.ts +0 -108
- package/src/pantheon/delegate-manager.ts +0 -766
- package/src/pantheon/delegation-classifier.ts +0 -307
- package/src/pantheon/delegation-finalize.ts +0 -406
- package/src/pantheon/delegation-notify.ts +0 -212
- package/src/pantheon/delegation-toast.ts +0 -73
- package/src/pantheon/delegation.ts +0 -1337
- package/src/pantheon/dispatch-guard.ts +0 -109
- package/src/pantheon/native-probe.ts +0 -234
- package/src/pantheon/permission-eval.ts +0 -284
- package/src/pantheon/preemptive-compact.ts +0 -76
- package/src/pantheon/provider-capabilities.ts +0 -221
- package/src/pantheon/provider-stream-tracker.ts +0 -182
- package/src/pantheon/retry-policy.ts +0 -212
- package/src/pantheon/step-cap.ts +0 -110
- package/src/pantheon/token-opt.ts +0 -689
- package/src/pantheon/toon-codec.ts +0 -481
- package/src/pantheon/v2-events.ts +0 -148
- package/src/pantheon/v2-hooks.ts +0 -287
- package/src/pantheon/v2-tool-definitions.ts +0 -331
- package/src/pantheon/zeus-delegate-with-retry.ts +0 -406
- package/src/plugins/tui/dist/tui.tsx +0 -2777
package/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pantheon"
|
|
7
|
-
version = "1.5.0
|
|
7
|
+
version = "1.5.0"
|
|
8
8
|
description = "Multi-agent orchestration framework"
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
dependencies = []
|
|
@@ -37,10 +37,8 @@ ignore = ["E501"]
|
|
|
37
37
|
"scripts/scrub-secrets.py" = ["N999"]
|
|
38
38
|
# These modules defer optional/heavy imports until the corresponding command is used.
|
|
39
39
|
"scripts/vector_memory/*.py" = ["PLC0415"]
|
|
40
|
-
"tests/test_code_mode_args.py" = ["PLC0415"]
|
|
41
40
|
"tests/test_code_mode_sandbox.py" = ["PLC0415", "N806"]
|
|
42
41
|
"tests/test_code_mode_server.py" = ["PLC0415"]
|
|
43
|
-
"tests/test_eval_store.py" = ["PLC0415", "PLR2004"]
|
|
44
42
|
"tests/test_memory_mcp_server.py" = ["PLC0415", "PLR2004"]
|
|
45
43
|
# Existing public APIs and test fixtures are intentionally broad/simple; refactoring
|
|
46
44
|
# them would be behavior-affecting rather than a mechanical lint correction.
|
|
@@ -54,8 +52,7 @@ ignore = ["E501"]
|
|
|
54
52
|
"scripts/vector_memory/query.py" = ["PLR0913"]
|
|
55
53
|
"src/mcp/eval_store.py" = ["PLR2004"]
|
|
56
54
|
"tests/test_code_mode_frontmatter.py" = ["PLR2004"]
|
|
57
|
-
"tests/
|
|
58
|
-
"tests/test_eval_static.py" = ["PLR2004"]
|
|
55
|
+
"tests/test_eval.py" = ["PLR2004"]
|
|
59
56
|
"tests/test_mcp_persistence_server.py" = ["PLR2004"]
|
|
60
57
|
|
|
61
58
|
[tool.mypy]
|
package/scripts/doctor.mjs
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
import { spawn as spawnAsync, spawnSync } from 'node:child_process'
|
|
25
25
|
import { createHash } from 'node:crypto'
|
|
26
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
26
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
27
27
|
import { homedir } from 'node:os'
|
|
28
28
|
import { basename, dirname, join, resolve } from 'node:path'
|
|
29
29
|
import { fileURLToPath } from 'node:url'
|
|
@@ -959,6 +959,35 @@ function checkVenvLayer(args) {
|
|
|
959
959
|
// Check F.2: Code-Mode Scripts Directory
|
|
960
960
|
// ---------------------------------------------------------------------------
|
|
961
961
|
|
|
962
|
+
/**
|
|
963
|
+
* Return whether *dir* contains an executable code-mode target.
|
|
964
|
+
*
|
|
965
|
+
* Mirrors `_has_usable_scripts` in `scripts/code_mode_server.py`: a directory
|
|
966
|
+
* is only a valid resolver candidate when it is a real (non-symlink) directory
|
|
967
|
+
* containing a regular `.py`/`.sh` direct child. This prevents an empty project
|
|
968
|
+
* overlay from masking a lower-priority installation that actually ships
|
|
969
|
+
* scripts. Keep both implementations in sync (see tests/test_doctor_layers.mjs).
|
|
970
|
+
*/
|
|
971
|
+
function hasUsableScripts(dir) {
|
|
972
|
+
try {
|
|
973
|
+
if (!existsSync(dir)) return false
|
|
974
|
+
const stat = lstatSync(dir)
|
|
975
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) return false
|
|
976
|
+
return readdirSync(dir).some((name) => {
|
|
977
|
+
if (name.startsWith('.')) return false
|
|
978
|
+
const lower = name.toLowerCase()
|
|
979
|
+
if (!lower.endsWith('.py') && !lower.endsWith('.sh')) return false
|
|
980
|
+
try {
|
|
981
|
+
return statSync(join(dir, name)).isFile()
|
|
982
|
+
} catch {
|
|
983
|
+
return false
|
|
984
|
+
}
|
|
985
|
+
})
|
|
986
|
+
} catch {
|
|
987
|
+
return false
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
962
991
|
/**
|
|
963
992
|
* Verify the code-mode scripts directory exists for the resolved runtime
|
|
964
993
|
* root (project → <target>/.opencode, otherwise the global config dir).
|
|
@@ -977,7 +1006,7 @@ export function checkCodeModeDir(args) {
|
|
|
977
1006
|
// install (.opencode present) gets its runtime dir created when missing.
|
|
978
1007
|
// Only targets with no project layout fall back to the global config dir.
|
|
979
1008
|
const codeModeDir =
|
|
980
|
-
projectCandidates.find(
|
|
1009
|
+
projectCandidates.find(hasUsableScripts) ??
|
|
981
1010
|
(existsSync(join(target, '.opencode'))
|
|
982
1011
|
? projectCandidates[0]
|
|
983
1012
|
: join(resolveOpenCodeConfigDir(env), '.pantheon', 'code-mode'))
|
|
@@ -1003,7 +1032,7 @@ export function resolveCodeModeDir(args) {
|
|
|
1003
1032
|
join(target, '.opencode', '.pantheon', 'code-mode'),
|
|
1004
1033
|
join(target, '.pantheon', 'code-mode'),
|
|
1005
1034
|
]
|
|
1006
|
-
const projectDir = projectCandidates.find(
|
|
1035
|
+
const projectDir = projectCandidates.find(hasUsableScripts)
|
|
1007
1036
|
if (projectDir) return projectDir
|
|
1008
1037
|
return join(resolveOpenCodeConfigDir(env), '.pantheon', 'code-mode')
|
|
1009
1038
|
}
|
|
@@ -8,8 +8,10 @@ import { isAbsolute, join, resolve } from 'node:path'
|
|
|
8
8
|
import { ROOT, writeIfChanged } from './shared.mjs'
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Copy a plugin's runtime payload (
|
|
12
|
-
* package
|
|
11
|
+
* Copy a plugin's runtime payload (the bundled dist/*, package.json and
|
|
12
|
+
* package-lock.json) from source to destination. Only the bundle is copied —
|
|
13
|
+
* the raw TSX source is never shipped to the destination, so the plugin can
|
|
14
|
+
* be modularized without the loader needing relative imports.
|
|
13
15
|
* @param {string} srcDir - Source plugin directory (e.g. ROOT/src/plugins/tui)
|
|
14
16
|
* @param {string} dstDir - Destination plugin directory
|
|
15
17
|
* @param {object} [options]
|
|
@@ -22,16 +24,11 @@ export function copyPluginFiles(srcDir, dstDir, { dryRun = false } = {}) {
|
|
|
22
24
|
if (!dryRun) mkdirSync(dstDir, { recursive: true })
|
|
23
25
|
if (!dryRun) mkdirSync(join(dstDir, 'dist'), { recursive: true })
|
|
24
26
|
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
readFileSync(srcIdx, 'utf8'),
|
|
31
|
-
dryRun,
|
|
32
|
-
)
|
|
33
|
-
if (idxStatus === 'created') result.created++
|
|
34
|
-
else result.skipped++
|
|
27
|
+
// Drop legacy raw copies from previous installs (root index.tsx + dist/tui.tsx).
|
|
28
|
+
// The loader consumes only the bundle, so stale raws must not linger.
|
|
29
|
+
if (!dryRun) {
|
|
30
|
+
rmSync(join(dstDir, 'index.tsx'), { force: true })
|
|
31
|
+
rmSync(join(dstDir, 'dist', 'tui.tsx'), { force: true })
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
// Copy dist/* files
|
|
@@ -1323,7 +1323,7 @@ def build_rehydration_blocks(checkpoint: dict) -> list[str] | None:
|
|
|
1323
1323
|
"""Build deterministic rehydration blocks from a checkpoint dict.
|
|
1324
1324
|
|
|
1325
1325
|
Pure string templating — no LLM, no embeddings. Mirrors the
|
|
1326
|
-
``<mission_context>`` convention of
|
|
1326
|
+
``<mission_context>`` convention of the compaction-context builder: only a
|
|
1327
1327
|
non-done goal is re-emitted. Returns None when nothing critical remains.
|
|
1328
1328
|
"""
|
|
1329
1329
|
if not isinstance(checkpoint, dict) or not checkpoint:
|
package/scripts/sync-tui.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* If the user hasn't initialized yet → do nothing (silent exit).
|
|
11
11
|
*/
|
|
12
12
|
import { execSync } from 'node:child_process'
|
|
13
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
13
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
14
14
|
import { homedir } from 'node:os'
|
|
15
15
|
import { join } from 'node:path'
|
|
16
16
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
@@ -58,9 +58,9 @@ function resolveConfigDir() {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
|
-
* Copy plugin runtime files (dist/*, package.json,
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
* Copy plugin runtime files (the bundled dist/*, package.json,
|
|
62
|
+
* package-lock.json) from srcDir to dstDir. Skips files that are byte-identical.
|
|
63
|
+
* The raw TSX source is never copied — the loader consumes only the bundle.
|
|
64
64
|
* @returns {{ created: number, skipped: number }}
|
|
65
65
|
*/
|
|
66
66
|
function copyPluginFiles(srcDir, dstDir) {
|
|
@@ -69,13 +69,10 @@ function copyPluginFiles(srcDir, dstDir) {
|
|
|
69
69
|
mkdirSync(dstDir, { recursive: true })
|
|
70
70
|
mkdirSync(join(dstDir, 'dist'), { recursive: true })
|
|
71
71
|
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (writeIfChanged(join(dstDir, 'index.tsx'), content)) result.created++
|
|
77
|
-
else result.skipped++
|
|
78
|
-
}
|
|
72
|
+
// Drop legacy raw copies from previous installs (root index.tsx + dist/tui.tsx).
|
|
73
|
+
// The loader consumes only the bundle, so stale raws must not linger.
|
|
74
|
+
rmSync(join(dstDir, 'index.tsx'), { force: true })
|
|
75
|
+
rmSync(join(dstDir, 'dist', 'tui.tsx'), { force: true })
|
|
79
76
|
|
|
80
77
|
// dist/*
|
|
81
78
|
const distSrc = join(srcDir, 'dist')
|
package/src/agents/apollo.md
CHANGED
package/src/agents/gaia.md
CHANGED
package/src/agents/zeus.md
CHANGED
|
@@ -144,33 +144,11 @@ task_status(task_id="ses_xxx", wait=true)
|
|
|
144
144
|
-> bloqueia ate completar: { state: "completed", task_result: "..." }
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
-
###
|
|
147
|
+
### DELEGATION_RULES (native task API)
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
pantheon_delegate({prompt, agent, description?, read_only?})
|
|
153
|
-
-> cria sessao filha, registra no board, retorna alias: [apo-1]
|
|
154
|
-
pantheon_delegation_read({id: "apo-1"})
|
|
155
|
-
-> BLOQUEIA ate o job terminar, retorna o report markdown, marca reconciled
|
|
156
|
-
-> durante a espera, mostra o que o agente esta fazendo: seccao final "## Agent Activity"
|
|
157
|
-
(amostra as messages do child a cada ~2s; tool calls + args truncados / texto)
|
|
158
|
-
pantheon_delegation_list()
|
|
159
|
-
-> lista jobs da sessao, com [unread] para jobs terminados nao lidos
|
|
160
|
-
-> jobs RUNNING mostram "last activity:" (ultima acao visivel do child)
|
|
161
|
-
-> fail-open: sem suporte a messages, formato atual inalterado
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
- **Read-only enforcement:** agentes read-only (apollo, gaia — `read_only_agents` no routing.yml) tem `edit`, `write`, `bash`, `task` NEGADOS dentro da sessao delegada (o guard de `tool.execute.before` THROW com mensagem acionavel). Use `read_only: true` no delegate para qualquer agente de investigacao.
|
|
165
|
-
- **Depth-2 hard-enforced:** sessao read-only NAO cria subagentes (`task` bloqueado) — investigacao nunca vira arvore.
|
|
166
|
-
- **Compactacao:** jobs em voo (running + unread terminal) sao injetados no contexto de compactacao via `experimental.session.compacting` — delegacao em andamento nao se perde ao compactar.
|
|
167
|
-
|
|
168
|
-
### DELEGATION_RULES (plugin path)
|
|
169
|
-
|
|
170
|
-
1. **NUNCA** polle `pantheon_delegation_list` para checar se um job terminou.
|
|
171
|
-
2. Voce NAO recebe notificacao injetada no chat — politica do usuario: ZERO `task-notification` no transcript. Visibilidade de conclusao: marcador `[unread]` em `pantheon_delegation_list`, `pantheon_delegation_read({id})`, toasts TUI (pantheon-hooks) e carry-forward de compactacao.
|
|
172
|
-
3. Use `pantheon_delegation_read({id})` APENAS para fan-in explicito / recuperacao de resultado sob demanda (bloqueia ate o fim).
|
|
173
|
-
4. `pantheon_delegation_list` e para diagnostico, nao para polling.
|
|
149
|
+
1. Todo dispatch usa `task(background=true, subagent_type=..., prompt=...)`.
|
|
150
|
+
2. Recolha com `task_status(task_id=..., wait=true)` — bloqueia ate o fim, sem polling.
|
|
151
|
+
3. Use `task_status(wait=true)` apenas para fan-in explicito / recuperacao sob demanda.
|
|
174
152
|
|
|
175
153
|
### Background (sempre usar)
|
|
176
154
|
- **Apollo, Hermes, Aphrodite, Demeter, Hephaestus, Prometheus**
|
|
@@ -301,32 +279,11 @@ vão a idle com todos incompletos. Guards (todos no `src/pantheon/todo-enforcer.
|
|
|
301
279
|
|
|
302
280
|
## Wave 4 (PR #46): Empty-Result Retry + /cost + Themis Tier
|
|
303
281
|
|
|
304
|
-
### Empty-Result Retry (
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
```
|
|
310
|
-
import { createDispatchGuard } from '.../src/pantheon/dispatch-guard.ts' # via code-mode ou subagente
|
|
311
|
-
|
|
312
|
-
guard = createDispatchGuard({ retryOnEmpty: true, logger: { warn: console.warn } })
|
|
313
|
-
|
|
314
|
-
# classificar resultado de task_status/wait:
|
|
315
|
-
# 'content' → tem texto, pronto
|
|
316
|
-
# 'empty-mode1' → SEM texto E SEM tokens (nada voltou) → RETRY 1x
|
|
317
|
-
# 'empty-mode2' → SEM texto, MAS tokens (raciocinou, perdeu a parte de texto;
|
|
318
|
-
# assinatura da falha themis Wave-2) → RETRY 1x
|
|
319
|
-
|
|
320
|
-
out = await guard.maybeRetry(result, async () => { ...task() de novo... })
|
|
321
|
-
# out.retried=true se redisparou; CAP DURO de 1 retry — nunca 2x.
|
|
322
|
-
# Se o retry voltar vazio, `out.retried=false` — NAO tente de novo: escale.
|
|
323
|
-
```
|
|
324
|
-
Regra: retry 1x APENAS em `empty-mode1`/`empty-mode2`. Resultado com conteudo
|
|
325
|
-
nunca redispara. Apos 1 retry vazio → escalate (mesma regra do TODO Enforcer).
|
|
326
|
-
|
|
327
|
-
> **Waves DEVEM usar `zeusDelegateWithRetry`** (`src/pantheon/zeus-delegate-with-retry.ts`) — encapsula `delegate → waitForTerminal → classify → retry 1x → escalate` com cap duro 1; se ainda vazio throw `ZeusEscalationError` (escale: tente outro agente, simplifique, manual).
|
|
328
|
-
> Uso alto-nível: `import { zeusDelegateWithRetry } from './pantheon/zeus-delegate-with-retry.ts'` → `await zeusDelegateWithRetry({ board, client, sessionID, agent, prompt })`.
|
|
329
|
-
> Uso baixo-nível: `createZeusRetryHelper().executeWithRetry(first, () => secondRead())` — classifica empty e decide retry/escalate (mesmo cap 1).
|
|
282
|
+
### Empty-Result Retry (runtime — automatic)
|
|
283
|
+
Empty-response detection for native `task()` subagent calls is wired at runtime
|
|
284
|
+
in `task-result-guard.ts` (plugin `tool.execute.after` chain): an
|
|
285
|
+
empty/whitespace-only result is converted into an explicit error so a silent
|
|
286
|
+
child failure is never mistaken for success. No manual retry helper is required.
|
|
330
287
|
|
|
331
288
|
### /cost — pantheon_cost tool (WIRED no plugin)
|
|
332
289
|
`pantheon_cost({ days?: number })` le o `opencode.db` READ-ONLY usando o backend
|
|
@@ -84,7 +84,9 @@ memory_store({
|
|
|
84
84
|
agreements: ["point1", "point2"],
|
|
85
85
|
divergences: [{"issue": "...", "resolution": "..."}],
|
|
86
86
|
response_rate: "X of Y",
|
|
87
|
-
themis_audit: "approved|issues"
|
|
87
|
+
themis_audit: "approved|issues",
|
|
88
|
+
precedent_used: false,
|
|
89
|
+
timestamp: "<ISO-8601>"
|
|
88
90
|
},
|
|
89
91
|
metadata: {
|
|
90
92
|
type: "council_decision",
|
|
@@ -93,6 +95,7 @@ memory_store({
|
|
|
93
95
|
}
|
|
94
96
|
})
|
|
95
97
|
```
|
|
98
|
+
`value` is JSON-serialized before storing (the MCP `memory_store.value` argument is a string).
|
|
96
99
|
|
|
97
100
|
### Read Path (Precedent Fast-Path)
|
|
98
101
|
Before dispatching a new council, Zeus runs:
|
|
@@ -103,8 +106,8 @@ memory_search(question, top_k=2, namespace="council_decisions")
|
|
|
103
106
|
Result interpretation:
|
|
104
107
|
| Score | Age | Action |
|
|
105
108
|
|-------|-----|--------|
|
|
106
|
-
| > 0.85 | < 30 days | Return precedent as fast-path answer. Skip council dispatch entirely. |
|
|
107
|
-
| > 0.85 | >= 30 days | Return with warning "Reavaliar se contexto mudou" + proceed with council |
|
|
109
|
+
| > 0.85 | < 30 days | Return precedent verbatim (note "⚠️ Decisão de [data] — reavaliar se contexto mudou") as fast-path answer. Skip council dispatch entirely. |
|
|
110
|
+
| > 0.85 | >= 30 days | Return with warning "Reavaliar se contexto mudou — decisão tem mais de 30 dias" + proceed with council |
|
|
108
111
|
| 0.5 - 0.85 | Any | Include as context for specialists but still dispatch council |
|
|
109
112
|
| < 0.5 | Any | Ignore, proceed with fresh council |
|
|
110
113
|
|
|
@@ -15,7 +15,7 @@ You MUST self-monitor for these stall conditions:
|
|
|
15
15
|
| Symptom | Detection Rule | Recovery Action |
|
|
16
16
|
|---------|---------------|-----------------|
|
|
17
17
|
| Silent loop | 3+ consecutive turns with no tool call AND no visible progress | Output `[STALL_DETECTED]` and re-read your task definition. If still stuck, escalate to user with: "I appear to be stuck on [task]. Options: (1) retry with different approach, (2) delegate to specialist, (3) simplify scope." |
|
|
18
|
-
| Delegation black hole | Agent dispatched but no response after 2x the timeout from routing.yml | Log the hang, cancel via `cancel_task`,
|
|
18
|
+
| Delegation black hole | Agent dispatched but no response after 2x the timeout from routing.yml | Log the hang, cancel via `cancel_task`, then follow the fallback chain in `## ⏱️ Timeout & Retry Enforcement` |
|
|
19
19
|
| Circular delegation | Same specialist re-dispatched for same task 2+ times without progress | Break cycle: dispatch to different specialist OR escalate to user |
|
|
20
20
|
| Idle after completion | All background tasks completed but no synthesis/next step for 2+ turns | Force synthesis: summarize all completed results and propose next action |
|
|
21
21
|
| Context thrash | Re-reading same files repeatedly without new action | Stop re-reading. State: "Already have context on [file]. Proceeding with [action]." |
|
|
@@ -41,9 +41,7 @@ When a delegation fails (timeout, empty response, error):
|
|
|
41
41
|
|
|
42
42
|
2. **Retry ONCE** with rephrased prompt — add: "Previous attempt failed with: [error]. Adjusted approach: [what changed]."
|
|
43
43
|
|
|
44
|
-
3. **If retry also fails** →
|
|
45
|
-
- Dispatch to fallback agent (from routing.yml)
|
|
46
|
-
- If no fallback, escalate to user with: "Task [X] failed after retry. Options: (a) simplify, (b) different agent, (c) manual intervention."
|
|
44
|
+
3. **If retry also fails** → do NOT retry blindly; follow the fallback chain and escalation protocol in `## ⏱️ Timeout & Retry Enforcement`.
|
|
47
45
|
|
|
48
46
|
## Progress Checkpoint
|
|
49
47
|
|
|
@@ -85,10 +85,7 @@ sequenceDiagram
|
|
|
85
85
|
## Dispatch Sequence (9-Step Protocol)
|
|
86
86
|
|
|
87
87
|
### Step 0 — Precedent Fast-Path (Fase 1)
|
|
88
|
-
|
|
89
|
-
- Score > 0.85 AND age < 30 days → return precedent verbatim with note "⚠️ Decisão de [data] — reavaliar se contexto mudou". SKIP entire dispatch.
|
|
90
|
-
- Score > 0.85 AND age > 30 days → return precedent with ⚠️ "Reavaliar se contexto mudou — decisão tem mais de 30 dias"
|
|
91
|
-
- Else → proceed to Step 0b
|
|
88
|
+
Run the precedent read path defined in `## Memory Protocol > Council Decisions Namespace` (`memory_search(query, top_k=2, namespace="council_decisions")`). If no precedent applies, proceed to Step 0b.
|
|
92
89
|
|
|
93
90
|
### Step 0b — Apollo Pre-Scan (Fase 2, --research flag)
|
|
94
91
|
If `/pantheon --research <question>`: dispatch @apollo with 30s timeout. Inject findings as `shared_context` into ALL specialist prompts. Skip if flag absent.
|
|
@@ -190,33 +187,11 @@ task(subagent_type: "themis", prompt: "Audit this council synthesis for fidelity
|
|
|
190
187
|
- If issues → fix each issue before delivering to user
|
|
191
188
|
|
|
192
189
|
### Step 9 — Persist & Reconcile (Fase 1)
|
|
193
|
-
|
|
194
|
-
memory_store({
|
|
195
|
-
namespace: "council_decisions",
|
|
196
|
-
key: "council:<yyyy-mm-dd>:<slug>",
|
|
197
|
-
value: JSON.stringify({
|
|
198
|
-
question, specialists, recommendation, confidence,
|
|
199
|
-
agreements, divergences, precedent_used, timestamp
|
|
200
|
-
}),
|
|
201
|
-
metadata: {type: "council_decision", specialist_count: N}
|
|
202
|
-
})
|
|
203
|
-
board.markReconciled("<task-id>")
|
|
204
|
-
```
|
|
190
|
+
Persist the decision using the write path in `## Memory Protocol > Council Decisions Namespace`, then `board.markReconciled("<task-id>")`.
|
|
205
191
|
|
|
206
192
|
## Specialist Output Format (Fase 1 — machine-parseable structured fields)
|
|
207
193
|
|
|
208
|
-
Specialists MUST return
|
|
209
|
-
|
|
210
|
-
```
|
|
211
|
-
## specialist_response
|
|
212
|
-
**position:** <clear one-sentence position>
|
|
213
|
-
**reasoning:** <2-4 sentences>
|
|
214
|
-
**trade_offs:** <what's gained vs lost>
|
|
215
|
-
**risks:** <what could go wrong>
|
|
216
|
-
**confidence:** High | Medium | Low
|
|
217
|
-
**agreement_signals:** agree: @agent1, @agent2 on [issue] | disagree: @agent3 on [issue]
|
|
218
|
-
**specific_claims:** <count of specific factual claims in response>
|
|
219
|
-
```
|
|
194
|
+
Specialists MUST return the structured `## specialist_response` format (position, reasoning, trade_offs, risks, confidence, agreement_signals, specific_claims) defined in `## Agent Return Format > Council Specialist Response Format`.
|
|
220
195
|
|
|
221
196
|
## Domain-to-Specialist Mapping
|
|
222
197
|
|
|
@@ -126,39 +126,14 @@ ANY NO → full task (IMPL artifact + Themis review mandatory)
|
|
|
126
126
|
3. **No Themis dependency** — output doesn't feed into a phase that requires review
|
|
127
127
|
|
|
128
128
|
## Subtask Return Format
|
|
129
|
-
|
|
130
|
-
```
|
|
131
|
-
## subtask_summary
|
|
132
|
-
**files_changed:** [paths]
|
|
133
|
-
**summary:** What was done
|
|
134
|
-
**tests:** ✅ or N/A
|
|
135
|
-
**status:** complete | partial | escalated
|
|
136
|
-
```
|
|
129
|
+
Return the standard `## subtask_summary` defined in `## Agent Return Format` (files_changed, summary, tests, coverage, tokens, status, blockers).
|
|
137
130
|
|
|
138
131
|
## Timeout Parcial (Partial Results)
|
|
139
132
|
|
|
140
|
-
Timeout
|
|
141
|
-
- ✅ @apollo — can return partial file list ("found 7 of 12 files before timeout")
|
|
142
|
-
- ✅ @gaia — can return partial literature findings
|
|
143
|
-
|
|
144
|
-
- ✅ @talos — can confirm progress if hotfix times out
|
|
145
|
-
- ❌ Never for implementers or reviewers — must complete or fail
|
|
146
|
-
|
|
147
|
-
When dispatching with partial-OK, set expectation:
|
|
148
|
-
```
|
|
149
|
-
@apollo Search for auth files. Timeout parcial OK — return whatever you have.
|
|
150
|
-
```
|
|
133
|
+
Only agents marked ✅ in the Timeout Behavior table above may return partial results: @apollo (partial file list, e.g. "found 7 of 12 files before timeout"), @gaia (partial literature findings) and @talos (confirm progress if a hotfix times out). Never for implementers or reviewers — they must complete or fail. When dispatching with partial-OK, set the expectation: `@apollo Search for auth files. Timeout parcial OK — return whatever you have.`
|
|
151
134
|
|
|
152
135
|
---
|
|
153
136
|
|
|
154
137
|
# 📊 TIMEOUT TRACKING
|
|
155
138
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
| Agent | Timeout | Status | Partial OK? |
|
|
159
|
-
|-------|---------|--------|-------------|
|
|
160
|
-
| @apollo | 60s | ✅ complete | ✅ |
|
|
161
|
-
| @hermes | 180s | ⏳ in progress | ❌ |
|
|
162
|
-
| @themis | 120s | ⏳ in progress | ❌ |
|
|
163
|
-
|
|
164
|
-
Log timeouts to `/memories/session/timeout-log.md` for later analysis.
|
|
139
|
+
Track in-flight delegations against the Timeout Behavior table above. Log timeouts to `/memories/session/timeout-log.md` for later analysis.
|
|
@@ -1323,7 +1323,7 @@ def build_rehydration_blocks(checkpoint: dict) -> list[str] | None:
|
|
|
1323
1323
|
"""Build deterministic rehydration blocks from a checkpoint dict.
|
|
1324
1324
|
|
|
1325
1325
|
Pure string templating — no LLM, no embeddings. Mirrors the
|
|
1326
|
-
``<mission_context>`` convention of
|
|
1326
|
+
``<mission_context>`` convention of the compaction-context builder: only a
|
|
1327
1327
|
non-done goal is re-emitted. Returns None when nothing critical remains.
|
|
1328
1328
|
"""
|
|
1329
1329
|
if not isinstance(checkpoint, dict) or not checkpoint:
|
package/src/mcp/toon_codec.py
CHANGED
|
@@ -9,7 +9,7 @@ absent/fails.
|
|
|
9
9
|
Only the standard library (``json`` + ``re``) — prohibited: external
|
|
10
10
|
gateway, generative embedding.
|
|
11
11
|
|
|
12
|
-
Encoding rules
|
|
12
|
+
Encoding rules:
|
|
13
13
|
|
|
14
14
|
- Scalars: ``None``/``True``/``False`` → ``null``/``true``/``false``;
|
|
15
15
|
numbers → shortest form; strings bare when unambiguous, else
|
|
@@ -20,8 +20,7 @@ Encoding rules (mirrored byte-for-byte by ``src/pantheon/toon-codec.ts``):
|
|
|
20
20
|
- Lists of scalars: ``- item`` lines; empty list → ``[]``.
|
|
21
21
|
- Lists of dicts with uniform keys: ``@table k1|k2`` header + ``v1|v2``
|
|
22
22
|
rows (savings are per payload class — ~11% on content-dominated board
|
|
23
|
-
records up to ~47% on large tabular checkpoints
|
|
24
|
-
``docs/ws3-token-opt-measurements.md`` for the reproducible table).
|
|
23
|
+
records up to ~47% on large tabular checkpoints).
|
|
25
24
|
- Dict values never need quoting for ``:`` (split is on the FIRST colon);
|
|
26
25
|
list items / top-level scalars that look like ``key: ...`` are quoted.
|
|
27
26
|
"""
|
|
@@ -471,7 +470,6 @@ def toon_size_report(value: Any) -> dict[str, Any]:
|
|
|
471
470
|
Reports BOTH chars and tokens: token ratios on tiny payloads suffer
|
|
472
471
|
ceiling bias (``ceil`` rounds 161→41 vs 143→36), so the char columns are
|
|
473
472
|
the honest basis and the token columns show what metering will debit.
|
|
474
|
-
Per-class numbers: see ``docs/ws3-token-opt-measurements.md``.
|
|
475
473
|
"""
|
|
476
474
|
compact_json = json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
|
477
475
|
toon = toon_encode(value)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Background Job Board — state machine for tracking background agent jobs.
|
|
3
3
|
*
|
|
4
4
|
* Tracks jobs through a state machine (running → completed/error/cancelled → reconciled),
|
|
5
|
-
* supports alias generation per session, persistence via adapter,
|
|
5
|
+
* supports alias generation per session, persistence via adapter, wake signal files,
|
|
6
6
|
* and terminal state listeners (persist-before-notify write-ahead-log pattern).
|
|
7
7
|
*
|
|
8
8
|
* Pure TypeScript — zero external dependencies beyond Node.js builtins (fs/path).
|
|
@@ -123,7 +123,7 @@ export interface BoardOptions {
|
|
|
123
123
|
/** Maximum completed jobs kept per agent for reuse (default: 3). */
|
|
124
124
|
maxReusablePerAgent?: number
|
|
125
125
|
/**
|
|
126
|
-
* Directory for
|
|
126
|
+
* Directory for wake signal files.
|
|
127
127
|
* When set, a `.signal.json` file is atomically written on every terminal transition.
|
|
128
128
|
* When null or undefined, no signal files are written.
|
|
129
129
|
*/
|
|
@@ -175,7 +175,7 @@ function getAgentPrefix(agent: string): string {
|
|
|
175
175
|
* - Strict state machine with validated transitions
|
|
176
176
|
* - Per-session, per-agent alias generation (apo-1, her-2, …)
|
|
177
177
|
* - Optional persistence via injected PersistenceAdapter
|
|
178
|
-
* - Optional
|
|
178
|
+
* - Optional wake signal files on terminal transitions
|
|
179
179
|
* - Terminal state listeners with persist-before-notify guarantee
|
|
180
180
|
* - expire/prune helpers for session lifecycle management
|
|
181
181
|
*/
|
|
@@ -652,7 +652,7 @@ export class BackgroundJobBoard {
|
|
|
652
652
|
}
|
|
653
653
|
|
|
654
654
|
/**
|
|
655
|
-
* Delete the
|
|
655
|
+
* Delete the wake signal file for an alias (`<alias>.signal.json`).
|
|
656
656
|
* No-op (false) when no signalDir is configured or the file is absent.
|
|
657
657
|
* The delegate manager calls this on the spot right after auto-reconcile.
|
|
658
658
|
*/
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Compaction Context (Phase 4 + release-134 Phase 2
|
|
3
|
-
*
|
|
4
|
-
* compacted session does not lose
|
|
5
|
-
*
|
|
2
|
+
* Compaction Context (Phase 4 + release-134 Phase 2, slimmed in Fase 2 of the
|
|
3
|
+
* delegate removal) — build the context blocks injected by the
|
|
4
|
+
* `experimental.session.compacting` hook so a compacted session does not lose
|
|
5
|
+
* its working state: active goals and pending todos.
|
|
6
6
|
*
|
|
7
7
|
* Returns a Promise<string[]> (empty when there is nothing to preserve).
|
|
8
8
|
* Section order is stable:
|
|
@@ -17,23 +17,10 @@
|
|
|
17
17
|
* `opts.todos`: " [id] description — status". Omitted when the todo
|
|
18
18
|
* source is absent, disabled, unscoped (no sessionID), fails, or has
|
|
19
19
|
* no pending todos.
|
|
20
|
-
* - "Background Delegations (running):" — every running job launched by
|
|
21
|
-
* the session (active work is never capped): alias, agent, started ISO,
|
|
22
|
-
* truncated prompt.
|
|
23
|
-
* - "Background Delegations (finished, unread):" — terminal-unreconciled
|
|
24
|
-
* jobs, newest first, capped at `maxItems` (default 10, matching
|
|
25
|
-
* routing.yml background_delegation.max_compaction_items).
|
|
26
20
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* The block style mirrors board.formatForPrompt (" [alias] description —
|
|
30
|
-
* STATUS") so compaction context reads consistently with the system prompt,
|
|
31
|
-
* but with the extra id/agent/started detail the compaction hook needs.
|
|
32
|
-
*
|
|
33
|
-
* @module delegation-compaction
|
|
21
|
+
* @module compaction-context
|
|
34
22
|
*/
|
|
35
23
|
|
|
36
|
-
import type { BackgroundJobBoard, BackgroundJobRecord } from './background-job-board.ts'
|
|
37
24
|
import type { Goal } from './goal-store.ts'
|
|
38
25
|
import { createPantheonLogger } from './logger.ts'
|
|
39
26
|
import type { TodoLike } from './todo-enforcer.ts'
|
|
@@ -72,56 +59,14 @@ export interface CompactionTodoSource {
|
|
|
72
59
|
}
|
|
73
60
|
|
|
74
61
|
export interface CompactionContextOptions {
|
|
75
|
-
/** Scope to
|
|
62
|
+
/** Scope to this session; omit to skip the per-session sections. */
|
|
76
63
|
sessionID?: string
|
|
77
|
-
/** Cap on unread terminal jobs kept (default 10). */
|
|
78
|
-
maxItems?: number
|
|
79
64
|
/** Goal source for <mission_context> (optional — omitted when absent). */
|
|
80
65
|
goals?: CompactionGoalSource
|
|
81
66
|
/** Todo source for <todo_context> (optional — omitted when absent). */
|
|
82
67
|
todos?: CompactionTodoSource
|
|
83
68
|
}
|
|
84
69
|
|
|
85
|
-
/** Prompt truncation length for a context line. */
|
|
86
|
-
const PROMPT_MAX_LEN = 100
|
|
87
|
-
|
|
88
|
-
// ─── Formatting ────────────────────────────────────────────────────────
|
|
89
|
-
|
|
90
|
-
/** Collapse whitespace and truncate a prompt to PROMPT_MAX_LEN chars. */
|
|
91
|
-
function truncatePrompt(text: string): string {
|
|
92
|
-
const flat = text.trim().replace(/\s+/g, ' ')
|
|
93
|
-
if (flat.length <= PROMPT_MAX_LEN) return flat
|
|
94
|
-
return `${flat.slice(0, PROMPT_MAX_LEN)}…`
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** One-line terminal status label, mirroring formatForPrompt. */
|
|
98
|
-
function terminalLabel(state: BackgroundJobRecord['state']): string {
|
|
99
|
-
switch (state) {
|
|
100
|
-
case 'completed':
|
|
101
|
-
return 'OK'
|
|
102
|
-
case 'error':
|
|
103
|
-
return 'ERR'
|
|
104
|
-
case 'cancelled':
|
|
105
|
-
return 'CAN'
|
|
106
|
-
default:
|
|
107
|
-
return state.toUpperCase()
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function runningLine(job: BackgroundJobRecord): string {
|
|
112
|
-
return (
|
|
113
|
-
` [${job.alias}] ${job.agent} — ${truncatePrompt(job.description)} — ` +
|
|
114
|
-
`RUNNING since ${new Date(job.launchedAt).toISOString()}`
|
|
115
|
-
)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function unreadTerminalLine(job: BackgroundJobRecord): string {
|
|
119
|
-
return (
|
|
120
|
-
` [${job.alias}] ${job.agent} — ${truncatePrompt(job.description)} — ` +
|
|
121
|
-
`${terminalLabel(job.state)} ${new Date(job.updatedAt).toISOString()} [unread]`
|
|
122
|
-
)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
70
|
// ─── Sections ──────────────────────────────────────────────────────────
|
|
126
71
|
|
|
127
72
|
/**
|
|
@@ -180,58 +125,23 @@ async function todoContextBlock(opts: CompactionContextOptions): Promise<string
|
|
|
180
125
|
return `<todo_context>\n${lines.join('\n')}`
|
|
181
126
|
}
|
|
182
127
|
|
|
183
|
-
/**
|
|
184
|
-
* `<delegation_context>` — the existing delegation blocks, byte-for-byte
|
|
185
|
-
* unchanged from Phase 4 (running, then unread terminal ≤ maxItems).
|
|
186
|
-
*/
|
|
187
|
-
function delegationBlocks(board: BackgroundJobBoard, opts: CompactionContextOptions): string[] {
|
|
188
|
-
const maxItems = opts.maxItems ?? 10
|
|
189
|
-
const jobs = opts.sessionID !== undefined ? board.list(opts.sessionID) : board.list()
|
|
190
|
-
|
|
191
|
-
const running = jobs
|
|
192
|
-
.filter((j) => j.state === 'running')
|
|
193
|
-
.sort((a, b) => a.launchedAt - b.launchedAt)
|
|
194
|
-
const unreadTerminal = jobs
|
|
195
|
-
.filter((j) => j.terminalUnreconciled)
|
|
196
|
-
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
197
|
-
.slice(0, maxItems)
|
|
198
|
-
|
|
199
|
-
const blocks: string[] = []
|
|
200
|
-
if (running.length > 0) {
|
|
201
|
-
blocks.push(`Background Delegations (running):\n${running.map(runningLine).join('\n')}`)
|
|
202
|
-
}
|
|
203
|
-
if (unreadTerminal.length > 0) {
|
|
204
|
-
blocks.push(
|
|
205
|
-
`Background Delegations (finished, unread):\n${unreadTerminal
|
|
206
|
-
.map(unreadTerminalLine)
|
|
207
|
-
.join('\n')}`,
|
|
208
|
-
)
|
|
209
|
-
}
|
|
210
|
-
return blocks
|
|
211
|
-
}
|
|
212
|
-
|
|
213
128
|
// ─── Build ─────────────────────────────────────────────────────────────
|
|
214
129
|
|
|
215
130
|
/**
|
|
216
131
|
* Build the compaction context blocks for a session: preservation
|
|
217
|
-
* directive, active goals, pending todos
|
|
218
|
-
*
|
|
219
|
-
* (caller skips the hook injection entirely).
|
|
132
|
+
* directive, active goals, and pending todos. Returns an empty array when
|
|
133
|
+
* there is nothing to preserve (caller skips the hook injection entirely).
|
|
220
134
|
*/
|
|
221
135
|
export async function buildCompactionContext(
|
|
222
|
-
board: BackgroundJobBoard,
|
|
223
136
|
opts: CompactionContextOptions = {},
|
|
224
137
|
): Promise<string[]> {
|
|
225
138
|
const [mission, todo] = await Promise.all([missionContextBlock(opts), todoContextBlock(opts)])
|
|
226
|
-
const delegation = delegationBlocks(board, opts)
|
|
227
139
|
|
|
228
140
|
// Totally-empty state → nothing to preserve; a lone directive is noise.
|
|
229
|
-
if (mission === undefined && todo === undefined
|
|
141
|
+
if (mission === undefined && todo === undefined) return []
|
|
230
142
|
|
|
231
|
-
const blocks: string[] = []
|
|
232
|
-
blocks.push(`<pantheon-context directive>\n${PANTHEON_COMPACTION_DIRECTIVE}`)
|
|
143
|
+
const blocks: string[] = [`<pantheon-context directive>\n${PANTHEON_COMPACTION_DIRECTIVE}`]
|
|
233
144
|
if (mission !== undefined) blocks.push(mission)
|
|
234
145
|
if (todo !== undefined) blocks.push(todo)
|
|
235
|
-
blocks.push(...delegation)
|
|
236
146
|
return blocks
|
|
237
147
|
}
|