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.
Files changed (82) hide show
  1. package/AGENTS.md +14 -63
  2. package/CHANGELOG.md +320 -0
  3. package/README.md +14 -27
  4. package/README.pt-BR.md +5 -6
  5. package/commands/pantheon.md +1 -35
  6. package/package.json +1 -2
  7. package/plugin.json +1 -1
  8. package/pyproject.toml +2 -5
  9. package/scripts/doctor.mjs +32 -3
  10. package/scripts/install/plugin.mjs +9 -12
  11. package/scripts/mcp_persistence_server.py +1 -1
  12. package/scripts/sync-tui.mjs +8 -11
  13. package/src/agents/apollo.md +1 -0
  14. package/src/agents/gaia.md +1 -0
  15. package/src/agents/zeus.md +9 -52
  16. package/src/instructions/memory-protocol.instructions.md +6 -3
  17. package/src/instructions/zeus-anti-stall.instructions.md +2 -4
  18. package/src/instructions/zeus-council-synthesis.instructions.md +3 -28
  19. package/src/instructions/zeus-timeout-retry.instructions.md +3 -28
  20. package/src/mcp/mcp_persistence_server.py +1 -1
  21. package/src/mcp/toon_codec.py +2 -4
  22. package/src/pantheon/background-job-board.ts +4 -4
  23. package/src/pantheon/{delegation-compaction.ts → compaction-context.ts} +10 -100
  24. package/src/pantheon/cost-command.ts +1 -2
  25. package/src/pantheon/delegation-enforce.ts +59 -14
  26. package/src/pantheon/goal-loop.ts +1 -1
  27. package/src/pantheon/goal-store.ts +1 -1
  28. package/src/pantheon/idle-continuation.ts +4 -6
  29. package/src/pantheon/model-command.ts +1 -2
  30. package/src/pantheon/native-task-status.ts +2 -2
  31. package/src/pantheon/presets.d.mts +0 -20
  32. package/src/pantheon/presets.mjs +0 -64
  33. package/src/pantheon/session-guard.ts +2 -2
  34. package/src/pantheon/session-id.ts +30 -0
  35. package/src/pantheon/task-result-guard.ts +2 -3
  36. package/src/pantheon/todo-enforcer.ts +3 -3
  37. package/src/pantheon/tool-context.ts +23 -0
  38. package/src/pantheon/v2-bridge.ts +1 -4
  39. package/src/plugin-v2.ts +8 -21
  40. package/src/plugin.ts +61 -245
  41. package/src/plugins/pantheon-hooks.ts +1 -1
  42. package/src/plugins/tui/dist/tui.d.ts +229 -77
  43. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  44. package/src/plugins/tui/dist/tui.js +509 -259
  45. package/src/plugins/tui/package-lock.json +2 -2
  46. package/src/plugins/tui/package.json +2 -2
  47. package/src/plugins/tui/src/index.tsx +753 -279
  48. package/src/routing.yml +0 -16
  49. package/src/skills/context-compression/SKILL.md +1 -1
  50. package/scripts/generate-preset-docs.mjs +0 -300
  51. package/scripts/generate-prompts.sh +0 -222
  52. package/scripts/hash_verify.py +0 -192
  53. package/scripts/install/component.mjs +0 -24
  54. package/scripts/install/migrate-config.mjs +0 -302
  55. package/scripts/test-context-compression.sh +0 -169
  56. package/src/pantheon/auto-wake.ts +0 -85
  57. package/src/pantheon/chat-reminders.ts +0 -9
  58. package/src/pantheon/comment-checker.ts +0 -48
  59. package/src/pantheon/compaction-assert.ts +0 -160
  60. package/src/pantheon/compaction-handoff.ts +0 -304
  61. package/src/pantheon/cost-tracker.ts +0 -108
  62. package/src/pantheon/delegate-manager.ts +0 -766
  63. package/src/pantheon/delegation-classifier.ts +0 -307
  64. package/src/pantheon/delegation-finalize.ts +0 -406
  65. package/src/pantheon/delegation-notify.ts +0 -212
  66. package/src/pantheon/delegation-toast.ts +0 -73
  67. package/src/pantheon/delegation.ts +0 -1337
  68. package/src/pantheon/dispatch-guard.ts +0 -109
  69. package/src/pantheon/native-probe.ts +0 -234
  70. package/src/pantheon/permission-eval.ts +0 -284
  71. package/src/pantheon/preemptive-compact.ts +0 -76
  72. package/src/pantheon/provider-capabilities.ts +0 -221
  73. package/src/pantheon/provider-stream-tracker.ts +0 -182
  74. package/src/pantheon/retry-policy.ts +0 -212
  75. package/src/pantheon/step-cap.ts +0 -110
  76. package/src/pantheon/token-opt.ts +0 -689
  77. package/src/pantheon/toon-codec.ts +0 -481
  78. package/src/pantheon/v2-events.ts +0 -148
  79. package/src/pantheon/v2-hooks.ts +0 -287
  80. package/src/pantheon/v2-tool-definitions.ts +0 -331
  81. package/src/pantheon/zeus-delegate-with-retry.ts +0 -406
  82. 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-beta.8"
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/test_eval_judge.py" = ["PLR2004"]
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]
@@ -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((candidate) => existsSync(candidate)) ??
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((candidate) => existsSync(candidate))
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 (src/index.tsx → index.tsx, dist/*,
12
- * package.json and package-lock.json) from source to destination.
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
- // Copy src/index.tsx -> index.tsx
26
- const srcIdx = join(srcDir, 'src', 'index.tsx')
27
- if (existsSync(srcIdx)) {
28
- const idxStatus = writeIfChanged(
29
- join(dstDir, 'index.tsx'),
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 delegation-compaction.ts: only a
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:
@@ -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, package-lock.json,
62
- * src/index.tsx) from srcDir
63
- * to dstDir. Skips files that are byte-identical.
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
- // src/index.tsx → index.tsx
73
- const srcIdx = join(srcDir, 'src', 'index.tsx')
74
- if (existsSync(srcIdx)) {
75
- const content = readFileSync(srcIdx, 'utf8')
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')
@@ -12,6 +12,7 @@ permission:
12
12
  glob: allow
13
13
  webfetch: allow
14
14
  edit: deny
15
+ hashline_edit: deny
15
16
  bash: deny
16
17
  task:
17
18
  "*": deny
@@ -11,6 +11,7 @@ permission:
11
11
  grep: allow
12
12
  webfetch: allow
13
13
  edit: deny
14
+ hashline_edit: deny
14
15
  bash: deny
15
16
  task:
16
17
  "*": deny
@@ -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
- ### Pantheon Delegation Tools (plugin) — 3-tool API
147
+ ### DELEGATION_RULES (native task API)
148
148
 
149
- O plugin Pantheon adiciona 3 tools de delegacao gerenciada pelo BackgroundJobBoard:
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 (dispatch-guard — MANUAL orchestration)
305
- OpenCode 1.18.x NAO permite interceptar a conclusao de `task()` via hooks,
306
- entao o `src/pantheon/dispatch-guard.ts` e uma lib pura usada POR VOCE na
307
- orquestracao (NAO esta wired no plugin):
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`, dispatch to fallback agent, report to user |
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** → DO NOT retry a third time blindly. Instead:
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
- Before any dispatch: `memory_search(query, top_k=2, namespace="council_decisions")`
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 these structured fields in their response:
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
- Expect a `subtask_summary` response with:
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 parcial is ONLY for read-only, independent agents:
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
- Maintain awareness of in-flight delegations:
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 delegation-compaction.ts: only a
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:
@@ -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 (mirrored byte-for-byte by ``src/pantheon/toon-codec.ts``):
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 — see
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, auto-wake signal files,
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 auto-wake signal files.
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 auto-wake signal files on terminal transitions
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 auto-wake signal file for an alias (`<alias>.signal.json`).
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) — build the context
3
- * blocks injected by the `experimental.session.compacting` hook so a
4
- * compacted session does not lose its working state: in-flight background
5
- * delegations, active goals and pending todos.
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
- * Reconciled jobs are excluded — they have been read already.
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 jobs launched by this session; omit for all jobs. */
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, and in-flight background
218
- * delegations. Returns an empty array when there is nothing to preserve
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 && delegation.length === 0) return []
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
  }