pantheon-opencode 1.4.1 → 1.4.3
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/README.md +175 -147
- package/bin/pantheon-init.mjs +7 -0
- package/commands/pantheon-model.md +34 -0
- package/package.json +2 -2
- package/scripts/generate-preset-docs.mjs +239 -0
- package/scripts/install/model-picker.mjs +367 -2
- package/scripts/install/opencode.mjs +150 -137
- package/src/agents/zeus.md +14 -25
- package/src/pantheon/cost-command.ts +7 -7
- package/src/pantheon/delegation-enforce.ts +12 -13
- package/src/pantheon/goal-loop.ts +2 -2
- package/src/pantheon/goal-store.ts +1 -1
- package/src/pantheon/hashline/tool.ts +2 -2
- package/src/pantheon/model-command.ts +911 -0
- package/src/pantheon/presets.mjs +30 -33
- package/src/pantheon/tool-context.ts +41 -0
- package/src/plugin.ts +45 -193
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +1 -1
- package/src/plugins/tui/dist/tui.tsx +13 -2
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +21 -8
- package/src/plugins/tui/tsdown.config.ts +11 -0
- package/src/routing.yml +71 -123
- package/src/pantheon/delegation-classifier.ts +0 -307
- package/src/pantheon/delegation-finalize.ts +0 -374
- package/src/pantheon/delegation-notify.ts +0 -197
- package/src/pantheon/delegation-toast.ts +0 -73
- package/src/pantheon/delegation.ts +0 -1110
- package/src/pantheon/permission-eval.ts +0 -284
- package/src/pantheon/step-cap.ts +0 -110
- package/src/pantheon/zeus-delegate-with-retry.ts +0 -401
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
syncDir,
|
|
44
44
|
writeIfChanged,
|
|
45
45
|
} from './shared.mjs'
|
|
46
|
+
|
|
46
47
|
import { setupVenv, venvPythonPath } from './venv.mjs'
|
|
47
48
|
|
|
48
49
|
const COMPONENT_NAMES = [
|
|
@@ -59,6 +60,16 @@ function deepClone(value) {
|
|
|
59
60
|
return JSON.parse(JSON.stringify(value))
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
function readJsonConfig(filePath) {
|
|
64
|
+
if (!existsSync(filePath)) return {}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(readFileSync(filePath, 'utf8'))
|
|
68
|
+
} catch (cause) {
|
|
69
|
+
throw new Error(`Invalid JSON in ${filePath}`, { cause })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
62
73
|
function mergeMissing(target, source) {
|
|
63
74
|
if (!source || typeof source !== 'object' || Array.isArray(source)) return target
|
|
64
75
|
if (!target || typeof target !== 'object' || Array.isArray(target)) return deepClone(source)
|
|
@@ -274,6 +285,11 @@ export async function installOpenCode(
|
|
|
274
285
|
}
|
|
275
286
|
}
|
|
276
287
|
|
|
288
|
+
const pantheonConfigPath = join(ROOT, 'opencode.json')
|
|
289
|
+
const targetConfigPath = join(target, 'opencode.json')
|
|
290
|
+
const config = readJsonConfig(targetConfigPath)
|
|
291
|
+
const pantheonConfig = readJsonConfig(pantheonConfigPath)
|
|
292
|
+
|
|
277
293
|
// Determine layout based on install scope.
|
|
278
294
|
// Global config dir (~/.opencode or ~/.config/opencode) uses a flat layout:
|
|
279
295
|
// agents/ skills/ commands/ plugins/
|
|
@@ -533,27 +549,6 @@ export async function installOpenCode(
|
|
|
533
549
|
// Reads TARGET's existing config first, then merges Pantheon settings
|
|
534
550
|
// on top. Preserves user's MCP, provider, plugin, compaction, theme.
|
|
535
551
|
// -----------------------------------------------------------------------
|
|
536
|
-
const pantheonConfigPath = join(ROOT, 'opencode.json')
|
|
537
|
-
const targetConfigPath = join(target, 'opencode.json')
|
|
538
|
-
|
|
539
|
-
let config = {}
|
|
540
|
-
if (existsSync(targetConfigPath)) {
|
|
541
|
-
try {
|
|
542
|
-
config = JSON.parse(readFileSync(targetConfigPath, 'utf8'))
|
|
543
|
-
} catch {
|
|
544
|
-
config = {}
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
let pantheonConfig = {}
|
|
549
|
-
if (existsSync(pantheonConfigPath)) {
|
|
550
|
-
try {
|
|
551
|
-
pantheonConfig = JSON.parse(readFileSync(pantheonConfigPath, 'utf8'))
|
|
552
|
-
} catch {
|
|
553
|
-
pantheonConfig = {}
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
552
|
// --------------------------------------------------------------------
|
|
558
553
|
// A. Parse canonical agent config from agents/*.agent.md frontmatter
|
|
559
554
|
// and merge into opencode.json config.
|
|
@@ -622,61 +617,64 @@ export async function installOpenCode(
|
|
|
622
617
|
'disable_model_invocation',
|
|
623
618
|
]
|
|
624
619
|
|
|
625
|
-
if (
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
620
|
+
if (config.agent === undefined) config.agent = {}
|
|
621
|
+
|
|
622
|
+
if (config.agent && typeof config.agent === 'object' && !Array.isArray(config.agent)) {
|
|
623
|
+
for (const [agentName, agentCfg] of Object.entries(canonicalAgentConfig)) {
|
|
624
|
+
if (!agentCfg || typeof agentCfg !== 'object') continue
|
|
625
|
+
|
|
626
|
+
if (Object.hasOwn(config.agent, agentName)) {
|
|
627
|
+
// ── Agent exists in target config ──
|
|
628
|
+
// Update framework-managed fields from canonical source
|
|
629
|
+
// Preserve user-customized fields (model, provider, mcp, etc.)
|
|
630
|
+
const existing = config.agent[agentName]
|
|
631
|
+
if (!existing || typeof existing !== 'object' || Array.isArray(existing)) continue
|
|
632
|
+
if (agentSources[agentName]) existing.source = agentSources[agentName]
|
|
633
|
+
for (const field of MANAGED_FIELDS) {
|
|
634
|
+
if (field in agentCfg) {
|
|
635
|
+
existing[field] = JSON.parse(JSON.stringify(agentCfg[field]))
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
// Remove stale fields that are no longer in canonical config
|
|
639
|
+
delete existing.model
|
|
640
|
+
delete existing.small_model
|
|
641
|
+
} else {
|
|
642
|
+
// ── New agent ──
|
|
643
|
+
const newAgent = {}
|
|
644
|
+
if (agentSources[agentName]) newAgent.source = agentSources[agentName]
|
|
645
|
+
if (agentCfg.description) newAgent.description = agentCfg.description
|
|
646
|
+
|
|
647
|
+
// Copy all framework-managed fields from canonical config
|
|
648
|
+
for (const field of MANAGED_FIELDS) {
|
|
649
|
+
if (field in agentCfg) {
|
|
650
|
+
newAgent[field] = JSON.parse(JSON.stringify(agentCfg[field]))
|
|
651
|
+
}
|
|
652
|
+
}
|
|
629
653
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
// Preserve user-customized fields (model, provider, mcp, etc.)
|
|
634
|
-
const existing = config.agent[agentName]
|
|
635
|
-
if (agentSources[agentName]) existing.source = agentSources[agentName]
|
|
636
|
-
for (const field of MANAGED_FIELDS) {
|
|
637
|
-
if (field in agentCfg) {
|
|
638
|
-
existing[field] = JSON.parse(JSON.stringify(agentCfg[field]))
|
|
654
|
+
// Ensure bash permission is set from canonical (default to deny if missing)
|
|
655
|
+
if (newAgent.permission === undefined) {
|
|
656
|
+
newAgent.permission = {}
|
|
639
657
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
delete existing.model
|
|
643
|
-
delete existing.small_model
|
|
644
|
-
} else {
|
|
645
|
-
// ── New agent ──
|
|
646
|
-
const newAgent = {}
|
|
647
|
-
if (agentSources[agentName]) newAgent.source = agentSources[agentName]
|
|
648
|
-
if (agentCfg.description) newAgent.description = agentCfg.description
|
|
649
|
-
|
|
650
|
-
// Copy all framework-managed fields from canonical config
|
|
651
|
-
for (const field of MANAGED_FIELDS) {
|
|
652
|
-
if (field in agentCfg) {
|
|
653
|
-
newAgent[field] = JSON.parse(JSON.stringify(agentCfg[field]))
|
|
658
|
+
if (newAgent.permission.bash === undefined && agentCfg.permission?.bash) {
|
|
659
|
+
newAgent.permission.bash = JSON.parse(JSON.stringify(agentCfg.permission.bash))
|
|
654
660
|
}
|
|
655
|
-
}
|
|
656
661
|
|
|
657
|
-
|
|
658
|
-
if (!newAgent.permission) {
|
|
659
|
-
newAgent.permission = {}
|
|
660
|
-
}
|
|
661
|
-
if (!newAgent.permission.bash && agentCfg.permission?.bash) {
|
|
662
|
-
newAgent.permission.bash = JSON.parse(JSON.stringify(agentCfg.permission.bash))
|
|
662
|
+
config.agent[agentName] = newAgent
|
|
663
663
|
}
|
|
664
|
-
|
|
665
|
-
config.agent[agentName] = newAgent
|
|
666
664
|
}
|
|
667
|
-
}
|
|
668
665
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
666
|
+
// Remove stale agents (exist in target config but not in canonical source)
|
|
667
|
+
// Only removes agents whose source is managed (starts with our prefix)
|
|
668
|
+
// so user-defined agents with different source paths are preserved.
|
|
669
|
+
const canonicalNames = new Set(Object.keys(canonicalAgentConfig))
|
|
670
|
+
for (const [agentName, agentCfg] of Object.entries(config.agent)) {
|
|
671
|
+
const source = agentCfg?.source || ''
|
|
672
|
+
if (source.startsWith(agentPrefix) && !canonicalNames.has(agentName)) {
|
|
673
|
+
delete config.agent[agentName]
|
|
674
|
+
}
|
|
677
675
|
}
|
|
676
|
+
if (Object.keys(config.agent).length === 0) delete config.agent
|
|
678
677
|
}
|
|
679
|
-
if (Object.keys(config.agent).length === 0) delete config.agent
|
|
680
678
|
|
|
681
679
|
// --------------------------------------------------------------------
|
|
682
680
|
// B. Commands from .md frontmatter (commands.json removed)
|
|
@@ -687,19 +685,16 @@ export async function installOpenCode(
|
|
|
687
685
|
// --------------------------------------------------------------------
|
|
688
686
|
// B.5 Ensure critical top-level OpenCode config sections
|
|
689
687
|
// --------------------------------------------------------------------
|
|
690
|
-
if (
|
|
688
|
+
if (config.default_agent === undefined && pantheonConfig.default_agent !== undefined) {
|
|
691
689
|
config.default_agent = pantheonConfig.default_agent
|
|
692
690
|
}
|
|
693
|
-
//
|
|
694
|
-
//
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
config.small_model = pantheonConfig.small_model
|
|
701
|
-
}
|
|
702
|
-
}
|
|
691
|
+
// Apply only explicit top-level model overrides. Existing values remain
|
|
692
|
+
// untouched and an install without either flag leaves both fields absent.
|
|
693
|
+
const modelFlag = typeof opts.model === 'string' && opts.model !== '' ? opts.model : undefined
|
|
694
|
+
const smallModelFlag =
|
|
695
|
+
typeof opts.smallModel === 'string' && opts.smallModel !== '' ? opts.smallModel : undefined
|
|
696
|
+
if (modelFlag !== undefined) config.model = modelFlag
|
|
697
|
+
if (smallModelFlag !== undefined) config.small_model = smallModelFlag
|
|
703
698
|
// Merge subagent_depth HERE (the config merge always runs, including on a
|
|
704
699
|
// fresh install) instead of a file-existence-gated block, so run #1 already
|
|
705
700
|
// produces the final config — run #2 must be a byte-identical no-op (#19).
|
|
@@ -710,16 +705,24 @@ export async function installOpenCode(
|
|
|
710
705
|
// experimental (preserving the user's choice) and default fresh installs to
|
|
711
706
|
// experimental.subagent_depth = 2. V1 reads experimental.subagent_depth
|
|
712
707
|
// too, so the V1-shape config stays valid under both versions.
|
|
713
|
-
if (config.experimental === undefined
|
|
708
|
+
if (config.experimental === undefined) {
|
|
714
709
|
config.experimental = {}
|
|
715
710
|
}
|
|
716
|
-
if (
|
|
717
|
-
|
|
718
|
-
|
|
711
|
+
if (
|
|
712
|
+
typeof config.experimental === 'object' &&
|
|
713
|
+
config.experimental !== null &&
|
|
714
|
+
!Array.isArray(config.experimental)
|
|
715
|
+
) {
|
|
716
|
+
if (config.subagent_depth !== undefined) {
|
|
717
|
+
if (config.experimental.subagent_depth === undefined) {
|
|
718
|
+
config.experimental.subagent_depth = config.subagent_depth
|
|
719
|
+
}
|
|
720
|
+
delete config.subagent_depth
|
|
721
|
+
} else if (config.experimental.subagent_depth === undefined) {
|
|
722
|
+
config.experimental.subagent_depth = 2
|
|
719
723
|
}
|
|
724
|
+
} else if (config.subagent_depth !== undefined) {
|
|
720
725
|
delete config.subagent_depth
|
|
721
|
-
} else if (config.experimental.subagent_depth === undefined) {
|
|
722
|
-
config.experimental.subagent_depth = 2
|
|
723
726
|
}
|
|
724
727
|
|
|
725
728
|
// --------------------------------------------------------------------
|
|
@@ -735,74 +738,82 @@ export async function installOpenCode(
|
|
|
735
738
|
// The source opencode.json keeps its dev path for local development; the
|
|
736
739
|
// transform happens at install/sync time only.
|
|
737
740
|
|
|
738
|
-
if (
|
|
741
|
+
if (config.plugin === undefined) {
|
|
739
742
|
config.plugin = []
|
|
740
743
|
}
|
|
741
|
-
if (Array.isArray(
|
|
742
|
-
|
|
743
|
-
const
|
|
744
|
+
if (Array.isArray(config.plugin)) {
|
|
745
|
+
if (Array.isArray(pantheonConfig.plugin)) {
|
|
746
|
+
for (const plugin of pantheonConfig.plugin) {
|
|
747
|
+
const resolved = resolveInstalledPlugin(plugin)
|
|
748
|
+
const file = basename(resolved)
|
|
749
|
+
// Replace any pre-existing entry for the same plugin file (e.g. a stale
|
|
750
|
+
// dev-machine path from an earlier install) so upgrades stay hermetic.
|
|
751
|
+
config.plugin = config.plugin.filter((p) => basename(p) !== file)
|
|
752
|
+
config.plugin.push(resolved)
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// The two Pantheon plugins MUST always be registered, on fresh installs AND
|
|
757
|
+
// upgrades. The packaged opencode.json carries no `plugin` key (dev-machine
|
|
758
|
+
// paths removed in 3e552cc), so config.plugin would otherwise only contain
|
|
759
|
+
// whatever the user had — a fresh install would register NOTHING and lose
|
|
760
|
+
// delegation tools, GoalLoop, cost command, hashline and vision. Register
|
|
761
|
+
// both unconditionally, replacing any stale entry for the same file
|
|
762
|
+
// (basename-dedup) so installs and upgrades are idempotent and hermetic
|
|
763
|
+
// while preserving any user plugins.
|
|
764
|
+
const ensurePantheonPlugin = (ref) => {
|
|
765
|
+
const resolved = resolveInstalledPlugin(ref)
|
|
744
766
|
const file = basename(resolved)
|
|
745
|
-
// Replace any pre-existing entry for the same plugin file (e.g. a stale
|
|
746
|
-
// dev-machine path from an earlier install) so upgrades stay hermetic.
|
|
747
767
|
config.plugin = config.plugin.filter((p) => basename(p) !== file)
|
|
748
768
|
config.plugin.push(resolved)
|
|
749
769
|
}
|
|
770
|
+
// Root-level delegation plugin (PR #45): tools, GoalLoop, cost, hashline.
|
|
771
|
+
ensurePantheonPlugin('src/plugin.ts')
|
|
772
|
+
// Runtime hooks plugin: chat hooks, hook-runner, TUI wiring.
|
|
773
|
+
ensurePantheonPlugin('src/plugins/pantheon-hooks.ts')
|
|
750
774
|
}
|
|
751
775
|
|
|
752
|
-
|
|
753
|
-
// upgrades. The packaged opencode.json carries no `plugin` key (dev-machine
|
|
754
|
-
// paths removed in 3e552cc), so config.plugin would otherwise only contain
|
|
755
|
-
// whatever the user had — a fresh install would register NOTHING and lose
|
|
756
|
-
// delegation tools, GoalLoop, cost command, hashline and vision. Register
|
|
757
|
-
// both unconditionally, replacing any stale entry for the same file
|
|
758
|
-
// (basename-dedup) so installs and upgrades are idempotent and hermetic
|
|
759
|
-
// while preserving any user plugins.
|
|
760
|
-
const ensurePantheonPlugin = (ref) => {
|
|
761
|
-
const resolved = resolveInstalledPlugin(ref)
|
|
762
|
-
const file = basename(resolved)
|
|
763
|
-
config.plugin = config.plugin.filter((p) => basename(p) !== file)
|
|
764
|
-
config.plugin.push(resolved)
|
|
765
|
-
}
|
|
766
|
-
// Root-level delegation plugin (PR #45): tools, GoalLoop, cost, hashline.
|
|
767
|
-
ensurePantheonPlugin('src/plugin.ts')
|
|
768
|
-
// Runtime hooks plugin: chat hooks, hook-runner, TUI wiring.
|
|
769
|
-
ensurePantheonPlugin('src/plugins/pantheon-hooks.ts')
|
|
770
|
-
|
|
771
|
-
if (!config.provider && pantheonConfig.provider) {
|
|
776
|
+
if (config.provider === undefined && pantheonConfig.provider !== undefined) {
|
|
772
777
|
config.provider = deepClone(pantheonConfig.provider)
|
|
773
|
-
} else if (config.provider && pantheonConfig.provider) {
|
|
778
|
+
} else if (config.provider !== undefined && pantheonConfig.provider !== undefined) {
|
|
774
779
|
mergeMissing(config.provider, pantheonConfig.provider)
|
|
775
780
|
}
|
|
776
781
|
|
|
777
|
-
if (
|
|
782
|
+
if (config.compaction === undefined && pantheonConfig.compaction !== undefined) {
|
|
778
783
|
config.compaction = deepClone(pantheonConfig.compaction)
|
|
779
|
-
} else if (config.compaction && pantheonConfig.compaction) {
|
|
784
|
+
} else if (config.compaction !== undefined && pantheonConfig.compaction !== undefined) {
|
|
780
785
|
mergeMissing(config.compaction, pantheonConfig.compaction)
|
|
781
786
|
}
|
|
782
787
|
|
|
783
788
|
// --------------------------------------------------------------------
|
|
784
789
|
// C. Merge permissions
|
|
785
790
|
// --------------------------------------------------------------------
|
|
786
|
-
if (
|
|
787
|
-
if (pantheonConfig.permission) {
|
|
791
|
+
if (config.permission === undefined) config.permission = {}
|
|
792
|
+
if (pantheonConfig.permission !== undefined) {
|
|
788
793
|
mergeMissing(config.permission, pantheonConfig.permission)
|
|
789
794
|
}
|
|
790
|
-
if (
|
|
791
|
-
config.permission
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
'
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
795
|
+
if (
|
|
796
|
+
typeof config.permission === 'object' &&
|
|
797
|
+
config.permission !== null &&
|
|
798
|
+
!Array.isArray(config.permission)
|
|
799
|
+
) {
|
|
800
|
+
if (componentSet.has('skills')) {
|
|
801
|
+
config.permission.skill = { '*': 'allow' }
|
|
802
|
+
}
|
|
803
|
+
if (config.permission.bash === undefined) {
|
|
804
|
+
config.permission.bash = {
|
|
805
|
+
'git *': 'allow',
|
|
806
|
+
'npm *': 'allow',
|
|
807
|
+
'npx *': 'allow',
|
|
808
|
+
'pytest *': 'allow',
|
|
809
|
+
'ruff *': 'allow',
|
|
810
|
+
'black *': 'allow',
|
|
811
|
+
'pip *': 'allow',
|
|
812
|
+
'docker *': 'allow',
|
|
813
|
+
'curl *': 'allow',
|
|
814
|
+
'gh *': 'allow',
|
|
815
|
+
'make *': 'allow',
|
|
816
|
+
}
|
|
806
817
|
}
|
|
807
818
|
}
|
|
808
819
|
|
|
@@ -816,9 +827,9 @@ export async function installOpenCode(
|
|
|
816
827
|
// glob is no longer needed — keeping it would duplicate content under V1
|
|
817
828
|
// and is ignored under V2 anyway. Only ensure AGENTS.md is present.
|
|
818
829
|
const pantheonInstructions = ['AGENTS.md']
|
|
819
|
-
if (
|
|
830
|
+
if (config.instructions === undefined) {
|
|
820
831
|
config.instructions = [...pantheonInstructions]
|
|
821
|
-
} else {
|
|
832
|
+
} else if (Array.isArray(config.instructions)) {
|
|
822
833
|
// Strip stale per-file instruction globs from PREVIOUS installs
|
|
823
834
|
// (e.g. `instructions/*.instructions.md` / `src/instructions/*.md`).
|
|
824
835
|
// The merge is additive-only otherwise, so without this filter existing
|
|
@@ -838,7 +849,7 @@ export async function installOpenCode(
|
|
|
838
849
|
// --------------------------------------------------------------------
|
|
839
850
|
// E. Ensure $schema
|
|
840
851
|
// --------------------------------------------------------------------
|
|
841
|
-
if (
|
|
852
|
+
if (config.$schema === undefined) {
|
|
842
853
|
config.$schema = 'https://opencode.ai/config.json'
|
|
843
854
|
}
|
|
844
855
|
|
|
@@ -1009,12 +1020,14 @@ export async function installOpenCode(
|
|
|
1009
1020
|
}
|
|
1010
1021
|
|
|
1011
1022
|
// -----------------------------------------------------------------------
|
|
1012
|
-
// 5. Model preset selection (--components agents). Interactive
|
|
1023
|
+
// 5. Model preset selection (--components agents). Interactive wizard runs
|
|
1013
1024
|
// unless autoYes (use defaults) or an explicit --preset was given.
|
|
1025
|
+
// Q1 default "herdar do chat" (inherit) writes no active-preset.json —
|
|
1026
|
+
// delegates inherit the parent chat model (native inheritance).
|
|
1014
1027
|
// -----------------------------------------------------------------------
|
|
1015
1028
|
if (interactive && !dryRun && !autoYes && !opts.preset) {
|
|
1016
|
-
const {
|
|
1017
|
-
await
|
|
1029
|
+
const { runInitWizard } = await import('./model-picker.mjs')
|
|
1030
|
+
await runInitWizard({ presetDir: target, logger: console })
|
|
1018
1031
|
}
|
|
1019
1032
|
|
|
1020
1033
|
if (opts.preset && !dryRun) {
|
package/src/agents/zeus.md
CHANGED
|
@@ -144,33 +144,23 @@ task_status(task_id="ses_xxx", wait=true)
|
|
|
144
144
|
-> bloqueia ate completar: { state: "completed", task_result: "..." }
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
-
### Pantheon Delegation
|
|
147
|
+
### Pantheon Delegation — BackgroundJobBoard (plugin)
|
|
148
148
|
|
|
149
|
-
O plugin Pantheon
|
|
149
|
+
O plugin Pantheon mantém um **BackgroundJobBoard** persistente
|
|
150
|
+
(`.pantheon/board/state.json`) que rastreia trabalho em voo e o carrega para o
|
|
151
|
+
contexto de compactação. O toolset de delegação do plugin
|
|
152
|
+
(`pantheon_delegate`, `pantheon_delegation_read`, `pantheon_delegation_list`)
|
|
153
|
+
foi **removido em 1.4.3** — dispatch e coleta usam a API nativa
|
|
154
|
+
`task(background=true)` / `task_status(wait=true)`.
|
|
150
155
|
|
|
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
156
|
- **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
157
|
|
|
168
|
-
### DELEGATION_RULES (
|
|
158
|
+
### DELEGATION_RULES (native task path)
|
|
169
159
|
|
|
170
|
-
1. **NUNCA** polle `
|
|
171
|
-
2. Voce NAO recebe notificacao injetada no chat — politica do usuario: ZERO `task-notification` no transcript. Visibilidade de conclusao: marcador `[unread]`
|
|
172
|
-
3. Use `
|
|
173
|
-
4. `
|
|
160
|
+
1. **NUNCA** polle `task_status` para checar se um job terminou — use `wait=true` para fan-in explicito.
|
|
161
|
+
2. Voce NAO recebe notificacao injetada no chat — politica do usuario: ZERO `task-notification` no transcript. Visibilidade de conclusao: marcador `[unread]` no board, toasts TUI (pantheon-hooks) e carry-forward de compactacao.
|
|
162
|
+
3. Use `task_status({task_id, wait: true})` APENAS para fan-in explicito / recuperacao de resultado sob demanda (bloqueia ate o fim).
|
|
163
|
+
4. `task_status` sem `wait` e para diagnostico, nao para polling.
|
|
174
164
|
|
|
175
165
|
### Background (sempre usar)
|
|
176
166
|
- **Apollo, Hermes, Aphrodite, Demeter, Hephaestus, Prometheus**
|
|
@@ -324,9 +314,8 @@ out = await guard.maybeRetry(result, async () => { ...task() de novo... })
|
|
|
324
314
|
Regra: retry 1x APENAS em `empty-mode1`/`empty-mode2`. Resultado com conteudo
|
|
325
315
|
nunca redispara. Apos 1 retry vazio → escalate (mesma regra do TODO Enforcer).
|
|
326
316
|
|
|
327
|
-
> **Waves DEVEM usar `
|
|
328
|
-
> Uso alto-nível: `import {
|
|
329
|
-
> Uso baixo-nível: `createZeusRetryHelper().executeWithRetry(first, () => secondRead())` — classifica empty e decide retry/escalate (mesmo cap 1).
|
|
317
|
+
> **Waves DEVEM usar `dispatch-guard`** (`src/pantheon/dispatch-guard.ts`) — encapsula `task → classify → retry 1x → escalate` com cap duro 1; se ainda vazio throw `ZeusEscalationError` (escale: tente outro agente, simplifique, manual).
|
|
318
|
+
> Uso alto-nível: `import { createDispatchGuard } from './pantheon/dispatch-guard.ts'` → `await guard.maybeRetry(result, () => task(...))`.
|
|
330
319
|
|
|
331
320
|
### /cost — pantheon_cost tool (WIRED no plugin)
|
|
332
321
|
`pantheon_cost({ days?: number })` le o `opencode.db` READ-ONLY (node:sqlite;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cost Command (Wave 4, PR #46) — `pantheon_cost` structural tool: cost +
|
|
3
|
-
* token visibility
|
|
3
|
+
* token visibility by agent, straight from opencode.db.
|
|
4
4
|
*
|
|
5
5
|
* Reads the opencode session database READ-ONLY (sum cost + tokens by
|
|
6
6
|
* agent over the last N days → markdown table). Zero new dependencies:
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* dbPath resolution: explicit option > env PANTHEON_COST_DB > the version-aware
|
|
14
14
|
* default selected by PANTHEON_OPENCODE_VERSION (v1 or v2). An unset version
|
|
15
15
|
* deliberately preserves V1 behavior. The tool is wired in
|
|
16
|
-
* plugin.ts
|
|
17
|
-
*
|
|
16
|
+
* plugin.ts (usable by zeus and any agent with tool access — no routing change
|
|
17
|
+
* needed).
|
|
18
18
|
*
|
|
19
19
|
* @module cost-command
|
|
20
20
|
*/
|
|
@@ -25,7 +25,7 @@ import { join } from 'node:path'
|
|
|
25
25
|
import { promisify } from 'node:util'
|
|
26
26
|
import { z } from 'zod'
|
|
27
27
|
|
|
28
|
-
import type { ToolContextLike } from './
|
|
28
|
+
import type { ToolContextLike } from './tool-context.ts'
|
|
29
29
|
|
|
30
30
|
const execFileAsync = promisify(execFile)
|
|
31
31
|
|
|
@@ -44,7 +44,7 @@ export interface CostCommandOptions {
|
|
|
44
44
|
|
|
45
45
|
export type OpenCodeVersion = 'v1' | 'v2'
|
|
46
46
|
|
|
47
|
-
/** One cost tool: description + zod args shape + execute
|
|
47
|
+
/** One cost tool: description + zod args shape + execute. */
|
|
48
48
|
export interface CostTool<Args extends z.ZodRawShape = typeof costArgs> {
|
|
49
49
|
description: string
|
|
50
50
|
args: Args
|
|
@@ -62,7 +62,7 @@ const costArgs = {
|
|
|
62
62
|
.min(1)
|
|
63
63
|
.max(365)
|
|
64
64
|
.optional()
|
|
65
|
-
.describe('Number of days of
|
|
65
|
+
.describe('Number of days of cost history to include (default 7).'),
|
|
66
66
|
} satisfies z.ZodRawShape
|
|
67
67
|
|
|
68
68
|
/** Return the safe, isolated state DB name for an OpenCode version. */
|
|
@@ -239,7 +239,7 @@ export function createCostCommand(options?: CostCommandOptions): CostCommand {
|
|
|
239
239
|
return {
|
|
240
240
|
pantheon_cost: {
|
|
241
241
|
description:
|
|
242
|
-
'Report
|
|
242
|
+
'Report cost + token usage by agent over the last N days, read from opencode.db (read-only, no writes).',
|
|
243
243
|
args: costArgs,
|
|
244
244
|
execute,
|
|
245
245
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Read-Only Enforcement (Phase 4) — deny mutating tools inside sessions that
|
|
3
|
-
* were delegated as read-only (advisory `read_only: true` on
|
|
4
|
-
*
|
|
3
|
+
* were delegated as read-only (advisory `read_only: true` on the native
|
|
4
|
+
* `task` delegate, or the agent ∈ `readOnlyAgents` from routing.yml).
|
|
5
5
|
*
|
|
6
6
|
* OpenCode hook: `tool.execute.before`. The hook shape is
|
|
7
7
|
* `(input, output) => Promise<void>`; THROWING from the hook denies the tool
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
* - `task` is blocked too, which hard-enforces depth-2: a read-only agent
|
|
19
19
|
* cannot spawn its own subagents.
|
|
20
20
|
*
|
|
21
|
-
* Unknown sessions are allowed for ordinary tools.
|
|
22
|
-
* fail-closed when
|
|
21
|
+
* Unknown sessions are allowed for ordinary tools. The runtime matrix is
|
|
22
|
+
* fail-closed when wired: missing agent identity or
|
|
23
23
|
* target metadata is denied rather than inferred.
|
|
24
24
|
*
|
|
25
|
-
* The registry is populated
|
|
25
|
+
* The registry is populated at delegate time: the CHILD
|
|
26
26
|
* session is registered when the delegate call is read-only.
|
|
27
27
|
*
|
|
28
28
|
* @module delegation-enforce
|
|
@@ -147,7 +147,6 @@ export function isDelegationAllowed(
|
|
|
147
147
|
function targetFromTool(tool: string, args: unknown): string | undefined {
|
|
148
148
|
if (args === null || typeof args !== 'object') return undefined
|
|
149
149
|
const record = args as Record<string, unknown>
|
|
150
|
-
if (tool === 'pantheon_delegate') return normalizeDelegationAgent(record.agent)
|
|
151
150
|
if (tool === 'task') {
|
|
152
151
|
return normalizeDelegationAgent(record.subagent_type ?? record.subagentType)
|
|
153
152
|
}
|
|
@@ -236,7 +235,7 @@ export function zeusReadGuard(tool: string, args: unknown, agent: string | undef
|
|
|
236
235
|
// ─── Registry ──────────────────────────────────────────────────────────
|
|
237
236
|
|
|
238
237
|
/**
|
|
239
|
-
* Read-only session registry.
|
|
238
|
+
* Read-only session registry. The plugin's delegate wiring registers child
|
|
240
239
|
* sessions here; the plugin's `tool.execute.before` guard consults it.
|
|
241
240
|
* Process-wide singleton for plugin wiring + factory for test isolation.
|
|
242
241
|
*/
|
|
@@ -291,19 +290,19 @@ export function createEnforcementGuard(input: {
|
|
|
291
290
|
options?: EnforcementGuardOptions
|
|
292
291
|
}): ToolExecuteBeforeHandler {
|
|
293
292
|
const blockedTools = input.options?.blockedTools ?? DEFAULT_BLOCKED_TOOLS
|
|
294
|
-
const prefix = input.options?.messagePrefix ?? '
|
|
293
|
+
const prefix = input.options?.messagePrefix ?? 'delegation guard'
|
|
295
294
|
|
|
296
295
|
return async (hook: ToolExecuteBeforeInput, output?: ToolExecuteBeforeOutput): Promise<void> => {
|
|
297
|
-
const isDelegationTool = hook.tool === 'task'
|
|
296
|
+
const isDelegationTool = hook.tool === 'task'
|
|
298
297
|
if (isDelegationTool && input.options?.isChildSession?.(hook.sessionID) === true) {
|
|
299
298
|
throw new Error(
|
|
300
299
|
`${prefix}: session ${hook.sessionID} is a child session — delegation tools are denied ` +
|
|
301
300
|
'to prevent nested delegation',
|
|
302
301
|
)
|
|
303
302
|
}
|
|
304
|
-
//
|
|
305
|
-
// time, so its matrix check lives in
|
|
306
|
-
// session-scoped agent learned from chat.params
|
|
303
|
+
// The native task() delegate has the authoritative ToolContext.agent at
|
|
304
|
+
// execution time, so its matrix check lives in dispatch-guard.ts. The hook
|
|
305
|
+
// only has the session-scoped agent learned from chat.params.
|
|
307
306
|
if (
|
|
308
307
|
hook.tool === 'task' &&
|
|
309
308
|
input.options?.isRootSession !== undefined &&
|
|
@@ -336,7 +335,7 @@ export function createEnforcementGuard(input: {
|
|
|
336
335
|
`This session was delegated for investigation only (read-only agent); it cannot ` +
|
|
337
336
|
`modify files, run commands, or spawn subagents. ` +
|
|
338
337
|
`If a write result is needed, dispatch a write-capable agent ` +
|
|
339
|
-
`(e.g. hermes, aphrodite) with
|
|
338
|
+
`(e.g. hermes, aphrodite) with the native task() tool instead.`,
|
|
340
339
|
)
|
|
341
340
|
}
|
|
342
341
|
}
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
import { z } from 'zod'
|
|
18
18
|
|
|
19
19
|
import type { BackgroundJobBoard } from './background-job-board.ts'
|
|
20
|
-
import type { ToolContextLike } from './delegation.ts'
|
|
21
20
|
import { GoalStore } from './goal-store.ts'
|
|
22
21
|
import { createPantheonLogger } from './logger.ts'
|
|
23
22
|
import { safeSessionPath } from './session-guard.ts'
|
|
24
23
|
import type { TodoEnforcerMessage } from './todo-enforcer.ts'
|
|
24
|
+
import type { ToolContextLike } from './tool-context.ts'
|
|
25
25
|
|
|
26
26
|
// Silence-by-default TUI policy (pantheon-hooks L42-58): warn → hooks.log,
|
|
27
27
|
// console echo opt-in via PANTHEON_HOOKS_LOG=1. `deps.logger` stays for tests.
|
|
@@ -103,7 +103,7 @@ const goalUpdateArgs = {
|
|
|
103
103
|
|
|
104
104
|
const goalGetArgs = {} satisfies z.ZodRawShape
|
|
105
105
|
|
|
106
|
-
/** One structural goal tool: description + zod args shape + execute
|
|
106
|
+
/** One structural goal tool: description + zod args shape + execute. */
|
|
107
107
|
export type GoalTool<Args extends z.ZodRawShape> = {
|
|
108
108
|
description: string
|
|
109
109
|
args: Args
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
|
|
16
16
|
import { join } from 'node:path'
|
|
17
17
|
|
|
18
|
-
import { assertSafeParentSessionID } from './
|
|
18
|
+
import { assertSafeParentSessionID } from './tool-context.ts'
|
|
19
19
|
|
|
20
20
|
export type GoalStatus = 'pending' | 'in_progress' | 'done'
|
|
21
21
|
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Hashline edit tool (Wave 2, PR #46) — structural `hashline_edit` tool that
|
|
3
3
|
* anchors edits to stable sha256 content tags instead of fragile line numbers.
|
|
4
4
|
*
|
|
5
|
-
* Tool shape
|
|
6
|
-
* zod-validated
|
|
5
|
+
* Tool shape follows the plugin tool convention (`{description, args, execute}`,
|
|
6
|
+
* zod-validated) used across the Pantheon toolset.
|
|
7
7
|
*
|
|
8
8
|
* Safety model (council-approved):
|
|
9
9
|
* 1. ALL refs are validated against the ORIGINAL file snapshot BEFORE any
|