pantheon-opencode 1.5.0-beta.15 → 1.5.0-beta.17

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 (54) hide show
  1. package/AGENTS.md +14 -63
  2. package/CHANGELOG.md +55 -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 -1
  7. package/plugin.json +1 -1
  8. package/pyproject.toml +1 -1
  9. package/scripts/mcp_persistence_server.py +1 -1
  10. package/src/agents/apollo.md +1 -0
  11. package/src/agents/gaia.md +1 -0
  12. package/src/agents/zeus.md +8 -29
  13. package/src/instructions/memory-protocol.instructions.md +6 -3
  14. package/src/instructions/zeus-anti-stall.instructions.md +2 -4
  15. package/src/instructions/zeus-council-synthesis.instructions.md +3 -28
  16. package/src/instructions/zeus-timeout-retry.instructions.md +3 -28
  17. package/src/mcp/mcp_persistence_server.py +1 -1
  18. package/src/mcp/toon_codec.py +2 -4
  19. package/src/pantheon/{delegation-compaction.ts → compaction-context.ts} +10 -100
  20. package/src/pantheon/cost-command.ts +1 -2
  21. package/src/pantheon/delegation-enforce.ts +59 -14
  22. package/src/pantheon/goal-loop.ts +1 -1
  23. package/src/pantheon/goal-store.ts +1 -1
  24. package/src/pantheon/idle-continuation.ts +4 -6
  25. package/src/pantheon/model-command.ts +1 -2
  26. package/src/pantheon/native-task-status.ts +2 -2
  27. package/src/pantheon/session-id.ts +30 -0
  28. package/src/pantheon/task-result-guard.ts +2 -3
  29. package/src/pantheon/todo-enforcer.ts +3 -3
  30. package/src/pantheon/tool-context.ts +23 -0
  31. package/src/pantheon/v2-bridge.ts +1 -4
  32. package/src/plugin-v2.ts +4 -15
  33. package/src/plugin.ts +61 -245
  34. package/src/plugins/tui/dist/tui.d.ts +33 -64
  35. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  36. package/src/plugins/tui/dist/tui.js +30 -177
  37. package/src/plugins/tui/package-lock.json +2 -2
  38. package/src/plugins/tui/package.json +1 -1
  39. package/src/plugins/tui/src/index.tsx +67 -277
  40. package/src/skills/context-compression/SKILL.md +1 -1
  41. package/scripts/dev-tui.sh +0 -48
  42. package/scripts/generate-preset-docs.mjs +0 -300
  43. package/scripts/generate-prompts.sh +0 -222
  44. package/scripts/install/component.mjs +0 -24
  45. package/scripts/install/migrate-config.mjs +0 -302
  46. package/src/pantheon/delegate-manager.ts +0 -851
  47. package/src/pantheon/delegation-classifier.ts +0 -306
  48. package/src/pantheon/delegation-finalize.ts +0 -468
  49. package/src/pantheon/delegation-notify.ts +0 -212
  50. package/src/pantheon/delegation-toast.ts +0 -73
  51. package/src/pantheon/delegation.ts +0 -1349
  52. package/src/pantheon/native-probe.ts +0 -234
  53. package/src/pantheon/step-cap.ts +0 -110
  54. package/src/pantheon/toon-codec.ts +0 -481
