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.
Files changed (128) hide show
  1. package/AGENTS.md +37 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1013 -0
  4. package/bin/pantheon-init.mjs +183 -0
  5. package/commands/pantheon-audit.md +25 -0
  6. package/commands/pantheon-bg.md +10 -0
  7. package/commands/pantheon-consolidate.md +11 -0
  8. package/commands/pantheon-deepwork.md +128 -0
  9. package/commands/pantheon-doc.md +10 -0
  10. package/commands/pantheon-focus.md +9 -0
  11. package/commands/pantheon-forget.md +58 -0
  12. package/commands/pantheon-hash.md +11 -0
  13. package/commands/pantheon-optimize.md +79 -0
  14. package/commands/pantheon-remember.md +44 -0
  15. package/commands/pantheon-search.md +48 -0
  16. package/commands/pantheon-status.md +71 -0
  17. package/commands/pantheon-todo.md +11 -0
  18. package/commands/pantheon.md +49 -0
  19. package/docs/AGENT-MCP.md +194 -0
  20. package/docs/ARCHITECTURE.md +384 -0
  21. package/docs/BRANCH-PROTECTION.md +142 -0
  22. package/docs/INDEX.md +81 -0
  23. package/docs/INSTALLATION.md +217 -0
  24. package/docs/MCP.md +238 -0
  25. package/docs/MEMORY.md +471 -0
  26. package/docs/MIGRATION-MEMORY-BANK.md +139 -0
  27. package/docs/PLATFORMS.md +5 -0
  28. package/docs/QUICKSTART.md +49 -0
  29. package/docs/README.md +18 -0
  30. package/docs/RELEASING.md +256 -0
  31. package/docs/SETUP.md +5 -0
  32. package/docs/UPGRADING.md +41 -0
  33. package/docs/mcp-recommendations.md +439 -0
  34. package/docs/mcp-tools.md +156 -0
  35. package/docs/mcp-user-guide.md +204 -0
  36. package/docs/persistence-mcp.md +111 -0
  37. package/package.json +72 -0
  38. package/pantheon.schema.json +158 -0
  39. package/scripts/__init__.py +0 -0
  40. package/scripts/_pantheon_paths.py +68 -0
  41. package/scripts/check-version-consistency.sh +23 -0
  42. package/scripts/code_mode_server.py +202 -0
  43. package/scripts/doctor.mjs +763 -0
  44. package/scripts/generate-prompts.sh +222 -0
  45. package/scripts/generate-routing-docs.mjs +104 -0
  46. package/scripts/hash_verify.py +192 -0
  47. package/scripts/init-pantheon-mcp.sh +118 -0
  48. package/scripts/install/agents-md.mjs +214 -0
  49. package/scripts/install/health-check.mjs +196 -0
  50. package/scripts/install/migrate.mjs +209 -0
  51. package/scripts/install/opencode.mjs +645 -0
  52. package/scripts/install/shared.mjs +655 -0
  53. package/scripts/install/venv.mjs +116 -0
  54. package/scripts/install-mcp.mjs +885 -0
  55. package/scripts/install.mjs +26 -0
  56. package/scripts/manifest.mjs +622 -0
  57. package/scripts/mcp_persistence_server.py +459 -0
  58. package/scripts/mcp_resources_server.py +206 -0
  59. package/scripts/memory_cache.py +78 -0
  60. package/scripts/memory_mcp_server.py +605 -0
  61. package/scripts/paths.py +64 -0
  62. package/scripts/prune_context.py +72 -0
  63. package/scripts/release-bundle.mjs +109 -0
  64. package/scripts/scrub-secrets.py +282 -0
  65. package/scripts/scrub_secrets.py +281 -0
  66. package/scripts/test-context-compression.sh +166 -0
  67. package/scripts/themis_heuristic_scan.py +287 -0
  68. package/scripts/todo_enforcer.py +242 -0
  69. package/scripts/uninstall.mjs +1057 -0
  70. package/scripts/validate-routing.mjs +160 -0
  71. package/scripts/validate_agent_frontmatter.py +226 -0
  72. package/scripts/versioning.mjs +254 -0
  73. package/skills-lock.json +16 -0
  74. package/src/agents/aphrodite.md +162 -0
  75. package/src/agents/apollo.md +109 -0
  76. package/src/agents/athena.md +226 -0
  77. package/src/agents/demeter.md +146 -0
  78. package/src/agents/gaia.md +82 -0
  79. package/src/agents/hephaestus.md +105 -0
  80. package/src/agents/hermes.md +302 -0
  81. package/src/agents/iris.md +99 -0
  82. package/src/agents/mnemosyne.md +226 -0
  83. package/src/agents/nyx.md +87 -0
  84. package/src/agents/prometheus.md +199 -0
  85. package/src/agents/talos.md +93 -0
  86. package/src/agents/themis.md +187 -0
  87. package/src/agents/zeus.md +209 -0
  88. package/src/instructions/agent-return-format.instructions.md +26 -0
  89. package/src/instructions/backend-standards.instructions.md +45 -0
  90. package/src/instructions/documentation-standards.instructions.md +53 -0
  91. package/src/instructions/frontend-standards.instructions.md +46 -0
  92. package/src/instructions/memory-protocol.instructions.md +67 -0
  93. package/src/instructions/yagni.instructions.md +21 -0
  94. package/src/instructions/zeus-anti-stall.instructions.md +72 -0
  95. package/src/instructions/zeus-communication-rules.instructions.md +15 -0
  96. package/src/instructions/zeus-council-synthesis.instructions.md +105 -0
  97. package/src/instructions/zeus-timeout-retry.instructions.md +127 -0
  98. package/src/mcp/_pantheon_paths.py +67 -0
  99. package/src/mcp/code_mode_server.py +202 -0
  100. package/src/mcp/init-pantheon-mcp.sh +118 -0
  101. package/src/mcp/install-mcp.mjs +885 -0
  102. package/src/mcp/mcp_persistence_server.py +458 -0
  103. package/src/mcp/mcp_resources_server.py +205 -0
  104. package/src/mcp/memory_mcp_server.py +604 -0
  105. package/src/mcp/requirements-mcp-core.txt +5 -0
  106. package/src/mcp/requirements-mcp.txt +5 -0
  107. package/src/plugin.ts +19 -0
  108. package/src/plugins/tui/dist/tui.js +143 -0
  109. package/src/plugins/tui/dist/tui.tsx +144 -0
  110. package/src/plugins/tui/package.json +32 -0
  111. package/src/plugins/tui/src/index.tsx +144 -0
  112. package/src/plugins/tui/tsdown.config.ts +22 -0
  113. package/src/routing.yml +499 -0
  114. package/src/skills/README.md +230 -0
  115. package/src/skills/agent-coordination/SKILL.md +95 -0
  116. package/src/skills/artifact-management/SKILL.md +118 -0
  117. package/src/skills/auto-continue/SKILL.md +280 -0
  118. package/src/skills/code-review-checklist/SKILL.md +139 -0
  119. package/src/skills/context-compression/SKILL.md +861 -0
  120. package/src/skills/git-workflow-and-versioning/SKILL.md +32 -0
  121. package/src/skills/incremental-implementation/SKILL.md +27 -0
  122. package/src/skills/memory-bank/SKILL.md +165 -0
  123. package/src/skills/orchestration-workflow/SKILL.md +311 -0
  124. package/src/skills/security-hardening/SKILL.md +36 -0
  125. package/src/skills/session-goal/SKILL.md +138 -0
  126. package/src/skills/spec-driven-development/SKILL.md +23 -0
  127. package/src/skills/tdd-with-agents/SKILL.md +170 -0
  128. 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
+ }
@@ -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
+ }