pantheon-opencode 1.3.5-beta.52.c75b04d → 1.3.5-beta.57.2e03676
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 +25 -3
- package/opencode.json +0 -9
- package/package.json +1 -1
- package/scripts/doctor.mjs +203 -2
- package/scripts/validate-routing.mjs +165 -1
- package/src/agents/aphrodite.md +3 -1
- package/src/agents/apollo.md +2 -0
- package/src/agents/athena.md +4 -1
- package/src/agents/demeter.md +3 -1
- package/src/agents/gaia.md +2 -0
- package/src/agents/hephaestus.md +3 -1
- package/src/agents/hermes.md +4 -1
- package/src/agents/iris.md +3 -1
- package/src/agents/mnemosyne.md +3 -1
- package/src/agents/nyx.md +3 -1
- package/src/agents/prometheus.md +3 -1
- package/src/agents/talos.md +3 -1
- package/src/agents/themis.md +3 -1
- package/src/pantheon/delegation-enforce.ts +139 -5
- package/src/pantheon/delegation-finalize.ts +24 -0
- package/src/pantheon/delegation.ts +128 -15
- package/src/plugin.ts +43 -3
- package/src/plugins/tui/package.json +1 -1
- package/src/routing.yml +12 -9
package/README.md
CHANGED
|
@@ -254,6 +254,25 @@ transition.
|
|
|
254
254
|
**Timeout:** `background_delegation.timeout_ms` (default `900000` = 15 min). A job
|
|
255
255
|
that has not reached a terminal state is finalized as `error`/`timedOut`.
|
|
256
256
|
|
|
257
|
+
**Runtime delegation environment variables:**
|
|
258
|
+
|
|
259
|
+
| Variable | Default | Scope and behavior |
|
|
260
|
+
|---|---:|---|
|
|
261
|
+
| `PANTHEON_ATHENA_APOLLO_BUDGET` | `5` dispatches | In-memory per `createDelegationTools()` factory/process instance and parent session, for the `athena → apollo` read-only exception. |
|
|
262
|
+
| `PANTHEON_HERMES_APOLLO_BUDGET` | `5` dispatches | In-memory per `createDelegationTools()` factory/process instance and parent session, for the `hermes → apollo` read-only exception. |
|
|
263
|
+
| `PANTHEON_DELEGATION_TIMEOUT_MS` | `900000` ms (15 min) | Per delegated child wall-clock timeout. The value is read when the plugin factory is created; unset or invalid values use the default. |
|
|
264
|
+
|
|
265
|
+
Budgets are reserved before asynchronous child-session creation, so real
|
|
266
|
+
concurrent calls cannot overshoot them. They reset when the factory/process
|
|
267
|
+
restarts. If `ToolContext.agent` is missing, the default runtime matrix rejects
|
|
268
|
+
the delegation (fail-closed); no caller identity or budget is inferred from the
|
|
269
|
+
target, prompt, or session state. An explicitly legacy host using
|
|
270
|
+
`enforceRuntimeMatrix: false` skips the budget because the caller cannot be
|
|
271
|
+
identified and emits a warning. These budgets are separate from
|
|
272
|
+
`background_delegation.max_concurrent_per_agent`: concurrency limits the number
|
|
273
|
+
of currently running children, while an exception budget limits total dispatch
|
|
274
|
+
reservations for that parent session during the factory lifetime.
|
|
275
|
+
|
|
257
276
|
**Read-only enforcement:** delegating with `read_only: true`, or delegating to an
|
|
258
277
|
agent in `background_delegation.read_only_agents` (`apollo`, `gaia`), registers
|
|
259
278
|
the child session as read-only — the `tool.execute.before` guard denies
|
|
@@ -892,9 +911,12 @@ publishing. If they diverge, the release is blocked until they are reconciled.
|
|
|
892
911
|
|
|
893
912
|
Beta releases are calculated from npm's published `latest` at workflow runtime:
|
|
894
913
|
the default is the next patch, formatted as
|
|
895
|
-
`<next-stable>-beta.<PR>.<short-sha>`.
|
|
896
|
-
|
|
897
|
-
|
|
914
|
+
`<next-stable>-beta.<PR>.<short-sha>`. Only the exact `release:beta` label
|
|
915
|
+
triggers this path; alternate label variants do not trigger it. Beta recovery
|
|
916
|
+
requires an explicit `workflow_dispatch` with its recovery inputs; a normal
|
|
917
|
+
push does not republish beta. The workflow owns the manifest update, tag, and
|
|
918
|
+
publish, creates the GitHub Release as `Pantheon <version>`, and does not create
|
|
919
|
+
PR comments.
|
|
898
920
|
|
|
899
921
|
### Anti-patterns
|
|
900
922
|
|
package/opencode.json
CHANGED
|
@@ -38,15 +38,6 @@
|
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
40
|
"agent": {
|
|
41
|
-
"athena": {
|
|
42
|
-
"model": "opencode-go/kimi-k3"
|
|
43
|
-
},
|
|
44
|
-
"themis": {
|
|
45
|
-
"model": "opencode-go/deepseek-v4-pro"
|
|
46
|
-
},
|
|
47
|
-
"hephaestus": {
|
|
48
|
-
"model": "opencode-go/deepseek-v4-pro"
|
|
49
|
-
},
|
|
50
41
|
"talos": {
|
|
51
42
|
"permission": {
|
|
52
43
|
"edit": {
|
package/package.json
CHANGED
package/scripts/doctor.mjs
CHANGED
|
@@ -757,7 +757,7 @@ function checkPermissionMismatches(args) {
|
|
|
757
757
|
// Check D: Sync Status
|
|
758
758
|
// ---------------------------------------------------------------------------
|
|
759
759
|
|
|
760
|
-
function checkSyncStatus(
|
|
760
|
+
function checkSyncStatus(_args) {
|
|
761
761
|
section('D. Sync Status')
|
|
762
762
|
|
|
763
763
|
// Platform sync removed in v1.0 (OpenCode-only)
|
|
@@ -979,9 +979,207 @@ function checkVenvLayer(args) {
|
|
|
979
979
|
}
|
|
980
980
|
|
|
981
981
|
// ---------------------------------------------------------------------------
|
|
982
|
-
//
|
|
982
|
+
// Check G: AGENTS.md freshness
|
|
983
983
|
// ---------------------------------------------------------------------------
|
|
984
984
|
|
|
985
|
+
/**
|
|
986
|
+
* Check AGENTS.md only where the repository's generator can provide a real
|
|
987
|
+
* source-of-truth comparison. Installed targets have no canonical instruction
|
|
988
|
+
* sources available to this process, so their freshness is deliberately not
|
|
989
|
+
* guessed from file age.
|
|
990
|
+
*/
|
|
991
|
+
function checkAgentsMdFreshness(args) {
|
|
992
|
+
section('G. AGENTS.md Freshness')
|
|
993
|
+
|
|
994
|
+
const agentsMdPath = join(args.target, 'AGENTS.md')
|
|
995
|
+
if (args.target !== ROOT) {
|
|
996
|
+
const outcome = classifyAgentsMdFreshness({
|
|
997
|
+
targetIsRoot: false,
|
|
998
|
+
agentsMdExists: existsSync(agentsMdPath),
|
|
999
|
+
generatorExists: false,
|
|
1000
|
+
})
|
|
1001
|
+
if (outcome === 'unverified') {
|
|
1002
|
+
info(
|
|
1003
|
+
'AGENTS.md is present; freshness was not checked because the canonical generator sources are not available in an installed target',
|
|
1004
|
+
)
|
|
1005
|
+
} else {
|
|
1006
|
+
warn(
|
|
1007
|
+
'AGENTS.md is not present; freshness cannot be verified for this installed target — run `pantheon-opencode init --components instructions`',
|
|
1008
|
+
)
|
|
1009
|
+
}
|
|
1010
|
+
return
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const generator = join(ROOT, 'scripts', 'build-agents-md.mjs')
|
|
1014
|
+
if (!existsSync(generator)) {
|
|
1015
|
+
info('AGENTS.md freshness skipped — canonical generator is not available')
|
|
1016
|
+
return
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const result = spawn(process.execPath, [generator, '--check'], ROOT)
|
|
1020
|
+
const outcome = classifyAgentsMdFreshness({
|
|
1021
|
+
targetIsRoot: true,
|
|
1022
|
+
agentsMdExists: existsSync(agentsMdPath),
|
|
1023
|
+
generatorExists: true,
|
|
1024
|
+
generatorStatus: result.status,
|
|
1025
|
+
})
|
|
1026
|
+
if (outcome === 'pass') {
|
|
1027
|
+
pass('AGENTS.md matches the canonical generated instructions')
|
|
1028
|
+
} else if (outcome === 'unverified' && !existsSync(agentsMdPath)) {
|
|
1029
|
+
warn(
|
|
1030
|
+
'AGENTS.md is missing — run `node scripts/build-agents-md.mjs` or `pantheon-opencode init`',
|
|
1031
|
+
)
|
|
1032
|
+
} else if (outcome === 'stale') {
|
|
1033
|
+
warn('AGENTS.md is stale — run `node scripts/build-agents-md.mjs` to refresh it')
|
|
1034
|
+
} else {
|
|
1035
|
+
warn(
|
|
1036
|
+
`AGENTS.md freshness could not be verified (generator exited ${result.status ?? 'unknown'})`,
|
|
1037
|
+
)
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* Classify the generator result without relying on timestamps or fragile
|
|
1043
|
+
* process mocks. The doctor CLI uses the same result contract internally.
|
|
1044
|
+
*
|
|
1045
|
+
* @param {{ targetIsRoot: boolean; agentsMdExists: boolean; generatorExists: boolean; generatorStatus?: number | null }} input
|
|
1046
|
+
* @returns {'pass' | 'missing' | 'stale' | 'skipped' | 'unverified'}
|
|
1047
|
+
*/
|
|
1048
|
+
export function classifyAgentsMdFreshness(input) {
|
|
1049
|
+
if (!input.targetIsRoot) return input.agentsMdExists ? 'unverified' : 'missing'
|
|
1050
|
+
if (!input.generatorExists) return 'skipped'
|
|
1051
|
+
if (input.generatorStatus === 0) return 'pass'
|
|
1052
|
+
if (input.generatorStatus === 1 && input.agentsMdExists) return 'stale'
|
|
1053
|
+
return 'unverified'
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// ---------------------------------------------------------------------------
|
|
1057
|
+
// Check H: Config migration — no top-level subagent_depth
|
|
1058
|
+
// ---------------------------------------------------------------------------
|
|
1059
|
+
|
|
1060
|
+
function checkSubagentDepthPlacement(args) {
|
|
1061
|
+
section('H. Config Migration')
|
|
1062
|
+
|
|
1063
|
+
const configs = collectMcpConfigs(args)
|
|
1064
|
+
if (configs.length === 0) {
|
|
1065
|
+
info('subagent_depth placement check skipped — no opencode.json was found')
|
|
1066
|
+
return
|
|
1067
|
+
}
|
|
1068
|
+
for (const cfg of configs) {
|
|
1069
|
+
if (cfg.data.subagent_depth !== undefined) {
|
|
1070
|
+
error(
|
|
1071
|
+
`${cfg.label}: top-level "subagent_depth" is unsupported — run \`pantheon-opencode init\` to migrate it to "experimental.subagent_depth"`,
|
|
1072
|
+
)
|
|
1073
|
+
} else if (
|
|
1074
|
+
cfg.data.experimental &&
|
|
1075
|
+
typeof cfg.data.experimental === 'object' &&
|
|
1076
|
+
cfg.data.experimental.subagent_depth !== undefined
|
|
1077
|
+
) {
|
|
1078
|
+
pass(`${cfg.label}: experimental.subagent_depth = ${cfg.data.experimental.subagent_depth}`)
|
|
1079
|
+
} else {
|
|
1080
|
+
info(
|
|
1081
|
+
`${cfg.label}: experimental.subagent_depth is not configured (older/custom config may omit it)`,
|
|
1082
|
+
)
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// ---------------------------------------------------------------------------
|
|
1088
|
+
// Check J: Agent permission.task presence
|
|
1089
|
+
// ---------------------------------------------------------------------------
|
|
1090
|
+
|
|
1091
|
+
/** @param {string} content @returns {boolean} */
|
|
1092
|
+
export function hasPermissionTask(content) {
|
|
1093
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
1094
|
+
if (!match) return false
|
|
1095
|
+
const permission = match[1].match(/(?:^|\r?\n)permission:\s*\r?\n([\s\S]*?)(?=\r?\n\S|$)/)
|
|
1096
|
+
return Boolean(permission && /^\s+task:\s*/m.test(permission[1]))
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Derive installed agent files from the paths already declared by configs.
|
|
1101
|
+
* This supports both global flat and project .opencode layouts without
|
|
1102
|
+
* assuming a user home directory.
|
|
1103
|
+
*
|
|
1104
|
+
* @param {{path: string, data: object}[]} configs
|
|
1105
|
+
* @returns {string[]}
|
|
1106
|
+
*/
|
|
1107
|
+
export function deriveInstalledAgentFiles(configs) {
|
|
1108
|
+
const directories = new Set()
|
|
1109
|
+
for (const cfg of configs) {
|
|
1110
|
+
for (const agent of Object.values(cfg.data.agent ?? {})) {
|
|
1111
|
+
const source = agent && typeof agent === 'object' ? agent.source : undefined
|
|
1112
|
+
if (typeof source !== 'string' || !source.includes('/')) continue
|
|
1113
|
+
const filePath = resolve(dirname(cfg.path), source)
|
|
1114
|
+
if (filePath.startsWith(`${AGENTS_DIR}/`)) continue
|
|
1115
|
+
if (existsSync(filePath)) directories.add(dirname(filePath))
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const files = []
|
|
1120
|
+
for (const directory of directories) {
|
|
1121
|
+
let entries
|
|
1122
|
+
try {
|
|
1123
|
+
entries = readdirSync(directory, { withFileTypes: true })
|
|
1124
|
+
} catch {
|
|
1125
|
+
continue
|
|
1126
|
+
}
|
|
1127
|
+
for (const entry of entries) {
|
|
1128
|
+
if (entry.isFile() && entry.name.endsWith('.md')) files.push(join(directory, entry.name))
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return [...new Set(files)].sort()
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function checkPermissionTaskPresence(args) {
|
|
1135
|
+
section('J. Installed Agent permission.task')
|
|
1136
|
+
|
|
1137
|
+
if (args.profile === 'lite') {
|
|
1138
|
+
info(
|
|
1139
|
+
'Installed agent permission.task check skipped (lite profile does not require managed agents)',
|
|
1140
|
+
)
|
|
1141
|
+
return
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const installedFiles = deriveInstalledAgentFiles(collectMcpConfigs(args))
|
|
1145
|
+
if (installedFiles.length === 0) {
|
|
1146
|
+
info(
|
|
1147
|
+
'Installed agent permission.task check skipped — no agent source directory could be derived from available config',
|
|
1148
|
+
)
|
|
1149
|
+
return
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const missing = findMissingPermissionTask(installedFiles)
|
|
1153
|
+
if (classifyPermissionTaskCheck(args.profile, installedFiles.length, missing.length) === 'pass') {
|
|
1154
|
+
pass(`All ${installedFiles.length} installed agents have permission.task`)
|
|
1155
|
+
return
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
for (const filePath of missing) {
|
|
1159
|
+
error(
|
|
1160
|
+
`${basename(filePath)}: missing permission.task in managed agent — run \`pantheon-opencode init\` to update installed agents`,
|
|
1161
|
+
)
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/** @param {string[]} filePaths @returns {string[]} */
|
|
1166
|
+
export function findMissingPermissionTask(filePaths) {
|
|
1167
|
+
return filePaths.filter((filePath) => !hasPermissionTask(readFileSync(filePath, 'utf8')))
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Classify the installed-agent permission.task layer for a doctor profile.
|
|
1172
|
+
*
|
|
1173
|
+
* @param {string} profile
|
|
1174
|
+
* @param {number} installedCount
|
|
1175
|
+
* @param {number} missingCount
|
|
1176
|
+
* @returns {'skip' | 'pass' | 'error'}
|
|
1177
|
+
*/
|
|
1178
|
+
export function classifyPermissionTaskCheck(profile, installedCount, missingCount) {
|
|
1179
|
+
if (profile === 'lite' || installedCount === 0) return 'skip'
|
|
1180
|
+
return missingCount === 0 ? 'pass' : 'error'
|
|
1181
|
+
}
|
|
1182
|
+
|
|
985
1183
|
/**
|
|
986
1184
|
* Return the final doctor status message without masking blocking errors.
|
|
987
1185
|
* @param {{ error: number; warn: number }} summaryCounts
|
|
@@ -1069,6 +1267,9 @@ async function main() {
|
|
|
1069
1267
|
checkPermissionMismatches(args)
|
|
1070
1268
|
checkSyncStatus(args)
|
|
1071
1269
|
checkGitStatus(args)
|
|
1270
|
+
checkAgentsMdFreshness(args)
|
|
1271
|
+
checkSubagentDepthPlacement(args)
|
|
1272
|
+
checkPermissionTaskPresence(args)
|
|
1072
1273
|
|
|
1073
1274
|
printSummary(args.target)
|
|
1074
1275
|
|
|
@@ -89,7 +89,9 @@ for (const dir of [srcSkillsDir, dotSkillsDir]) {
|
|
|
89
89
|
if (existsSync(join(skillDir, 'SKILL.md')) && !existingSkills.includes(d)) {
|
|
90
90
|
existingSkills.push(d)
|
|
91
91
|
}
|
|
92
|
-
} catch {
|
|
92
|
+
} catch {
|
|
93
|
+
/* skip */
|
|
94
|
+
}
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
}
|
|
@@ -165,6 +167,168 @@ for (const w of presetValidation.warnings) {
|
|
|
165
167
|
}
|
|
166
168
|
console.log(` Presets defined: ${Object.keys(presetDefs).length}`)
|
|
167
169
|
|
|
170
|
+
// G. Delegation Matrix Invariants (B1 — Zeus strict + read-only exceptions)
|
|
171
|
+
console.log(`\n Delegation matrix invariants:`)
|
|
172
|
+
|
|
173
|
+
// G1. can_delegate: true only for zeus, athena (→apollo), hermes (→apollo)
|
|
174
|
+
const CAN_DELEGATE_AGENTS = ['zeus', 'athena', 'hermes']
|
|
175
|
+
for (const [name, agentInfo] of Object.entries(routing.agents || {})) {
|
|
176
|
+
if (agentInfo.can_delegate === true) {
|
|
177
|
+
check(
|
|
178
|
+
CAN_DELEGATE_AGENTS.includes(name),
|
|
179
|
+
`Agent "${name}" has can_delegate: true but is not in allowed list [${CAN_DELEGATE_AGENTS}]`,
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// G3. subagent_can_delegate_to only references existing agents
|
|
185
|
+
for (const [name, agentInfo] of Object.entries(routing.agents || {})) {
|
|
186
|
+
const delegates = agentInfo.subagent_can_delegate_to || []
|
|
187
|
+
for (const target of delegates) {
|
|
188
|
+
check(
|
|
189
|
+
routingAgents.includes(target),
|
|
190
|
+
`Agent "${name}" can_delegate_to unknown agent "${target}"`,
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// G4. athena and hermes can only delegate to apollo (read-only exception)
|
|
196
|
+
for (const agentName of ['athena', 'hermes']) {
|
|
197
|
+
const agentInfo = routing.agents[agentName]
|
|
198
|
+
if (agentInfo) {
|
|
199
|
+
const delegates = agentInfo.subagent_can_delegate_to || []
|
|
200
|
+
check(
|
|
201
|
+
delegates.length <= 1 && delegates[0] === 'apollo',
|
|
202
|
+
`Agent "${agentName}" subagent_can_delegate_to must be [apollo] only, got [${delegates}]`,
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// G5. Frontmatter permission.task invariants
|
|
208
|
+
const AGENT_FILES_DIR = join(ROOT, 'src', 'agents')
|
|
209
|
+
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---/
|
|
210
|
+
|
|
211
|
+
function parseFrontmatter(filePath) {
|
|
212
|
+
const content = readFileSync(filePath, 'utf8')
|
|
213
|
+
const match = content.match(FRONTMATTER_RE)
|
|
214
|
+
if (!match) return null
|
|
215
|
+
try {
|
|
216
|
+
return yaml.load(match[1])
|
|
217
|
+
} catch {
|
|
218
|
+
return null
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// G5a. Frontmatter mode must match routing.yml task eligibility.
|
|
223
|
+
const frontmatterByAgent = new Map()
|
|
224
|
+
for (const agentName of canonicalFiles) {
|
|
225
|
+
frontmatterByAgent.set(agentName, parseFrontmatter(join(AGENT_FILES_DIR, `${agentName}.md`)))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for (const [name, agentInfo] of Object.entries(routing.agents || {})) {
|
|
229
|
+
const fm = frontmatterByAgent.get(name)
|
|
230
|
+
if (!fm) continue
|
|
231
|
+
|
|
232
|
+
if (name === 'zeus') {
|
|
233
|
+
check(fm.mode === 'primary', 'Agent "zeus" must use mode primary')
|
|
234
|
+
} else if (agentInfo.can_receive_delegation === true) {
|
|
235
|
+
const expectedMode = agentInfo.user_invocable === true ? 'all' : 'subagent'
|
|
236
|
+
check(
|
|
237
|
+
fm.mode === expectedMode,
|
|
238
|
+
`Agent "${name}" mode must be "${expectedMode}" for routing.yml eligibility, got "${fm.mode}"`,
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// G5b. Zeus targets must exist, receive delegation, and never be primary.
|
|
244
|
+
for (const targetEntry of routing.delegation?.zeus || []) {
|
|
245
|
+
const target = typeof targetEntry === 'string' ? targetEntry : targetEntry.target
|
|
246
|
+
const targetInfo = routing.agents[target]
|
|
247
|
+
const targetFm = frontmatterByAgent.get(target)
|
|
248
|
+
check(targetInfo !== undefined, `Zeus delegation target "${target}" must exist`)
|
|
249
|
+
if (targetInfo && targetFm) {
|
|
250
|
+
check(
|
|
251
|
+
targetInfo.can_receive_delegation === true && targetFm.mode !== 'primary',
|
|
252
|
+
`Zeus delegation target "${target}" must not be mode primary and must receive delegation`,
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// G5c. Every subagent delegation target must exist and be eligible.
|
|
258
|
+
for (const [name, agentInfo] of Object.entries(routing.agents || {})) {
|
|
259
|
+
for (const target of agentInfo.subagent_can_delegate_to || []) {
|
|
260
|
+
const targetInfo = routing.agents[target]
|
|
261
|
+
const targetFm = frontmatterByAgent.get(target)
|
|
262
|
+
check(targetInfo !== undefined, `Agent "${name}" can_delegate_to unknown agent "${target}"`)
|
|
263
|
+
if (targetInfo && targetFm) {
|
|
264
|
+
check(
|
|
265
|
+
targetInfo.can_receive_delegation === true && targetFm.mode !== 'primary',
|
|
266
|
+
`Agent "${name}" can_delegate_to target "${target}" is not eligible`,
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Expected permission.task matrix
|
|
273
|
+
const EXPECTED_TASK_PERMISSIONS = {
|
|
274
|
+
zeus: { '*': 'allow' },
|
|
275
|
+
athena: { '*': 'deny', apollo: 'allow' },
|
|
276
|
+
hermes: { '*': 'deny', apollo: 'allow' },
|
|
277
|
+
themis: { '*': 'deny' },
|
|
278
|
+
aphrodite: { '*': 'deny' },
|
|
279
|
+
demeter: { '*': 'deny' },
|
|
280
|
+
apollo: { '*': 'deny' },
|
|
281
|
+
gaia: { '*': 'deny' },
|
|
282
|
+
prometheus: { '*': 'deny' },
|
|
283
|
+
hephaestus: { '*': 'deny' },
|
|
284
|
+
nyx: { '*': 'deny' },
|
|
285
|
+
iris: { '*': 'deny' },
|
|
286
|
+
talos: { '*': 'deny' },
|
|
287
|
+
mnemosyne: { '*': 'deny' },
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
for (const agentName of canonicalFiles) {
|
|
291
|
+
const fmPath = join(AGENT_FILES_DIR, `${agentName}.md`)
|
|
292
|
+
const fm = parseFrontmatter(fmPath)
|
|
293
|
+
if (!fm) {
|
|
294
|
+
check(false, `Agent "${agentName}" has no valid frontmatter`)
|
|
295
|
+
continue
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// G5a. permission.task must exist
|
|
299
|
+
const hasTask = fm.permission?.task
|
|
300
|
+
check(hasTask, `Agent "${agentName}" missing permission.task in frontmatter`)
|
|
301
|
+
|
|
302
|
+
if (!hasTask) continue
|
|
303
|
+
|
|
304
|
+
// G5b. Only zeus may have "*": allow
|
|
305
|
+
const starRule = fm.permission.task['*']
|
|
306
|
+
if (starRule === 'allow') {
|
|
307
|
+
check(
|
|
308
|
+
agentName === 'zeus',
|
|
309
|
+
`Agent "${agentName}" has permission.task "*": allow — only zeus is allowed`,
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// G5c. Verify exact expected permissions
|
|
314
|
+
const expected = EXPECTED_TASK_PERMISSIONS[agentName]
|
|
315
|
+
if (expected) {
|
|
316
|
+
const actual = fm.permission.task
|
|
317
|
+
const actualKeys = Object.keys(actual).sort()
|
|
318
|
+
const expectedKeys = Object.keys(expected).sort()
|
|
319
|
+
check(
|
|
320
|
+
JSON.stringify(actualKeys) === JSON.stringify(expectedKeys),
|
|
321
|
+
`Agent "${agentName}" permission.task keys mismatch: expected [${expectedKeys}], got [${actualKeys}]`,
|
|
322
|
+
)
|
|
323
|
+
for (const [key, val] of Object.entries(expected)) {
|
|
324
|
+
check(
|
|
325
|
+
actual[key] === val,
|
|
326
|
+
`Agent "${agentName}" permission.task.${key}: expected "${val}", got "${actual[key]}"`,
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
168
332
|
// Summary
|
|
169
333
|
console.log(`\n${'='.repeat(50)}`)
|
|
170
334
|
const status = failures === 0 ? '✅ PASSED' : `❌ FAILED (${failures}/${checks} checks failed)`
|
package/src/agents/aphrodite.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: aphrodite
|
|
3
3
|
description: "Frontend specialist — React 19, TypeScript strict, WCAG accessibility, responsive design, TDD, modern API patterns, deprecated npm detection. Calls apollo for discovery, sends to themis for review."
|
|
4
|
-
mode:
|
|
4
|
+
mode: all
|
|
5
5
|
reasoning_effort: medium
|
|
6
6
|
permission:
|
|
7
7
|
read: allow
|
|
@@ -11,6 +11,8 @@ permission:
|
|
|
11
11
|
webfetch: allow
|
|
12
12
|
glob: allow
|
|
13
13
|
question: allow
|
|
14
|
+
task:
|
|
15
|
+
"*": deny
|
|
14
16
|
temperature: 0.3
|
|
15
17
|
steps: 50
|
|
16
18
|
skills:
|
package/src/agents/apollo.md
CHANGED
package/src/agents/athena.md
CHANGED
|
@@ -3,7 +3,7 @@ name: athena
|
|
|
3
3
|
description: Strategic planner & architect — research-first, plan-only, never implements.
|
|
4
4
|
Plans include quality gates (ruff/Biome, dep detection, LTS policy). Calls apollo
|
|
5
5
|
for discovery.
|
|
6
|
-
mode:
|
|
6
|
+
mode: all
|
|
7
7
|
reasoning_effort: high
|
|
8
8
|
|
|
9
9
|
steps: 30
|
|
@@ -21,6 +21,9 @@ permission:
|
|
|
21
21
|
grep: allow
|
|
22
22
|
glob: allow
|
|
23
23
|
webfetch: allow
|
|
24
|
+
task:
|
|
25
|
+
"*": deny
|
|
26
|
+
apollo: allow
|
|
24
27
|
---
|
|
25
28
|
|
|
26
29
|
## When NOT to Use Athena
|
package/src/agents/demeter.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: demeter
|
|
3
3
|
description: "Database specialist — SQLAlchemy 2.0, Alembic, query optimization, N+1 prevention, TDD migrations, modern DB libs. Calls apollo for discovery, sends to themis."
|
|
4
|
-
mode:
|
|
4
|
+
mode: all
|
|
5
5
|
reasoning_effort: medium
|
|
6
6
|
permission:
|
|
7
7
|
read: allow
|
|
@@ -9,6 +9,8 @@ permission:
|
|
|
9
9
|
edit: allow
|
|
10
10
|
bash: allow
|
|
11
11
|
webfetch: allow
|
|
12
|
+
task:
|
|
13
|
+
"*": deny
|
|
12
14
|
temperature: 0.2
|
|
13
15
|
steps: 40
|
|
14
16
|
skills:
|
package/src/agents/gaia.md
CHANGED
package/src/agents/hephaestus.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hephaestus
|
|
3
3
|
description: "AI tooling & pipelines specialist — LangChain/LangGraph chains, RAG architecture, vector stores, embedding strategies. Forges AI infrastructure. Calls apollo, sends to themis."
|
|
4
|
-
mode:
|
|
4
|
+
mode: all
|
|
5
5
|
reasoning_effort: medium
|
|
6
6
|
permission:
|
|
7
7
|
read: allow
|
|
@@ -9,6 +9,8 @@ permission:
|
|
|
9
9
|
edit: allow
|
|
10
10
|
bash: allow
|
|
11
11
|
webfetch: allow
|
|
12
|
+
task:
|
|
13
|
+
"*": deny
|
|
12
14
|
temperature: 0.3
|
|
13
15
|
steps: 40
|
|
14
16
|
skills:
|
package/src/agents/hermes.md
CHANGED
|
@@ -3,7 +3,7 @@ name: hermes
|
|
|
3
3
|
description: Backend specialist — FastAPI, Python, async, TDD (RED→GREEN→REFACTOR),
|
|
4
4
|
modern Python stdlib, obsolete lib detection. Calls apollo for discovery, sends
|
|
5
5
|
to themis.
|
|
6
|
-
mode:
|
|
6
|
+
mode: all
|
|
7
7
|
reasoning_effort: medium
|
|
8
8
|
|
|
9
9
|
mcp_tools:
|
|
@@ -25,6 +25,9 @@ permission:
|
|
|
25
25
|
edit: allow
|
|
26
26
|
webfetch: allow
|
|
27
27
|
glob: allow
|
|
28
|
+
task:
|
|
29
|
+
"*": deny
|
|
30
|
+
apollo: allow
|
|
28
31
|
---
|
|
29
32
|
|
|
30
33
|
## Table of Contents
|
package/src/agents/iris.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: iris
|
|
3
3
|
description: "GitHub operations specialist — branches, pull requests, issues, releases, tags. Called by zeus after review. Never pushes or merges without explicit human approval. Integrates with GitHub CLI (gh) for operations."
|
|
4
|
-
mode:
|
|
4
|
+
mode: all
|
|
5
5
|
reasoning_effort: low
|
|
6
6
|
permission:
|
|
7
7
|
read: allow
|
|
@@ -11,6 +11,8 @@ permission:
|
|
|
11
11
|
gh *: allow
|
|
12
12
|
webfetch: allow
|
|
13
13
|
edit: deny
|
|
14
|
+
task:
|
|
15
|
+
"*": deny
|
|
14
16
|
temperature: 0.2
|
|
15
17
|
steps: 25
|
|
16
18
|
skills:
|
package/src/agents/mnemosyne.md
CHANGED
|
@@ -3,7 +3,7 @@ name: mnemosyne
|
|
|
3
3
|
description: Memory bank quality owner — initializes .pantheon/memory-bank/, writes ADRs
|
|
4
4
|
and task records on explicit request. Called by zeus. Never invoked automatically
|
|
5
5
|
after phases.
|
|
6
|
-
mode:
|
|
6
|
+
mode: all
|
|
7
7
|
reasoning_effort: low
|
|
8
8
|
|
|
9
9
|
steps: 25
|
|
@@ -35,6 +35,8 @@ permission:
|
|
|
35
35
|
read: allow
|
|
36
36
|
grep: allow
|
|
37
37
|
edit: deny
|
|
38
|
+
task:
|
|
39
|
+
"*": deny
|
|
38
40
|
---
|
|
39
41
|
|
|
40
42
|
## ⚠️ SCOPE BOUNDARY
|
package/src/agents/nyx.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: nyx
|
|
3
3
|
description: "Observability & monitoring specialist — OpenTelemetry tracing, token/cost tracking, agent performance analytics, LangSmith integration. Calls apollo for discovery, sends to themis."
|
|
4
|
-
mode:
|
|
4
|
+
mode: all
|
|
5
5
|
reasoning_effort: low
|
|
6
6
|
permission:
|
|
7
7
|
read: allow
|
|
@@ -9,6 +9,8 @@ permission:
|
|
|
9
9
|
bash: allow
|
|
10
10
|
webfetch: allow
|
|
11
11
|
edit: deny
|
|
12
|
+
task:
|
|
13
|
+
"*": deny
|
|
12
14
|
temperature: 0.1
|
|
13
15
|
steps: 25
|
|
14
16
|
skills:
|
package/src/agents/prometheus.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: prometheus
|
|
3
3
|
description: "Infrastructure + model provider specialist — Docker, CI/CD, multi-model routing, cost optimization, provider abstraction"
|
|
4
|
-
mode:
|
|
4
|
+
mode: all
|
|
5
5
|
reasoning_effort: medium
|
|
6
6
|
permission:
|
|
7
7
|
read: allow
|
|
@@ -9,6 +9,8 @@ permission:
|
|
|
9
9
|
edit: allow
|
|
10
10
|
bash: allow
|
|
11
11
|
webfetch: allow
|
|
12
|
+
task:
|
|
13
|
+
"*": deny
|
|
12
14
|
temperature: 0.2
|
|
13
15
|
steps: 30
|
|
14
16
|
skills:
|
package/src/agents/talos.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: talos
|
|
3
3
|
description: "Hotfix express lane — direct fixes for small bugs, CSS, typos, minor logic. No TDD ceremony, no orchestration overhead. Standalone, no subagents. Escalates complex issues to zeus."
|
|
4
|
-
mode:
|
|
4
|
+
mode: all
|
|
5
5
|
reasoning_effort: low
|
|
6
6
|
permission:
|
|
7
7
|
read: allow
|
|
8
8
|
edit: allow
|
|
9
9
|
bash: allow
|
|
10
|
+
task:
|
|
11
|
+
"*": deny
|
|
10
12
|
temperature: 0.3
|
|
11
13
|
steps: 35
|
|
12
14
|
skills:
|