pantheon-opencode 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +37 -0
- package/LICENSE +21 -0
- package/README.md +1013 -0
- package/bin/pantheon-init.mjs +183 -0
- package/commands/pantheon-audit.md +25 -0
- package/commands/pantheon-bg.md +10 -0
- package/commands/pantheon-consolidate.md +11 -0
- package/commands/pantheon-deepwork.md +128 -0
- package/commands/pantheon-doc.md +10 -0
- package/commands/pantheon-focus.md +9 -0
- package/commands/pantheon-forget.md +58 -0
- package/commands/pantheon-hash.md +11 -0
- package/commands/pantheon-optimize.md +79 -0
- package/commands/pantheon-remember.md +44 -0
- package/commands/pantheon-search.md +48 -0
- package/commands/pantheon-status.md +71 -0
- package/commands/pantheon-todo.md +11 -0
- package/commands/pantheon.md +49 -0
- package/docs/AGENT-MCP.md +194 -0
- package/docs/ARCHITECTURE.md +384 -0
- package/docs/BRANCH-PROTECTION.md +142 -0
- package/docs/INDEX.md +81 -0
- package/docs/INSTALLATION.md +217 -0
- package/docs/MCP.md +238 -0
- package/docs/MEMORY.md +471 -0
- package/docs/MIGRATION-MEMORY-BANK.md +139 -0
- package/docs/PLATFORMS.md +5 -0
- package/docs/QUICKSTART.md +49 -0
- package/docs/README.md +18 -0
- package/docs/RELEASING.md +256 -0
- package/docs/SETUP.md +5 -0
- package/docs/UPGRADING.md +41 -0
- package/docs/mcp-recommendations.md +439 -0
- package/docs/mcp-tools.md +156 -0
- package/docs/mcp-user-guide.md +204 -0
- package/docs/persistence-mcp.md +111 -0
- package/package.json +72 -0
- package/pantheon.schema.json +158 -0
- package/scripts/__init__.py +0 -0
- package/scripts/_pantheon_paths.py +68 -0
- package/scripts/check-version-consistency.sh +23 -0
- package/scripts/code_mode_server.py +202 -0
- package/scripts/doctor.mjs +763 -0
- package/scripts/generate-prompts.sh +222 -0
- package/scripts/generate-routing-docs.mjs +104 -0
- package/scripts/hash_verify.py +192 -0
- package/scripts/init-pantheon-mcp.sh +118 -0
- package/scripts/install/agents-md.mjs +214 -0
- package/scripts/install/health-check.mjs +196 -0
- package/scripts/install/migrate.mjs +209 -0
- package/scripts/install/opencode.mjs +645 -0
- package/scripts/install/shared.mjs +655 -0
- package/scripts/install/venv.mjs +116 -0
- package/scripts/install-mcp.mjs +885 -0
- package/scripts/install.mjs +26 -0
- package/scripts/manifest.mjs +622 -0
- package/scripts/mcp_persistence_server.py +459 -0
- package/scripts/mcp_resources_server.py +206 -0
- package/scripts/memory_cache.py +78 -0
- package/scripts/memory_mcp_server.py +605 -0
- package/scripts/paths.py +64 -0
- package/scripts/prune_context.py +72 -0
- package/scripts/release-bundle.mjs +109 -0
- package/scripts/scrub-secrets.py +282 -0
- package/scripts/scrub_secrets.py +281 -0
- package/scripts/test-context-compression.sh +166 -0
- package/scripts/themis_heuristic_scan.py +287 -0
- package/scripts/todo_enforcer.py +242 -0
- package/scripts/uninstall.mjs +1057 -0
- package/scripts/validate-routing.mjs +160 -0
- package/scripts/validate_agent_frontmatter.py +226 -0
- package/scripts/versioning.mjs +254 -0
- package/skills-lock.json +16 -0
- package/src/agents/aphrodite.md +162 -0
- package/src/agents/apollo.md +109 -0
- package/src/agents/athena.md +226 -0
- package/src/agents/demeter.md +146 -0
- package/src/agents/gaia.md +82 -0
- package/src/agents/hephaestus.md +105 -0
- package/src/agents/hermes.md +302 -0
- package/src/agents/iris.md +99 -0
- package/src/agents/mnemosyne.md +226 -0
- package/src/agents/nyx.md +87 -0
- package/src/agents/prometheus.md +199 -0
- package/src/agents/talos.md +93 -0
- package/src/agents/themis.md +187 -0
- package/src/agents/zeus.md +209 -0
- package/src/instructions/agent-return-format.instructions.md +26 -0
- package/src/instructions/backend-standards.instructions.md +45 -0
- package/src/instructions/documentation-standards.instructions.md +53 -0
- package/src/instructions/frontend-standards.instructions.md +46 -0
- package/src/instructions/memory-protocol.instructions.md +67 -0
- package/src/instructions/yagni.instructions.md +21 -0
- package/src/instructions/zeus-anti-stall.instructions.md +72 -0
- package/src/instructions/zeus-communication-rules.instructions.md +15 -0
- package/src/instructions/zeus-council-synthesis.instructions.md +105 -0
- package/src/instructions/zeus-timeout-retry.instructions.md +127 -0
- package/src/mcp/_pantheon_paths.py +67 -0
- package/src/mcp/code_mode_server.py +202 -0
- package/src/mcp/init-pantheon-mcp.sh +118 -0
- package/src/mcp/install-mcp.mjs +885 -0
- package/src/mcp/mcp_persistence_server.py +458 -0
- package/src/mcp/mcp_resources_server.py +205 -0
- package/src/mcp/memory_mcp_server.py +604 -0
- package/src/mcp/requirements-mcp-core.txt +5 -0
- package/src/mcp/requirements-mcp.txt +5 -0
- package/src/plugin.ts +19 -0
- package/src/plugins/tui/dist/tui.js +143 -0
- package/src/plugins/tui/dist/tui.tsx +144 -0
- package/src/plugins/tui/package.json +32 -0
- package/src/plugins/tui/src/index.tsx +144 -0
- package/src/plugins/tui/tsdown.config.ts +22 -0
- package/src/routing.yml +499 -0
- package/src/skills/README.md +230 -0
- package/src/skills/agent-coordination/SKILL.md +95 -0
- package/src/skills/artifact-management/SKILL.md +118 -0
- package/src/skills/auto-continue/SKILL.md +280 -0
- package/src/skills/code-review-checklist/SKILL.md +139 -0
- package/src/skills/context-compression/SKILL.md +861 -0
- package/src/skills/git-workflow-and-versioning/SKILL.md +32 -0
- package/src/skills/incremental-implementation/SKILL.md +27 -0
- package/src/skills/memory-bank/SKILL.md +165 -0
- package/src/skills/orchestration-workflow/SKILL.md +311 -0
- package/src/skills/security-hardening/SKILL.md +36 -0
- package/src/skills/session-goal/SKILL.md +138 -0
- package/src/skills/spec-driven-development/SKILL.md +23 -0
- package/src/skills/tdd-with-agents/SKILL.md +170 -0
- package/src/skills/visual-review-pipeline/SKILL.md +200 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* validate-routing.mjs — Validate routing.yml consistency
|
|
4
|
+
*
|
|
5
|
+
* Checks:
|
|
6
|
+
* - All 18 canonical agents present in routing.yml
|
|
7
|
+
* - All routing.yml agents have canonical files
|
|
8
|
+
* - Skills referenced exist in skills/
|
|
9
|
+
* - Handoffs reference valid agents
|
|
10
|
+
* - Routing matrix references valid agents
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node scripts/validate-routing.mjs
|
|
14
|
+
* node scripts/validate-routing.mjs --verbose # show all entries checked
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
18
|
+
import { dirname, join } from 'node:path'
|
|
19
|
+
import { fileURLToPath } from 'node:url'
|
|
20
|
+
import yaml from 'js-yaml'
|
|
21
|
+
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
23
|
+
const ROOT = join(__dirname, '..')
|
|
24
|
+
|
|
25
|
+
const VERBOSE = process.argv.includes('--verbose')
|
|
26
|
+
|
|
27
|
+
let exitCode = 0
|
|
28
|
+
let checks = 0
|
|
29
|
+
let failures = 0
|
|
30
|
+
|
|
31
|
+
function check(condition, message) {
|
|
32
|
+
checks++
|
|
33
|
+
if (!condition) {
|
|
34
|
+
console.error(` ❌ ${message}`)
|
|
35
|
+
failures++
|
|
36
|
+
exitCode = 1
|
|
37
|
+
} else if (VERBOSE) {
|
|
38
|
+
console.log(` ✅ ${message}`)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function warn(message) {
|
|
43
|
+
console.warn(` ⚠️ ${message}`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Load routing.yml
|
|
47
|
+
const routingPath = join(ROOT, 'routing.yml')
|
|
48
|
+
if (!existsSync(routingPath)) {
|
|
49
|
+
console.error('❌ routing.yml not found')
|
|
50
|
+
process.exit(1)
|
|
51
|
+
}
|
|
52
|
+
const routing = yaml.load(readFileSync(routingPath, 'utf8'))
|
|
53
|
+
|
|
54
|
+
console.log('🔍 Validating routing.yml...\n')
|
|
55
|
+
|
|
56
|
+
// A1. Agents in routing.yml must have canonical files
|
|
57
|
+
const routingAgents = Object.keys(routing.agents || {})
|
|
58
|
+
console.log(` Agents in routing.yml: ${routingAgents.length}`)
|
|
59
|
+
|
|
60
|
+
// Get canonical agent files
|
|
61
|
+
const agentsDir = join(ROOT, 'agents')
|
|
62
|
+
const canonicalFiles = readdirSync(agentsDir)
|
|
63
|
+
.filter((f) => f.endsWith('.agent.md'))
|
|
64
|
+
.map((f) => f.replace('.agent.md', ''))
|
|
65
|
+
.sort()
|
|
66
|
+
|
|
67
|
+
console.log(` Canonical agent files: ${canonicalFiles.length}`)
|
|
68
|
+
|
|
69
|
+
for (const name of routingAgents) {
|
|
70
|
+
if (name === 'zen' || name === 'zeus_copilot') continue // legacy aliases, skip
|
|
71
|
+
check(canonicalFiles.includes(name), `Agent "${name}" in routing.yml has no canonical file`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// A2. Canonical files must be in routing.yml
|
|
75
|
+
for (const name of canonicalFiles) {
|
|
76
|
+
check(routingAgents.includes(name), `Canonical agent "${name}" missing from routing.yml`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// B. Skill validation
|
|
80
|
+
const skillsDir = join(ROOT, 'skills')
|
|
81
|
+
const existingSkills = existsSync(skillsDir)
|
|
82
|
+
? readdirSync(skillsDir).filter((d) => {
|
|
83
|
+
const skillDir = join(skillsDir, d)
|
|
84
|
+
try {
|
|
85
|
+
return existsSync(join(skillDir, 'SKILL.md'))
|
|
86
|
+
} catch {
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
: []
|
|
91
|
+
|
|
92
|
+
console.log(`\n Skills in skills/: ${existingSkills.length}`)
|
|
93
|
+
|
|
94
|
+
for (const [name, info] of Object.entries(routing.agents || {})) {
|
|
95
|
+
const agentSkills = info.skills || []
|
|
96
|
+
for (const skill of agentSkills) {
|
|
97
|
+
if (!existingSkills.includes(skill)) {
|
|
98
|
+
warn(`Agent "${name}" references skill "${skill}" but skills/${skill}/SKILL.md not found`)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// C. Handoff validation
|
|
104
|
+
console.log(
|
|
105
|
+
`\n Handoff definitions: ${Object.keys(routing.handoffs || {}).length} agents with handoffs`,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
for (const [agentName, handoffs] of Object.entries(routing.handoffs || {})) {
|
|
109
|
+
for (const [key, handoff] of Object.entries(handoffs)) {
|
|
110
|
+
// Skip if handoff is just a string (auto-generated)
|
|
111
|
+
if (typeof handoff !== 'object') continue
|
|
112
|
+
const targetAgent = handoff.agent
|
|
113
|
+
check(
|
|
114
|
+
routingAgents.includes(targetAgent),
|
|
115
|
+
`Handoff "${agentName}/${key}" references unknown agent "${targetAgent}"`,
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// D. Routing matrix validation
|
|
121
|
+
console.log(`\n Routing matrix entries: ${(routing.routing_matrix || []).length}`)
|
|
122
|
+
|
|
123
|
+
for (const entry of routing.routing_matrix || []) {
|
|
124
|
+
check(
|
|
125
|
+
routingAgents.includes(entry.primary_agent),
|
|
126
|
+
`Routing matrix "${entry.category}" references unknown primary agent "${entry.primary_agent}"`,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
for (const parallel of entry.parallel_with || []) {
|
|
130
|
+
check(
|
|
131
|
+
routingAgents.includes(parallel),
|
|
132
|
+
`Routing matrix "${entry.category}" references unknown parallel agent "${parallel}"`,
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// E. Subagent delegation validation
|
|
138
|
+
console.log(`\n Subagent delegation rules:`)
|
|
139
|
+
|
|
140
|
+
for (const [name, info] of Object.entries(routing.agents || {})) {
|
|
141
|
+
const delegates = info.subagent_can_delegate_to || []
|
|
142
|
+
for (const target of delegates) {
|
|
143
|
+
check(
|
|
144
|
+
routingAgents.includes(target),
|
|
145
|
+
`Agent "${name}" can delegate to unknown agent "${target}"`,
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Summary
|
|
151
|
+
console.log(`\n${'='.repeat(50)}`)
|
|
152
|
+
const status = failures === 0 ? '✅ PASSED' : `❌ FAILED (${failures}/${checks} checks failed)`
|
|
153
|
+
console.log(` ${status}`)
|
|
154
|
+
console.log(` ${checks} total checks${VERBOSE ? ', see verbose output above' : ''}`)
|
|
155
|
+
if (!VERBOSE && failures > 0) {
|
|
156
|
+
console.log(' Run with --verbose to see all passing checks.')
|
|
157
|
+
}
|
|
158
|
+
console.log('')
|
|
159
|
+
|
|
160
|
+
process.exit(exitCode)
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validate all OpenCode agent .md files for correct frontmatter."""
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
VALID_FIELDS = {
|
|
10
|
+
"description",
|
|
11
|
+
"mode",
|
|
12
|
+
"reasoning_effort",
|
|
13
|
+
"permission",
|
|
14
|
+
"temperature",
|
|
15
|
+
"steps",
|
|
16
|
+
"color",
|
|
17
|
+
"hidden",
|
|
18
|
+
"task_budget",
|
|
19
|
+
"source",
|
|
20
|
+
}
|
|
21
|
+
VALID_MODES = {"primary", "subagent"}
|
|
22
|
+
VALID_PERMISSION_KEYS = {
|
|
23
|
+
"edit",
|
|
24
|
+
"bash",
|
|
25
|
+
"task",
|
|
26
|
+
"skill",
|
|
27
|
+
"read",
|
|
28
|
+
"glob",
|
|
29
|
+
"grep",
|
|
30
|
+
"webfetch",
|
|
31
|
+
"websearch",
|
|
32
|
+
"question",
|
|
33
|
+
"lsp",
|
|
34
|
+
}
|
|
35
|
+
VALID_PERMISSION_VALUES = {"allow", "deny", "ask"}
|
|
36
|
+
|
|
37
|
+
AGENT_DIRS = [
|
|
38
|
+
Path("/home/ils15/pantheon/platform/opencode/agents"),
|
|
39
|
+
Path("/home/ils15/.config/opencode/agents"),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
errors = []
|
|
43
|
+
warnings = []
|
|
44
|
+
valid_count = 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def err(filename, field, msg):
|
|
48
|
+
errors.append(f"❌ [{filename}] {field}: {msg}")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def warn(filename, field, msg):
|
|
52
|
+
warnings.append(f"⚠️ [{filename}] {field}: {msg}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def validate_permission_value(val, path_ctx, filename):
|
|
56
|
+
if isinstance(val, str):
|
|
57
|
+
if val not in VALID_PERMISSION_VALUES:
|
|
58
|
+
err(
|
|
59
|
+
filename,
|
|
60
|
+
f"permission.{path_ctx}",
|
|
61
|
+
f"invalid value '{val}'; must be one of {sorted(VALID_PERMISSION_VALUES)}",
|
|
62
|
+
)
|
|
63
|
+
elif isinstance(val, dict):
|
|
64
|
+
for key, subval in val.items():
|
|
65
|
+
validate_permission_value(subval, f"{path_ctx}.{key}", filename)
|
|
66
|
+
else:
|
|
67
|
+
err(
|
|
68
|
+
filename,
|
|
69
|
+
f"permission.{path_ctx}",
|
|
70
|
+
f"unexpected type '{type(val).__name__}'; expected string or dict",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def validate_file(filepath): # noqa: C901, PLR0912, PLR0915
|
|
75
|
+
global valid_count # noqa: PLW0603
|
|
76
|
+
filename = filepath.name
|
|
77
|
+
text = filepath.read_text(encoding="utf-8")
|
|
78
|
+
|
|
79
|
+
if not text.startswith("---"):
|
|
80
|
+
err(filename, "frontmatter", "file does not start with '---'")
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
parts = text.split("---")
|
|
84
|
+
if len(parts) < 3: # noqa: PLR2004
|
|
85
|
+
err(filename, "frontmatter", "missing closing '---' delimiter")
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
raw_yaml = parts[1]
|
|
89
|
+
try:
|
|
90
|
+
data = yaml.safe_load(raw_yaml)
|
|
91
|
+
except yaml.YAMLError as e:
|
|
92
|
+
err(filename, "frontmatter", f"YAML parse error: {e}")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
if not isinstance(data, dict):
|
|
96
|
+
err(filename, "frontmatter", "frontmatter is not a mapping")
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
# Check for unknown fields
|
|
100
|
+
unknown = set(data.keys()) - VALID_FIELDS
|
|
101
|
+
for field in sorted(unknown):
|
|
102
|
+
err(filename, field, f"unknown field '{field}'; valid: {sorted(VALID_FIELDS)}")
|
|
103
|
+
|
|
104
|
+
# description: must be present
|
|
105
|
+
if "description" not in data:
|
|
106
|
+
err(filename, "description", "description field is required")
|
|
107
|
+
elif not isinstance(data["description"], str) or not data["description"].strip():
|
|
108
|
+
err(filename, "description", "must be a non-empty string")
|
|
109
|
+
|
|
110
|
+
# mode: must be present and valid
|
|
111
|
+
if "mode" not in data:
|
|
112
|
+
err(filename, "mode", "mode field is required")
|
|
113
|
+
elif data["mode"] not in VALID_MODES:
|
|
114
|
+
err(
|
|
115
|
+
filename,
|
|
116
|
+
"mode",
|
|
117
|
+
f"invalid value '{data['mode']}'; must be one of {sorted(VALID_MODES)}",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# permission: validate keys and values
|
|
121
|
+
if "permission" in data:
|
|
122
|
+
perm = data["permission"]
|
|
123
|
+
if isinstance(perm, dict):
|
|
124
|
+
for key in perm:
|
|
125
|
+
if key not in VALID_PERMISSION_KEYS:
|
|
126
|
+
err(
|
|
127
|
+
filename,
|
|
128
|
+
f"permission.{key}",
|
|
129
|
+
f"invalid permission key '{key}'; valid: {sorted(VALID_PERMISSION_KEYS)}",
|
|
130
|
+
)
|
|
131
|
+
validate_permission_value(perm[key], key, filename)
|
|
132
|
+
else:
|
|
133
|
+
err(
|
|
134
|
+
filename,
|
|
135
|
+
"permission",
|
|
136
|
+
f"unexpected type '{type(perm).__name__}'; expected mapping",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# temperature: between 0.0 and 2.0
|
|
140
|
+
if "temperature" in data:
|
|
141
|
+
t = data["temperature"]
|
|
142
|
+
if not isinstance(t, (int, float)):
|
|
143
|
+
err(filename, "temperature", f"expected a number, got '{type(t).__name__}'")
|
|
144
|
+
elif t < 0.0 or t > 2.0: # noqa: PLR2004
|
|
145
|
+
err(filename, "temperature", f"value {t} is out of range [0.0, 2.0]")
|
|
146
|
+
|
|
147
|
+
# steps: must be a positive integer
|
|
148
|
+
if "steps" in data:
|
|
149
|
+
s = data["steps"]
|
|
150
|
+
if not isinstance(s, int) or isinstance(s, bool):
|
|
151
|
+
err(
|
|
152
|
+
filename,
|
|
153
|
+
"steps",
|
|
154
|
+
f"expected a positive integer, got '{type(s).__name__}'",
|
|
155
|
+
)
|
|
156
|
+
elif s < 1:
|
|
157
|
+
err(filename, "steps", f"value {s} is not a positive integer")
|
|
158
|
+
|
|
159
|
+
# Body content checks
|
|
160
|
+
body = "---".join(parts[2:])
|
|
161
|
+
|
|
162
|
+
bash_value = None
|
|
163
|
+
if isinstance(data.get("permission"), dict):
|
|
164
|
+
bash_value = data["permission"].get("bash")
|
|
165
|
+
|
|
166
|
+
if bash_value == "deny":
|
|
167
|
+
if "⛔ TOOLS NOT AVAILABLE" not in body:
|
|
168
|
+
err(
|
|
169
|
+
filename,
|
|
170
|
+
"body",
|
|
171
|
+
"permission.bash is 'deny' but body is missing '## ⛔ TOOLS NOT AVAILABLE' section",
|
|
172
|
+
)
|
|
173
|
+
elif "bash" not in body.split("⛔ TOOLS NOT AVAILABLE")[1].split("\n")[0:20]:
|
|
174
|
+
warn(
|
|
175
|
+
filename,
|
|
176
|
+
"body",
|
|
177
|
+
"permission.bash is 'deny' but 'bash' may not be listed as unavailable in the section",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
edit_value = None
|
|
181
|
+
if isinstance(data.get("permission"), dict):
|
|
182
|
+
edit_value = data["permission"].get("edit")
|
|
183
|
+
|
|
184
|
+
if edit_value == "deny":
|
|
185
|
+
edit_mentions = body.lower().count("edit")
|
|
186
|
+
if edit_mentions > 0:
|
|
187
|
+
warn(
|
|
188
|
+
filename,
|
|
189
|
+
"body",
|
|
190
|
+
f"permission.edit is 'deny' but 'edit' appears {edit_mentions} time(s) in body",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
valid_count += 1
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def main():
|
|
197
|
+
all_files = []
|
|
198
|
+
for d in AGENT_DIRS:
|
|
199
|
+
if not d.exists():
|
|
200
|
+
print(f"⚠️ Config error: directory not found: {d}", file=sys.stderr)
|
|
201
|
+
sys.exit(2)
|
|
202
|
+
all_files.extend(sorted(d.glob("*.md")))
|
|
203
|
+
|
|
204
|
+
if not all_files:
|
|
205
|
+
print("⚠️ Config error: no agent .md files found", file=sys.stderr)
|
|
206
|
+
sys.exit(2)
|
|
207
|
+
|
|
208
|
+
for f in all_files:
|
|
209
|
+
validate_file(f)
|
|
210
|
+
|
|
211
|
+
for msg in errors:
|
|
212
|
+
print(msg, file=sys.stderr)
|
|
213
|
+
for msg in warnings:
|
|
214
|
+
print(msg, file=sys.stderr)
|
|
215
|
+
|
|
216
|
+
print(
|
|
217
|
+
f"✅ {valid_count} files valid, {len(warnings)} warnings, {len(errors)} errors"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if errors:
|
|
221
|
+
sys.exit(1)
|
|
222
|
+
sys.exit(0)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__":
|
|
226
|
+
main()
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* versioning.mjs — Pantheon release versioning helper
|
|
4
|
+
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* recommend Analyze commits and suggest next version bump type
|
|
7
|
+
* apply [type] Bump manifests + move [Unreleased] → [vX.Y.Z] in CHANGELOG
|
|
8
|
+
* type: patch | minor | major | auto (default: auto)
|
|
9
|
+
* changelog [ver] (Internal) Insert a versioned section into CHANGELOG
|
|
10
|
+
* Normally called by `apply`; can be run standalone.
|
|
11
|
+
* status Show current version, latest tag, and pending bump type
|
|
12
|
+
*
|
|
13
|
+
* Design: the release signal is "package.json version > latest git tag".
|
|
14
|
+
* Developers (or AI agents) call `apply` to bump + update CHANGELOG, then push.
|
|
15
|
+
* The auto-release workflow detects the version bump and creates the release.
|
|
16
|
+
* No version bumping ever happens inside GitHub Actions.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { execSync } from 'node:child_process'
|
|
20
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
21
|
+
import { dirname, join } from 'node:path'
|
|
22
|
+
import { fileURLToPath } from 'node:url'
|
|
23
|
+
|
|
24
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
25
|
+
const ROOT = join(__dirname, '..')
|
|
26
|
+
|
|
27
|
+
const MANIFEST_FILES = [
|
|
28
|
+
'platform/forge.json',
|
|
29
|
+
'pyproject.toml',
|
|
30
|
+
'package.json',
|
|
31
|
+
'plugin.json',
|
|
32
|
+
'.github/plugin/plugin.json',
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
const CHANGELOG_PATH = join(ROOT, 'CHANGELOG.md')
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Git helpers
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
function run(cmd) {
|
|
42
|
+
try {
|
|
43
|
+
return execSync(cmd, { cwd: ROOT, encoding: 'utf-8' }).trim()
|
|
44
|
+
} catch {
|
|
45
|
+
return ''
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getLatestTag() {
|
|
50
|
+
// Use numerically highest tag, not just nearest git ancestor
|
|
51
|
+
const tag = run("git tag -l 'v[0-9]*.[0-9]*.[0-9]*' | sort -V | tail -1")
|
|
52
|
+
return tag || 'v0.0.0'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getCurrentVersion() {
|
|
56
|
+
return JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8')).version
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Semver helpers
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
function bumpVersion(version, type) {
|
|
64
|
+
const [major, minor, patch] = version.split('.').map(Number)
|
|
65
|
+
switch (type) {
|
|
66
|
+
case 'major':
|
|
67
|
+
return `${major + 1}.0.0`
|
|
68
|
+
case 'minor':
|
|
69
|
+
return `${major}.${minor + 1}.0`
|
|
70
|
+
default:
|
|
71
|
+
return `${major}.${minor}.${patch + 1}`
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function analyzeConventionalCommits(since) {
|
|
76
|
+
const log = run(`git log ${since}..HEAD --format="%s"`)
|
|
77
|
+
if (!log) return 'patch'
|
|
78
|
+
let bump = 'patch'
|
|
79
|
+
for (const msg of log.split('\n').filter(Boolean)) {
|
|
80
|
+
if (/BREAKING CHANGE/i.test(msg) || /^[a-z]+!/i.test(msg)) return 'major'
|
|
81
|
+
if (/^feat/i.test(msg)) bump = 'minor'
|
|
82
|
+
}
|
|
83
|
+
return bump
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Manifest updater
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
function updateManifests(newVersion) {
|
|
91
|
+
for (const file of MANIFEST_FILES) {
|
|
92
|
+
const path = join(ROOT, file)
|
|
93
|
+
try {
|
|
94
|
+
const raw = readFileSync(path, 'utf-8')
|
|
95
|
+
if (file.endsWith('.toml')) {
|
|
96
|
+
// TOML: replace version = "X.Y.Z"
|
|
97
|
+
const updated = raw.replace(/^(version\s*=\s*")[^"]+(")/m, `$1${newVersion}$2`)
|
|
98
|
+
writeFileSync(path, updated)
|
|
99
|
+
console.log(` ✓ ${file} → ${newVersion}`)
|
|
100
|
+
} else {
|
|
101
|
+
// JSON
|
|
102
|
+
const content = JSON.parse(raw)
|
|
103
|
+
content.version = newVersion
|
|
104
|
+
writeFileSync(path, `${JSON.stringify(content, null, 2)}\n`)
|
|
105
|
+
console.log(` ✓ ${file} → ${newVersion}`)
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
console.log(` ⚠ ${file} not found — skipped`)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// CHANGELOG updater
|
|
115
|
+
//
|
|
116
|
+
// Finds the [Unreleased] section and:
|
|
117
|
+
// 1. Strips empty subsections (### Added\n\n### Changed...)
|
|
118
|
+
// 2. Renames [Unreleased] → [vX.Y.Z] - date
|
|
119
|
+
// 3. Inserts a fresh empty [Unreleased] template above it
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
function promoteUnreleased(newVersion, dateStr) {
|
|
123
|
+
const content = readFileSync(CHANGELOG_PATH, 'utf-8')
|
|
124
|
+
|
|
125
|
+
const unreleasedHeader = '## [Unreleased]'
|
|
126
|
+
const idx = content.indexOf(unreleasedHeader)
|
|
127
|
+
if (idx === -1) {
|
|
128
|
+
console.log(' ⚠ [Unreleased] section not found in CHANGELOG — skipping')
|
|
129
|
+
return false
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Find the end of [Unreleased]: next ## header or end of file
|
|
133
|
+
const afterHeader = idx + unreleasedHeader.length
|
|
134
|
+
const nextSectionIdx = content.indexOf('\n## [', afterHeader)
|
|
135
|
+
const unreleasedBody =
|
|
136
|
+
nextSectionIdx === -1 ? content.slice(afterHeader) : content.slice(afterHeader, nextSectionIdx)
|
|
137
|
+
|
|
138
|
+
// Check if the [Unreleased] section has any real content (non-empty lines
|
|
139
|
+
// that aren't just section headers or comments)
|
|
140
|
+
const realLines = unreleasedBody
|
|
141
|
+
.split('\n')
|
|
142
|
+
.filter((l) => l.trim() && !l.startsWith('###') && !l.startsWith('<!--'))
|
|
143
|
+
|
|
144
|
+
if (realLines.length === 0) {
|
|
145
|
+
console.log(' ℹ [Unreleased] is empty — CHANGELOG not modified')
|
|
146
|
+
return false
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Strip lines that are just empty subsections (### X followed by blank line
|
|
150
|
+
// then another ### or end)
|
|
151
|
+
const cleanedBody = unreleasedBody
|
|
152
|
+
.replace(/\n### \w[^\n]*\n(\n(?=###|\n## |\n$))+/g, '\n')
|
|
153
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
154
|
+
.trimEnd()
|
|
155
|
+
|
|
156
|
+
const newTemplate = `\n\n<!-- Add new changes here. Running \`node scripts/versioning.mjs apply\` will\n move this section to a versioned entry and reset the template below. -->\n\n### Added\n\n### Changed\n\n### Fixed\n\n### Removed`
|
|
157
|
+
const newVersionHeader = `## [v${newVersion}] - ${dateStr}`
|
|
158
|
+
|
|
159
|
+
const before = content.slice(0, idx)
|
|
160
|
+
const after = nextSectionIdx === -1 ? '' : content.slice(nextSectionIdx)
|
|
161
|
+
|
|
162
|
+
const updated = `${before + unreleasedHeader + newTemplate}\n\n${newVersionHeader}${cleanedBody}${after}`
|
|
163
|
+
|
|
164
|
+
writeFileSync(CHANGELOG_PATH, updated)
|
|
165
|
+
console.log(` ✓ CHANGELOG: [Unreleased] → [v${newVersion}]`)
|
|
166
|
+
return true
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Commands
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
const command = process.argv[2]
|
|
174
|
+
const arg = process.argv[3]
|
|
175
|
+
|
|
176
|
+
switch (command) {
|
|
177
|
+
case 'status': {
|
|
178
|
+
const latestTag = getLatestTag()
|
|
179
|
+
const current = getCurrentVersion()
|
|
180
|
+
const latestVer = latestTag.replace(/^v/, '')
|
|
181
|
+
const bump = analyzeConventionalCommits(latestTag)
|
|
182
|
+
const next = bumpVersion(current, bump)
|
|
183
|
+
const needsRelease = current !== latestVer
|
|
184
|
+
|
|
185
|
+
console.log(`Current version : ${current}`)
|
|
186
|
+
console.log(`Latest git tag : ${latestTag}`)
|
|
187
|
+
console.log(
|
|
188
|
+
`Release pending : ${needsRelease ? `YES — tag ${latestTag} < pkg ${current}` : 'NO — already tagged'}`,
|
|
189
|
+
)
|
|
190
|
+
console.log(`Recommended bump : ${bump}`)
|
|
191
|
+
console.log(`Next version : ${next}`)
|
|
192
|
+
break
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
case 'recommend': {
|
|
196
|
+
const latestTag = getLatestTag()
|
|
197
|
+
const bump = analyzeConventionalCommits(latestTag)
|
|
198
|
+
const current = getCurrentVersion()
|
|
199
|
+
console.log(bumpVersion(current, bump))
|
|
200
|
+
break
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
case 'apply': {
|
|
204
|
+
const type = arg || 'auto'
|
|
205
|
+
const latestTag = getLatestTag()
|
|
206
|
+
const current = getCurrentVersion()
|
|
207
|
+
const latestVer = latestTag.replace(/^v/, '')
|
|
208
|
+
|
|
209
|
+
// If package.json is already ahead of the latest tag, someone bumped
|
|
210
|
+
// without tagging — warn and use the current version as-is.
|
|
211
|
+
if (current !== latestVer) {
|
|
212
|
+
console.log(`⚠ package.json (${current}) already ahead of latest tag (${latestTag}).`)
|
|
213
|
+
console.log(` Syncing all manifests to ${current} and promoting CHANGELOG.`)
|
|
214
|
+
updateManifests(current)
|
|
215
|
+
const date = new Date().toISOString().slice(0, 10)
|
|
216
|
+
promoteUnreleased(current, date)
|
|
217
|
+
break
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const bumpType = type === 'auto' ? analyzeConventionalCommits(latestTag) : type
|
|
221
|
+
const newVersion = bumpVersion(current, bumpType)
|
|
222
|
+
const date = new Date().toISOString().slice(0, 10)
|
|
223
|
+
|
|
224
|
+
console.log(`Bumping ${current} → ${newVersion} (${bumpType})`)
|
|
225
|
+
updateManifests(newVersion)
|
|
226
|
+
promoteUnreleased(newVersion, date)
|
|
227
|
+
console.log(`\nDone. Commit with: git add -A && git commit -m "chore(release): v${newVersion}"`)
|
|
228
|
+
break
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Legacy standalone command — kept for backward compat
|
|
232
|
+
case 'changelog': {
|
|
233
|
+
const version = arg || getCurrentVersion()
|
|
234
|
+
const date = new Date().toISOString().slice(0, 10)
|
|
235
|
+
promoteUnreleased(version, date)
|
|
236
|
+
break
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
default:
|
|
240
|
+
console.log(`Usage: node scripts/versioning.mjs <command>
|
|
241
|
+
|
|
242
|
+
Commands:
|
|
243
|
+
status Show current version, latest tag, release status
|
|
244
|
+
recommend Print recommended bump type (patch/minor/major)
|
|
245
|
+
apply [type] Bump manifests + move [Unreleased] → [vX.Y.Z]
|
|
246
|
+
type: patch | minor | major | auto (default: auto)
|
|
247
|
+
changelog [version] Promote [Unreleased] → [vX.Y.Z] without bumping
|
|
248
|
+
|
|
249
|
+
Release flow:
|
|
250
|
+
1. node scripts/versioning.mjs apply [minor]
|
|
251
|
+
2. git add -A && git commit -m "chore(release): vX.Y.Z"
|
|
252
|
+
3. git push ← CI passes → auto-release fires because pkg > tag
|
|
253
|
+
`)
|
|
254
|
+
}
|
package/skills-lock.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"src/skills/agent-coordination/SKILL.md": "cfd040ef29e3dd0fd5e0e67773874facf7c06e3284898afe1fddba4456c3f1aa",
|
|
3
|
+
"src/skills/artifact-management/SKILL.md": "c60e493666bedf46e9113d0dd20106489d5c5a73905e1370fcfc1c0d71c59185",
|
|
4
|
+
"src/skills/auto-continue/SKILL.md": "791b7f8d981bca571a6350cfd0f7f623ab8d9c2190b015dc63c15f8663b25e53",
|
|
5
|
+
"src/skills/code-review-checklist/SKILL.md": "0e59367c65ef4608db80a0674279f32cfc3f67dca7236f233b61a47fe506579f",
|
|
6
|
+
"src/skills/context-compression/SKILL.md": "07cfddfedeb8087794ae49ef8e60d8adca3ec2342d592cc30d0407d1b8ab1b90",
|
|
7
|
+
"src/skills/git-workflow-and-versioning/SKILL.md": "685a94041f1b4ce2a430301f517b89102055696267c369a7e8fb191606659276",
|
|
8
|
+
"src/skills/incremental-implementation/SKILL.md": "7065ddb45da3c6b6751de07f290e92e6cb072e949a2e3aaae13ee7762f0a70fc",
|
|
9
|
+
"src/skills/memory-bank/SKILL.md": "4717cf18b43550c9f02cfab810968b97a5b2584f00a15516ecba643c80cdc110",
|
|
10
|
+
"src/skills/orchestration-workflow/SKILL.md": "0eeabaf71a802d1f673f79996548cda2d394bbcc7c62eaca03482d3fa99be688",
|
|
11
|
+
"src/skills/security-hardening/SKILL.md": "6d637e122421f7bc3b751f04854ad343a5a5d35249458bc8939233138fd3f20d",
|
|
12
|
+
"src/skills/session-goal/SKILL.md": "793498265f70253c20524f6adc88bcc86db11d7160fe8232a26aedc6b1c5489f",
|
|
13
|
+
"src/skills/spec-driven-development/SKILL.md": "e93b9103d6cd1848ab01328efde82c1daf564d1cabbdc2278254b346fcf55d02",
|
|
14
|
+
"src/skills/tdd-with-agents/SKILL.md": "9880919a46b74f549f948147d8b086b008635de94fa259f76261d067772bb8e3",
|
|
15
|
+
"src/skills/visual-review-pipeline/SKILL.md": "f6e65ff75a9325c38e0e9dde8e87f09b873e14e490936ef48aa80201bb68fe10"
|
|
16
|
+
}
|