@@ -886,31 +886,32 @@ async function setupUsageBar(api: TuiPluginApi) {
886
886
  * CHILDREN + MD (delegations-sidebar pattern, feat/compaction-134):
887
887
  *
888
888
  * 1. PRIMARY — SDK child sessions via `api.client.session.children`:
889
- * every pantheon_delegate creates a child session with
890
- * parentID = caller (src/pantheon/delegation.ts session.create), so
889
+ * every delegated child session is created with
890
+ * parentID = caller (native `task()` child session), so
891
891
  * the children of the current session ARE the delegation list — the
892
892
  * same mechanism the opencode-delegations-sidebar reference uses
893
893
  * (`client.session.children` + `session.parentID === current`).
894
894
  * Live status comes from `api.state.session.status(childID)`
895
895
  * (busy/retry → running; idle → terminal). This does NOT depend on
896
896
  * tool-part events, which runtime 1.18.13 may not deliver for
897
- * pantheon_delegate (the "(0) even while delegating" symptom).
897
+ * native `task()` (the "(0) even while delegating" symptom).
898
898
  *
899
899
  * 1b. NATIVE task() CHILDREN — `session.children` returns ALL children,
900
900
  * and the native `task()` tool ALSO spawns a child session with
901
- * parentID = caller. A child WITHOUT a board report is a native
901
+ * parentID = caller. A child WITHOUT a delegate report is a native
902
902
  * task() child: childrenToDelegationEntries tags it source
903
903
  * 'children-only' (distinct info color),
904
- * so native task() work is visible in the panel alongside board
905
- * delegates — never filtered out, never duplicated
906
- * with a board row for the same child (a child WITH a report keeps the
904
+ * so native task() work is visible in the panel alongside delegate
905
+ * children — never filtered out, never duplicated
906
+ * with a report row for the same child (a child WITH a report keeps the
907
907
  * md entry, source 'md').
908
908
  *
909
- * 2. ENRICHMENT — markdown reports the job board persists under
910
- * `.pantheon/delegations/<sessionID>/<alias>.md` (written by
911
- * src/pantheon/delegation-finalize.ts at terminal state). Each child
909
+ * 2. ENRICHMENT — markdown reports written under
910
+ * `.pantheon/delegations/<sessionID>/<alias>.md` (legacy delegate output;
911
+ * the delegate layer was removed, existing reports stay readable). Each
912
+ * child
912
913
  * is matched to its report by the `Task ID` header (== child session
913
- * id == board task id), which supplies the alias (apo-N), agent,
914
+ * id == task id), which supplies the alias (apo-N), agent,
914
915
  * description and terminal duration/timedOut. No report → the child
915
916
  * itself still renders (title as description, derived state). The md
916
917
  * channel is read from EVERY session (readAllDelegationEntries) as an
@@ -927,7 +928,7 @@ async function setupUsageBar(api: TuiPluginApi) {
927
928
  * "panel: children=N(pantheon=M native=K) md=N events=N"
928
929
  * to `.pantheon/logs/hooks.log` (silence-by-default, createPantheonLogger
929
930
  * policy) so the (0) symptom is diagnosable from disk: children>0 means
930
- * the source works, pantheon/native splits board vs task() children.
931
+ * the source works, pantheon/native splits report vs task() children.
931
932
  * A children fetch
932
933
  * failure logs "panel: error <msg>" (previously silenced), and a missing
933
934
  * current session (null/placeholder sessionID) logs "panel: children=0
@@ -956,7 +957,7 @@ export type DelegationEntry = {
956
957
  alias: string
957
958
  /** Parent session the job was launched from (dir name under .pantheon/delegations). */
958
959
  sessionID: string
959
- /** Child session id (= board task id, from the `Task ID` header). The
960
+ /** Child session id (= task id, from the `Task ID` header). The
960
961
  * children channel always sets it from the child session itself; the md
961
962
  * channel parses it so child↔md matching works by taskID. */
962
963
  taskID?: string
@@ -984,10 +985,11 @@ export type DelegationEntry = {
984
985
  // childCount fields + tree-connector helpers were removed as dead code.
985
986
  // Reintroduce them once nested rows are actually produced.
986
987
  /** Internal provenance used to keep a finalized md report authoritative.
987
- * 'children-only' = a native task() child session with NO board report
988
- * (a native child row); 'md' = board report wins;
989
- * 'board' = read from .pantheon/board/state.json (cross-session FSM). */
990
- source?: 'child' | 'live' | 'md' | 'children-only' | 'board'
988
+ * 'children-only' = a native task() child session with NO delegate report
989
+ * (a native child row); 'md' = the child has a matching report;
990
+ * 'child' = the plain children channel; 'live' = the optimistic tool-event
991
+ * channel. */
992
+ source?: 'child' | 'live' | 'md' | 'children-only'
991
993
  }
992
994
 
993
995
  /** True for `\s` characters (space, tab, newline, CR) — plain char checks so
@@ -1172,90 +1174,11 @@ export async function readAllDelegationEntries(root: string): Promise<Delegation
1172
1174
  return readDelegationEntries(join(root, '.pantheon', 'delegations'))
1173
1175
  }
1174
1176
 
1175
- /* ─── Job board channel (4th source: .pantheon/board/state.json) ────────
1176
- * `session.children` is scoped to the focused session, so a job running in
1177
- * ANOTHER session is invisible to it (the "Delegations (0)" symptom).
1178
- * The board state file is the only durable, cross-session source:
1179
- * FilePersistence (src/pantheon/file-persistence.ts) stores every
1180
- * BackgroundJobRecord there as a JSON array. Reads are fail-open — absent,
1181
- * corrupt or unreadable → [] — so a broken board never crashes the panel. */
1182
-
1183
- /** The persisted subset of BackgroundJobRecord the panel consumes. Duck-typed
1184
- * so a corrupt/older record never breaks parsing. */
1185
- export type BoardJobRecord = {
1186
- taskID: string
1187
- parentSessionID: string
1188
- agent: string
1189
- description?: string
1190
- state: string
1191
- alias: string
1192
- launchedAt?: number
1193
- completedAt?: number
1194
- updatedAt?: number
1195
- timedOut?: boolean
1196
- }
1197
-
1198
- /** The FilePersistence state path: `<root>/.pantheon/board/state.json`. */
1199
- export function boardStatePath(root: string): string {
1200
- return join(root, '.pantheon', 'board', 'state.json')
1201
- }
1202
-
1203
- /** Validate one parsed record, keeping only the fields the panel uses.
1204
- * Returns null for anything without a taskID/state (a corrupt entry is
1205
- * skipped individually — the rest of the snapshot still loads). */
1206
- function normalizeBoardRecord(item: unknown): BoardJobRecord | null {
1207
- if (typeof item !== 'object' || item === null) return null
1208
- const rec = item as Record<string, unknown>
1209
- if (typeof rec.taskID !== 'string' || rec.taskID === '') return null
1210
- if (typeof rec.state !== 'string' || rec.state === '') return null
1211
- const record: BoardJobRecord = {
1212
- taskID: rec.taskID,
1213
- parentSessionID: typeof rec.parentSessionID === 'string' ? rec.parentSessionID : '',
1214
- agent: typeof rec.agent === 'string' && rec.agent !== '' ? rec.agent : 'agent',
1215
- state: rec.state,
1216
- alias: typeof rec.alias === 'string' ? rec.alias : '',
1217
- }
1218
- if (typeof rec.description === 'string') record.description = rec.description
1219
- if (typeof rec.launchedAt === 'number' && Number.isFinite(rec.launchedAt))
1220
- record.launchedAt = rec.launchedAt
1221
- if (typeof rec.completedAt === 'number' && Number.isFinite(rec.completedAt))
1222
- record.completedAt = rec.completedAt
1223
- if (typeof rec.updatedAt === 'number' && Number.isFinite(rec.updatedAt))
1224
- record.updatedAt = rec.updatedAt
1225
- if (typeof rec.timedOut === 'boolean') record.timedOut = rec.timedOut
1226
- return record
1227
- }
1228
-
1229
- /** Read `.pantheon/board/state.json` (the cross-session job board snapshot).
1230
- * Fail-open: missing file, corrupt JSON, non-array payload or an unreadable
1231
- * path all yield [] — the panel keeps rendering from the other channels. */
1232
- export async function readBoardState(root: string): Promise<BoardJobRecord[]> {
1233
- let raw: string
1234
- try {
1235
- raw = await readFile(boardStatePath(root), 'utf8')
1236
- } catch {
1237
- return [] // absent/unreadable — no board yet
1238
- }
1239
- let parsed: unknown
1240
- try {
1241
- parsed = JSON.parse(raw)
1242
- } catch {
1243
- return [] // corrupt JSON — never crash the sidebar
1244
- }
1245
- if (!Array.isArray(parsed)) return []
1246
- const records: BoardJobRecord[] = []
1247
- for (const item of parsed) {
1248
- const record = normalizeBoardRecord(item)
1249
- if (record !== null) records.push(record)
1250
- }
1251
- return records
1252
- }
1253
-
1254
1177
  /** Resolve the PROJECT ROOT used by every pantheon file channel. `directory`
1255
1178
  * wins over `worktree` (the old `resolveDelegationsDir` already did this);
1256
1179
  * an absent/empty root or `/` (no git — e.g. the sandbox test project) falls
1257
- * back to cwd. Standardised here so the delegations md, the board state file
1258
- * and the panel logger all read the SAME root (audit finding: the channels
1180
+ * back to cwd. Standardised here so the delegations md and the panel logger
1181
+ * all read the SAME root (audit finding: the channels
1259
1182
  * resolved the root independently). Pure — no I/O. */
1260
1183
  export function resolvePantheonRoot(
1261
1184
  state: { directory?: string; worktree?: string } | undefined,
@@ -1266,8 +1189,8 @@ export function resolvePantheonRoot(
1266
1189
  return root
1267
1190
  }
1268
1191
 
1269
- /** Resolve the directory where the job board writes delegation md reports.
1270
- * The board writes `.pantheon/delegations` RELATIVE to the server cwd,
1192
+ /** Resolve the directory where delegation md reports are written.
1193
+ * The finalizer writes `.pantheon/delegations` RELATIVE to the server cwd,
1271
1194
  * which the TUI exposes as `TuiState.path.directory`. */
1272
1195
  export function resolveDelegationsDir(
1273
1196
  state: { directory?: string; worktree?: string } | undefined,
@@ -1365,12 +1288,12 @@ export const STALE_RUNNING_THRESHOLD_MS = 30 * 60 * 1000
1365
1288
  * display-only `stale-running` state. */
1366
1289
  export const IDLE_SILENCE_MS = 60 * 1000
1367
1290
 
1368
- /** Visual-only terminal retention windows. Reports remain on disk and in the
1369
- * board; these constants only control which rows enter the TUI window. */
1291
+ /** Visual-only terminal retention windows. Reports remain on disk; these
1292
+ * constants only control which rows enter the TUI window. */
1370
1293
  export const DELEGATION_DONE_RETENTION_MS = 2 * 60 * 1000
1371
1294
  export const DELEGATION_FAILED_RETENTION_MS = 10 * 60 * 1000
1372
1295
 
1373
- /** Alias-less NATIVE task() live entries never receive a board alias (the
1296
+ /** Alias-less NATIVE task() live entries never receive a report alias (the
1374
1297
  * task tool output carries none), so the 30s alias-less prune in
1375
1298
  * mergeChildDelegationSources must not apply to them — 5 minutes covers a
1376
1299
  * slow child listing while still bounding the live map. */
@@ -1379,7 +1302,7 @@ export const NATIVE_LIVE_ALIASLESS_TTL_MS = 5 * 60 * 1000
1379
1302
  /**
1380
1303
  * Mark a running entry as `stale-running` if it has been running longer than
1381
1304
  * the threshold AND has no recent activity (no `updatedAt` change in the last
1382
- * `IDLE_SILENCE_MS`). This is DISPLAY-ONLY — the board state is unchanged.
1305
+ * `IDLE_SILENCE_MS`). This is DISPLAY-ONLY — the persisted state is unchanged.
1383
1306
  *
1384
1307
  * A `stale-running` entry renders with a warning indicator but the underlying
1385
1308
  * delegation is still treated as running by the backend.
@@ -1471,9 +1394,10 @@ export function delegationSpinnerFrame(now: number): string {
1471
1394
  return DELEGATION_SPINNER_FRAMES[index] ?? DELEGATION_SPINNER_FRAMES[0]
1472
1395
  }
1473
1396
 
1474
- /** Every state the row knows how to draw: the real BackgroundJobBoard FSM
1475
- * (src/pantheon/background-job-board.ts) plus the TUI display-only states
1476
- * (`retry`, `stale-running`). Fase 1 deliberately omits speculative
1397
+ /** Every state the row knows how to draw: the delegation lifecycle states
1398
+ * derived from the children status + md reports (`completed`, `error`,
1399
+ * `cancelled`, `startup_failed`, `startup_unknown`) plus the TUI-only
1400
+ * states (`retry`, `stale-running`). Fase 1 deliberately omits speculative
1477
1401
  * blocked/paused/scheduled/skipped. There is no `pending` display state: a
1478
1402
  * pre-dispatch tool part maps to `running` in {@link reduceDelegationToolPart}. */
1479
1403
  export type DelegationDisplayState = DelegationEntry['state']
@@ -1603,7 +1527,7 @@ export function mergeChildDelegationSources(
1603
1527
  })
1604
1528
 
1605
1529
  for (const liveEntry of live) {
1606
- // A native task() live part never carries a board alias or taskID, so it
1530
+ // A native task() live part never carries a report alias or taskID, so it
1607
1531
  // is exempt from the 30s alias-less prune and lives on its own longer TTL.
1608
1532
  const aliaslessTTL = liveEntry.tool === 'task' ? NATIVE_LIVE_ALIASLESS_TTL_MS : 30_000
1609
1533
  if (
@@ -1613,13 +1537,13 @@ export function mergeChildDelegationSources(
1613
1537
  )
1614
1538
  continue
1615
1539
  const incoming = toDelegationEntry(liveEntry)
1616
- // A native task() live part never carries a board alias — it is a
1540
+ // A native task() live part never carries a report alias — it is a
1617
1541
  // children-only row by definition whether it lands on a new row or
1618
1542
  // upgrades an existing
1619
1543
  // children-only one. pantheon_delegate pending parts keep source 'live'.
1620
1544
  const isNativeLive = liveEntry.tool === 'task' && liveEntry.alias === null
1621
1545
  if (isNativeLive) {
1622
- // Native task() rows carry no board alias — keep the per-call
1546
+ // Native task() rows carry no report alias — keep the per-call
1623
1547
  // `live-<callID>` identity from toDelegationEntry (one row per native
1624
1548
  // call) with the children-only provenance; the row identity still falls
1625
1549
  // back to the agent via delegationRowIdentity. A taskID-known native
@@ -1820,7 +1744,7 @@ export function parseDelegationToolPart(
1820
1744
  : ''
1821
1745
  // The alias/taskID only exist once the delegate tool COMPLETES (they are
1822
1746
  // returned in its output); a running/pending part has neither. Native
1823
- // `task` parts never carry a board alias — the children channel supplies
1747
+ // `task` parts never carry a report alias — the children channel supplies
1824
1748
  // the child session id and the row's identity.
1825
1749
  let alias: string | null = null
1826
1750
  let taskID: string | null = null
@@ -1894,7 +1818,7 @@ export function reduceDelegationToolPart(
1894
1818
  // Delegate tool (pantheon_delegate AND native task): pending/running →
1895
1819
  // job launched (running). A COMPLETED delegate tool only means the job was
1896
1820
  // registered — it stays running and we pick up alias + taskID from the
1897
- // output. Native `task` parts never yield a board alias, so their rows
1821
+ // output. Native `task` parts never yield a report alias, so their rows
1898
1822
  // stay identity-less until the children channel resolves them.
1899
1823
  const existing = map.get(parsed.callID)
1900
1824
  if (parsed.status === 'error') {
@@ -2078,136 +2002,18 @@ export function mergeDelegationSources(
2078
2002
  return all
2079
2003
  }
2080
2004
 
2081
- /* ─── Board channel: record → display + authoritative merge ───────────── */
2082
-
2083
- /** Job-board FSM → panel display state. `reconciled` is intentionally absent:
2084
- * the panel parser rejects it (a reconciled job is done and folded away). */
2085
- const BOARD_STATE_TO_DISPLAY: Record<string, DelegationEntry['state']> = {
2086
- running: 'running',
2087
- completed: 'completed',
2088
- error: 'error',
2089
- startup_failed: 'startup_failed',
2090
- startup_unknown: 'startup_unknown',
2091
- cancelled: 'cancelled',
2092
- }
2093
-
2094
- /** Map one persisted board record into the display shape. Returns null for
2095
- * states the panel does not render (reconciled / unknown). Pure. */
2096
- export function boardRecordToDelegationEntry(record: BoardJobRecord): DelegationEntry | null {
2097
- const state = BOARD_STATE_TO_DISPLAY[record.state]
2098
- if (state === undefined) return null
2099
- const running = state === 'running'
2100
- return {
2101
- alias: record.alias !== '' ? record.alias : `job-${record.taskID.slice(0, 8)}`,
2102
- sessionID: record.parentSessionID,
2103
- taskID: record.taskID,
2104
- agent: record.agent,
2105
- state,
2106
- startedAt: record.launchedAt ?? record.updatedAt ?? 0,
2107
- updatedAt: running ? null : (record.completedAt ?? record.updatedAt ?? null),
2108
- timedOut: record.timedOut === true,
2109
- description: record.description ?? '',
2110
- source: 'board',
2111
- }
2112
- }
2113
-
2114
- /** Map a whole board snapshot, dropping unrenderable states. Sorted like every
2115
- * other channel (running first, then most recent). Pure. */
2116
- export function boardRecordsToDelegationEntries(
2117
- records: readonly BoardJobRecord[],
2118
- ): DelegationEntry[] {
2119
- const out: DelegationEntry[] = []
2120
- for (const record of records) {
2121
- const entry = boardRecordToDelegationEntry(record)
2122
- if (entry !== null) out.push(entry)
2123
- }
2124
- out.sort(compareDelegationEntries)
2125
- return out
2126
- }
2127
-
2128
- /** Active (non-terminal) display states — a job in one of these is still live. */
2129
- function isActiveDelegationState(state: DelegationEntry['state']): boolean {
2130
- return state === 'running' || state === 'retry' || state === 'stale-running'
2131
- }
2132
-
2133
- /** Temporal guard for the board merge (recycled-alias regression): board
2134
- * aliases recycle across jobs (apo-1 of a previous job), so a board
2135
- * terminal record only closes a running entry when its Finalized timestamp
2136
- * is STRICTLY NEWER than the entry it would close. Older/equal/missing
2137
- * keeps the live entry — when in doubt the live job wins. Pure. */
2138
- function shouldKeepRunningOverBoardTerminal(
2139
- existing: DelegationEntry | undefined,
2140
- incoming: DelegationEntry,
2141
- ): boolean {
2142
- if (existing === undefined) return false
2143
- if (!isActiveDelegationState(existing.state)) return false
2144
- if (isActiveDelegationState(incoming.state)) return false
2145
- const incomingTs = incoming.updatedAt
2146
- if (incomingTs === null) return true
2147
- const existingTs = existing.updatedAt ?? existing.startedAt
2148
- return incomingTs <= existingTs
2149
- }
2150
-
2151
- /** Merge the board over any other channel. The board is AUTHORITATIVE for
2152
- * state/alias/agent (it is the persisted FSM). This merge is intentionally
2153
- * UNSCOPED so the board can enrich an active-session row (dedup by taskID);
2154
- * the caller scopes the merged list with {@link filterDelegationsToSession},
2155
- * which is what keeps other-session board jobs out of the panel. Dedup by
2156
- * taskID first, then by (sessionID, alias) — aliases are per parent session. Pure. */
2157
- export function mergeBoardDelegationSources(
2158
- base: readonly DelegationEntry[],
2159
- board: readonly DelegationEntry[],
2160
- ): DelegationEntry[] {
2161
- const key = (sessionID: string, alias: string): string => `${sessionID}\u0000${alias}`
2162
- const byTaskID = new Map<string, number>()
2163
- const bySessionAlias = new Map<string, number>()
2164
- const out = [...base]
2165
- out.forEach((entry, index) => {
2166
- if (entry.taskID !== undefined) byTaskID.set(entry.taskID, index)
2167
- bySessionAlias.set(key(entry.sessionID, entry.alias), index)
2168
- })
2169
-
2170
- for (const incoming of board) {
2171
- const index =
2172
- (incoming.taskID !== undefined ? byTaskID.get(incoming.taskID) : undefined) ??
2173
- bySessionAlias.get(key(incoming.sessionID, incoming.alias))
2174
- if (index === undefined) {
2175
- out.push(incoming)
2176
- const next = out.length - 1
2177
- if (incoming.taskID !== undefined) byTaskID.set(incoming.taskID, next)
2178
- bySessionAlias.set(key(incoming.sessionID, incoming.alias), next)
2179
- continue
2180
- }
2181
- // Temporal guard (recycled-alias regression): a STALE board terminal must
2182
- // never downgrade a live running entry — skip the board record entirely
2183
- // and keep the live row (same job alias, previous incarnation).
2184
- if (shouldKeepRunningOverBoardTerminal(out[index], incoming)) continue
2185
- out[index] = {
2186
- ...incoming,
2187
- // Session preservation: the board is authoritative for state/alias/agent,
2188
- // but an EMPTY board sessionID (record without parentSessionID) must
2189
- // never clobber a known session — otherwise a focused-session row would
2190
- // lose its attribution. Keep the known session.
2191
- sessionID: incoming.sessionID !== '' ? incoming.sessionID : (out[index]?.sessionID ?? ''),
2192
- } // board wins — it is the persisted source of truth
2193
- }
2194
-
2195
- out.sort(compareDelegationEntries)
2196
- return out
2197
- }
2198
-
2199
2005
  /** Scope a fully-merged display list to the ACTIVE session only.
2200
2006
  *
2201
- * The panel is session-scoped: rows from other sessions (board snapshot, md
2202
- * history under `.pantheon/delegations/<other-session>/`) and rows with no
2007
+ * The panel is session-scoped: rows from other sessions (md history under
2008
+ * `.pantheon/delegations/<other-session>/`) and rows with no
2203
2009
  * attributable session (empty sessionID) are DROPPED. The previous
2204
2010
  * cross-session behavior rendered those rows and clicking them led to
2205
2011
  * "Session not found" — there is no cross-session channel anymore.
2206
2012
  *
2207
- * The board/md channels still feed the merge UNFILTERED so the board can
2208
- * ENRICH an active-session row with state/alias/agent (dedup by taskID), but
2209
- * they can never introduce a row for another session: this filter is applied
2210
- * ONCE, after every merge (children + live + md + board).
2013
+ * The md channel still feeds the merge UNFILTERED so it can ENRICH an
2014
+ * active-session row with state/alias/agent (dedup by taskID), but it can
2015
+ * never introduce a row for another session: this filter is applied
2016
+ * ONCE, after every merge (children + live + md).
2211
2017
  *
2212
2018
  * Returns [] when no active session resolves (null/placeholder) — there is no
2213
2019
  * scope to show. Native children always carry the active session id (stamped
@@ -2313,12 +2119,12 @@ export function buildChildrenPath(id: string | null | undefined): { path: { id:
2313
2119
 
2314
2120
  /** Duck-typed subset of a child Session (+ its live status type). */
2315
2121
  export type ChildDelegationLike = {
2316
- /** Child session id (= board task id). */
2122
+ /** Child session id (= task id). */
2317
2123
  id: string
2318
2124
  /** Session title — the delegate's description or prompt prefix. */
2319
2125
  title?: string
2320
2126
  /** Agent name when the child session carries one (duck-typed; native
2321
- * task() children may expose it, board reports always do). */
2127
+ * task() children may expose it, md reports always do). */
2322
2128
  agent?: string
2323
2129
  /** Status type from api.state.session.status: 'busy' | 'retry' | 'idle',
2324
2130
  * or undefined when the status API is unavailable. */
@@ -2343,12 +2149,12 @@ export function childStatusToState(status: string | undefined): 'running' | 'com
2343
2149
  *
2344
2150
  * A row is
2345
2151
  * `{glyph} {alias:7} {elapsed:>5}` plus a muted description line. The short
2346
- * alias (`apo-1`) is the single identity; rows without a board alias show
2152
+ * alias (`apo-1`) is the single identity; rows without a report alias show
2347
2153
  * the agent instead (never both). */
2348
2154
  export type DelegationRowStatus = 'active' | 'done' | 'failed' | 'retry'
2349
2155
 
2350
2156
  /** Map every FSM/display state to one of the 4 row kinds. reconciled never
2351
- * reaches the panel (the board parser drops it) but reads as done;
2157
+ * reaches the panel (the md parser drops it) but reads as done;
2352
2158
  * cancelled reads as done; startup_failed reads as failed while
2353
2159
  * startup_unknown (no error known) reads as retry. Pure. */
2354
2160
  export function delegationRowStatus(state: DelegationDisplayState): DelegationRowStatus {
@@ -2390,8 +2196,8 @@ export function delegationRowMarker(state: DelegationDisplayState, now = Date.no
2390
2196
  return `${glyph} `
2391
2197
  }
2392
2198
 
2393
- /** ONE identity per row: the short board alias (`apo-1`); a row without a
2394
- * board alias (native task() child `native-task` / `native-<id>`, or an
2199
+ /** ONE identity per row: the short report alias (`apo-1`); a row without a
2200
+ * report alias (native task() child `native-task` / `native-<id>`, or an
2395
2201
  * alias-less native live row `live-<callID>` kept as children-only) shows
2396
2202
  * the agent instead — never alias + agent stacked. Pure. */
2397
2203
  export function delegationRowIdentity(entry: DelegationEntry): string {
@@ -2519,7 +2325,7 @@ export function ceilingDelegationList(
2519
2325
  }
2520
2326
 
2521
2327
  /** Split a display list into native task() rows vs pantheon_delegate rows.
2522
- * Native = source 'children-only' (no board report); everything else counts
2328
+ * Native = source 'children-only' (no delegate report); everything else counts
2523
2329
  * as pantheon. Pure — powers the hooks.log line. */
2524
2330
  export function countDelegationSources(entries: readonly DelegationEntry[]): {
2525
2331
  native: number
@@ -2532,7 +2338,7 @@ export function countDelegationSources(entries: readonly DelegationEntry[]): {
2532
2338
  }
2533
2339
 
2534
2340
  /** Diagnostic hooks.log line for a panel re-fetch, with the children
2535
- * breakdown (pantheon = children WITH a board report, native = children
2341
+ * breakdown (pantheon = children WITH a delegate report, native = children
2536
2342
  * WITHOUT one). Pure — the View logs the returned string verbatim. */
2537
2343
  export function formatPanelLogLine(
2538
2344
  children: number,
@@ -2554,7 +2360,7 @@ export function formatPanelLogLine(
2554
2360
  * child of the current session — pantheon_delegate OR the native `task()`
2555
2361
  * tool — carries parentID = caller), so it gets source 'children-only', a
2556
2362
  * per-child alias 'native-<last4 of the child id>' (one identity per native
2557
- * row) instead of a board alias. The 'task nativa' description fallback
2363
+ * row) instead of a report alias. The 'task nativa' description fallback
2558
2364
  * keeps the row non-empty when the child carries no title.
2559
2365
  * A FRESH terminal md state wins over the derived state (a stale md report
2560
2366
  * from a previous incarnation never flips a live child — see
@@ -2567,7 +2373,7 @@ export function formatPanelLogLine(
2567
2373
  * Sorted running-first (compareDelegationEntries).
2568
2374
  * Pure — no I/O. */
2569
2375
  /** Temporal guard for the children channel (same recycled-alias class as the
2570
- * board merge): an md terminal report matched by taskID may still belong to
2376
+ * md merge): an md terminal report matched by taskID may still belong to
2571
2377
  * a PREVIOUS job incarnation. Accept it only when its Finalized timestamp
2572
2378
  * covers the child's latest activity, when either side has no timestamp to
2573
2379
  * compare, or when both startedAt agree on a single job incarnation.
@@ -2776,7 +2582,7 @@ function SessionRow(props: { api: TuiPluginApi; session: any }) {
2776
2582
  )
2777
2583
  }
2778
2584
 
2779
- /* ─── Delegation Row (single job from the children/md/board channels) ─ */
2585
+ /* ─── Delegation Row (single job from the children/md channels) ─ */
2780
2586
 
2781
2587
  function DelegationRow(props: {
2782
2588
  api: TuiPluginApi
@@ -2947,14 +2753,14 @@ function View(props: {
2947
2753
  // and terminal duration via the `Task ID` match. Live tool-call events are
2948
2754
  // merged optimistically so the row appears before those channels catch up.
2949
2755
  // Consistent project root (directory ?? worktree, cwd fallback) — the md
2950
- // history, the board state file and the panel logger all read the SAME
2951
- // root (audit finding: the channels resolved it independently).
2756
+ // history and the panel logger both read the SAME root (audit finding: the
2757
+ // channels resolved it independently).
2952
2758
  const projectRoot = createMemo(() => resolvePantheonRoot((props.api.state as any).path))
2953
2759
  // Silence-by-default panel logger → the REAL <root>/.pantheon/logs/hooks.log.
2954
2760
  const panelLog = createTuiLogger(projectRoot())
2955
2761
  // ONLY the active session renders: the final list is filtered by
2956
- // filterDelegationsToSession after every merge (children + live + md +
2957
- // board). Live-first ordering keeps running rows on top; the ceiling
2762
+ // filterDelegationsToSession after every merge (children + live + md).
2763
+ // Live-first ordering keeps running rows on top; the ceiling
2958
2764
  // collapses the rest into "… +N more".
2959
2765
  // The rendered list is childDelegations (children enriched with md);
2960
2766
  // there is no separate md-only signal — the md channel only feeds
@@ -2992,16 +2798,6 @@ function View(props: {
2992
2798
  } catch {
2993
2799
  md = [] // fail-open: never crash the sidebar on a read error
2994
2800
  }
2995
- // 0b. board snapshot — the 4th channel. It is cross-session on disk but
2996
- // used here ONLY to ENRICH an active-session row (state/alias/agent,
2997
- // dedup by taskID) — the active-session filter applied after the merge
2998
- // drops its other-session records. Fail-open: absent/corrupt → [].
2999
- let board: DelegationEntry[] = []
3000
- try {
3001
- board = boardRecordsToDelegationEntries(await readBoardState(projectRoot()))
3002
- } catch {
3003
- board = [] // fail-open: never crash the sidebar on a board read error
3004
- }
3005
2801
  // 1. current session — resolve + GUARD (placeholder regression). The
3006
2802
  // runtime may render the sidebar with an unsubstituted "{sessionID}"
3007
2803
  // slot prop; forwarding it to session.children made the server reject
@@ -3011,12 +2807,10 @@ function View(props: {
3011
2807
  // an EMPTY panel (zero children, zero rows, zero errors).
3012
2808
  const sessionID = resolveCurrentSessionID({ sessionID: props.sessionID, api: props.api })
3013
2809
  if (sessionID === null) {
3014
- // No focused session → no scope → nothing renders. The md/board reads
3015
- // still ran (enrichment sources) but the active-session filter drops
3016
- // every row: there is no cross-session panel.
3017
- setChildDelegations(
3018
- filterDelegationsToSession(mergeBoardDelegationSources(md, board), sessionID),
3019
- )
2810
+ // No focused session → no scope → empty list. The md read still ran
2811
+ // (enrichment source) but with no session there is nothing to scope
2812
+ // to: there is no cross-session panel.
2813
+ setChildDelegations([])
3020
2814
  panelLog.info(
3021
2815
  `${formatPanelLogLine(0, 0, 0, md.length, eventRefreshCount)} (no sessionID — inactive panel)`,
3022
2816
  )
@@ -3062,17 +2856,13 @@ function View(props: {
3062
2856
  const liveEntries = [...props.liveStore.map.values()].filter(
3063
2857
  (entry) => entry.sessionID === sessionID,
3064
2858
  )
3065
- // Board LAST so it is authoritative over children/md/live for the same
3066
- // job and can ENRICH an active-session row (state/alias/agent). Then the
3067
- // active-session filter — applied ONCE after every merge — keeps ONLY
3068
- // rows whose sessionID === the focused session, so board/md entries from
2859
+ // The event channel is optimistic; merge it over the children/md rows.
2860
+ // Then the active-session filter — applied ONCE after the merge — keeps
2861
+ // ONLY rows whose sessionID === the focused session, so md entries from
3069
2862
  // other sessions (and orphans with no sessionID) can never render.
3070
2863
  setChildDelegations(
3071
2864
  filterDelegationsToSession(
3072
- mergeBoardDelegationSources(
3073
- mergeChildDelegationSources(childEntries, liveEntries),
3074
- board,
3075
- ),
2865
+ mergeChildDelegationSources(childEntries, liveEntries),
3076
2866
  sessionID,
3077
2867
  ),
3078
2868
  )
@@ -3100,7 +2890,7 @@ function View(props: {
3100
2890
  const delegationCeiling = createMemo(() =>
3101
2891
  ceilingDelegationList(childDelegations(), DELEGATION_VISIBLE_CEILING, now()),
3102
2892
  )
3103
- // Header counts the same retained rows shown below, so hidden/expired board
2893
+ // Header counts the same retained rows shown below, so hidden/expired md
3104
2894
  // history cannot inflate the summary.
3105
2895
  const delegationHeader = createMemo(() => formatDelegationHeader(delegationCeiling().visible))
3106
2896
 
@@ -3242,7 +3032,7 @@ function View(props: {
3242
3032
  </Show>
3243
3033
  </Show>
3244
3034
 
3245
- {/* ── Delegations (session.children primary + .pantheon/delegations md enrichment + board snapshot) ── */}
3035
+ {/* ── Delegations (session.children primary + .pantheon/delegations md enrichment) ── */}
3246
3036
  <box onMouseDown={() => setShowDelegations((x) => !x)}>
3247
3037
  <text fg={theme().text} attributes={1}>
3248
3038
  {`${showDelegations() ? '▼' : '▶'} Delegations`}
@@ -895,6 +895,6 @@ Camada P0 do Deepwork Plan "Context Window Optimization" (council 2026-08-25, 3/
895
895
  ```
896
896
  Desabilitar: `"enabled": false` (plugin vira no-op, sem throw).
897
897
 
898
- **Session Memory (P1 — já existe, só conectado):** `pantheon-persistence` KV com TTL 4h já fornece `context_save`/`context_get`. Auto-save antes de compactar (`experimental.session.compacting` → `buildCompactionContext` + `todoPreserver.capture`) e recovery pós-compactação (`session.compacted` → `todoPreserver.onCompacted` + `reassertAfterCompaction`) preservam estado sem novo MCP. Testado em `delegation-compaction.test.ts` + `todo-preserve.test.ts`.
898
+ **Session Memory (P1 — já existe, só conectado):** `pantheon-persistence` KV com TTL 4h já fornece `context_save`/`context_get`. Auto-save antes de compactar (`experimental.session.compacting` → `buildCompactionContext` + `todoPreserver.capture`) e recovery pós-compactação (`session.compacted` → `todoPreserver.onCompacted` + `reassertAfterCompaction`) preservam estado sem novo MCP. Testado em `todo-preserve.test.ts`.
899
899
 
900
900
  **Fora de escopo v1 (YAGNI):** tiktoken counting, LLM summarization, budget dashboard — só se medição provar necessidade.
@@ -1,48 +0,0 @@
1
- #!/usr/bin/env bash
2
- # dev-tui.sh — load the Pantheon TUI from the repo (src/plugins/tui) WITHOUT publishing.
3
- #
4
- # Flow: build the bundle → register the repo's absolute path in
5
- # .opencode/tui.json (read by the OpenCode TUI loader) → restart hint.
6
- # It deliberately does NOT touch opencode.json: that file is packaged and
7
- # merged into user configs by the installer, so a repo-relative path would
8
- # leak into third-party installs and break the CI package gates.
9
- #
10
- # Usage: scripts/dev-tui.sh [--test]
11
- # --test also run the full repo test suite (npm run test:all) after build.
12
- set -euo pipefail
13
-
14
- ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
15
- TUI_DIR="$ROOT/src/plugins/tui"
16
- TUI_JSON="$ROOT/.opencode/tui.json"
17
-
18
- command -v npm >/dev/null 2>&1 || { echo "error: npm not found in PATH" >&2; exit 1; }
19
- [ -d "$TUI_DIR" ] || { echo "error: TUI source not found: $TUI_DIR" >&2; exit 1; }
20
-
21
- echo "==> TUI dev source: $TUI_DIR"
22
-
23
- # 1. Install pinned deps + build dist/tui.js (the bundle is the only load path).
24
- echo "==> Installing TUI dev dependencies (npm ci)"
25
- npm ci --prefix "$TUI_DIR" --ignore-scripts --no-audit --no-fund
26
-
27
- echo "==> Building TUI plugin"
28
- npm run build --prefix "$TUI_DIR"
29
-
30
- # 2. Optional: full suite to match the CI gate before opening a PR.
31
- if [[ "${1:-}" == "--test" ]]; then
32
- echo "==> Running full test suite (npm run test:all)"
33
- npm run test:all
34
- fi
35
-
36
- # 3. Register the repo path in the project tui.json (absolute, gitignored).
37
- # Reuses the installer's registerPlugin so the array is merged, not clobbered.
38
- echo "==> Registering repo TUI path in $TUI_JSON"
39
- ROOT="$ROOT" TUI_JSON="$TUI_JSON" TUI_DIR="$TUI_DIR" node --input-type=module <<'NODE'
40
- const { registerPlugin } = await import(`${process.env.ROOT}/scripts/install/plugin.mjs`)
41
- const result = registerPlugin(process.env.TUI_JSON, process.env.TUI_DIR)
42
- console.log(`${result}: ${process.env.TUI_DIR}`)
43
- NODE
44
-
45
- # 4. Restart hint + dev-mode caveat.
46
- echo "==> Done. Restart OpenCode to load the rebuilt TUI plugin."
47
- echo " Dev mode caveat: do NOT run 'init' in this repo while iterating — it"
48
- echo " removes repo path refs as stale. If you do, re-run scripts/dev-tui.sh."