pantheon-opencode 1.5.2 → 1.6.0-beta.10
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/.pantheon/code-mode/README.md +4 -2
- package/.pantheon/code-mode/checkpoint_session.py +138 -102
- package/.pantheon/code-mode/compress-inline.py +102 -57
- package/.pantheon/code-mode/manifest.json +2 -8
- package/AGENTS.global.md +182 -0
- package/AGENTS.md +10 -891
- package/CHANGELOG.md +343 -0
- package/README.md +577 -79
- package/README.pt-BR.md +345 -60
- package/bin/pantheon-init.mjs +26 -3
- package/commands/pantheon-audit.md +7 -1
- package/commands/pantheon-consolidate.md +1 -1
- package/commands/pantheon-deepwork.md +14 -3
- package/commands/pantheon-focus.md +17 -0
- package/commands/pantheon-optimize.md +2 -2
- package/commands/pantheon-vision.md +61 -12
- package/package.json +30 -11
- package/plugin.json +2 -2
- package/pyproject.toml +11 -16
- package/scripts/build-agents-md.mjs +376 -58
- package/scripts/changelog-merge.mjs +161 -0
- package/scripts/check-plugin-v2-coverage.mjs +213 -0
- package/scripts/ci-validate-yaml.py +7 -5
- package/scripts/{code_mode_server.py → code_mode.py} +119 -27
- package/scripts/cost.mjs +244 -41
- package/scripts/doctor.mjs +236 -29
- package/scripts/install/agents-md.mjs +4 -4
- package/scripts/install/cli-ui.mjs +1 -1
- package/scripts/install/config-migration.mjs +319 -6
- package/scripts/install/health-check.mjs +26 -37
- package/scripts/install/interactive.mjs +7 -7
- package/scripts/install/migrate.mjs +1 -1
- package/scripts/install/opencode-version.mjs +246 -4
- package/scripts/install/opencode.mjs +627 -179
- package/scripts/install/plugin.mjs +43 -9
- package/scripts/install/shared.mjs +120 -12
- package/scripts/install/vision-prompt.mjs +161 -0
- package/scripts/install-mcp.mjs +28 -9
- package/scripts/{mcp_persistence_server.py → mcp_persistence.py} +392 -78
- package/{src/mcp/mcp_resources_server.py → scripts/mcp_resources.py} +2 -2
- package/scripts/{memory_mcp_server.py → memory_mcp.py} +372 -198
- package/scripts/postinstall.mjs +0 -7
- package/scripts/probe-context-rehydrate.mjs +36 -13
- package/scripts/probe-pantheon-cost.mjs +71 -15
- package/scripts/prune.mjs +2 -2
- package/scripts/run-node-tests.mjs +189 -0
- package/scripts/sync-artifacts.mjs +26 -13
- package/scripts/sync-tui.mjs +24 -3
- package/scripts/test-opencode-v2-sandbox.sh +1913 -0
- package/scripts/validate-routing.mjs +43 -3
- package/scripts/validate-sandbox-install.mjs +138 -0
- package/scripts/versioning.mjs +13 -1
- package/scripts/vision-status.mjs +682 -0
- package/skills-lock.json +15 -16
- package/src/agents/aphrodite.md +20 -37
- package/src/agents/apollo.md +7 -3
- package/src/agents/athena.md +63 -35
- package/src/agents/demeter.md +8 -28
- package/src/agents/gaia.md +1 -1
- package/src/agents/hephaestus.md +10 -21
- package/src/agents/hermes.md +15 -38
- package/src/agents/iris.md +1 -1
- package/src/agents/mnemosyne.md +23 -60
- package/src/agents/nyx.md +1 -1
- package/src/agents/prometheus.md +16 -20
- package/src/agents/talos.md +10 -2
- package/src/agents/themis.md +5 -5
- package/src/agents/zeus.md +99 -130
- package/src/instructions/agent-return-format.instructions.md +18 -3
- package/src/instructions/backend-standards.instructions.md +2 -0
- package/src/instructions/frontend-standards.instructions.md +3 -1
- package/src/instructions/memory-protocol.instructions.md +11 -110
- package/src/instructions/zeus-anti-stall.instructions.md +14 -61
- package/src/instructions/zeus-communication-rules.instructions.md +0 -1
- package/src/instructions/zeus-council-synthesis.instructions.md +32 -260
- package/src/instructions/zeus-memory-operations.instructions.md +26 -0
- package/src/instructions/zeus-timeout-retry.instructions.md +40 -87
- package/src/mcp/{code_mode_server.py → code_mode.py} +119 -27
- package/src/mcp/eval_store.py +2 -2
- package/src/mcp/mcp_codemap_module.py +559 -129
- package/src/mcp/{mcp_persistence_server.py → mcp_persistence.py} +392 -78
- package/{scripts/mcp_resources_server.py → src/mcp/mcp_resources.py} +2 -2
- package/src/mcp/{memory_mcp_server.py → memory_mcp.py} +373 -250
- package/src/mcp/{pantheon_vision_server.py → pantheon_vision.py} +396 -40
- package/src/mcp/requirements-mcp.txt +10 -6
- package/src/mcp/requirements-vision.txt +4 -2
- package/src/pantheon/cost-command.ts +399 -101
- package/src/pantheon/delegation-enforce.ts +10 -1
- package/src/pantheon/model-command.ts +18 -7
- package/src/pantheon/presets.d.mts +0 -8
- package/src/pantheon/presets.mjs +5 -32
- package/src/pantheon/todo-enforcer.ts +1 -1
- package/src/pantheon/v2-bounded-read.ts +640 -0
- package/src/pantheon/v2-delegation-enforce.ts +214 -0
- package/src/pantheon/v2-execute-after.ts +186 -0
- package/src/pantheon/v2-tools.ts +429 -0
- package/src/pantheon/v2-unsupported.d.mts +11 -0
- package/src/pantheon/v2-unsupported.mjs +90 -0
- package/src/pantheon/v2-vision.ts +622 -0
- package/src/pantheon/vision-local.ts +531 -0
- package/src/pantheon/vision.ts +1161 -116
- package/src/plugin-v2.ts +1244 -288
- package/src/plugin.ts +12 -9
- package/src/plugins/tui/dist/server.d.ts +1 -2
- package/src/plugins/tui/dist/server.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui-v2.d.ts +154 -0
- package/src/plugins/tui/dist/tui-v2.d.ts.map +1 -0
- package/src/plugins/tui/dist/tui-v2.js +659 -0
- package/src/plugins/tui/dist/tui-v2.js.map +1 -0
- package/src/plugins/tui/dist/tui.d.ts +85 -82
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +72 -59
- package/src/plugins/tui/index-v2.ts +6 -0
- package/src/plugins/tui/package-lock.json +5504 -1341
- package/src/plugins/tui/package.json +25 -15
- package/src/plugins/tui/src/index-v2.tsx +734 -0
- package/src/plugins/tui/src/index.tsx +106 -85
- package/src/plugins/tui/src/opencode-v2.d.ts +13 -0
- package/src/plugins/tui/tsconfig.build.json +5 -0
- package/src/plugins/tui/tsconfig.typecheck.json +5 -0
- package/src/plugins/tui/tsdown.config.ts +22 -2
- package/src/plugins/tui/tui-v2.ts +2 -0
- package/src/plugins/tui/tui.ts +3 -0
- package/src/routing.yml +11 -22
- package/src/skills/README.md +34 -39
- package/src/skills/agent-coordination/SKILL.md +7 -2
- package/src/skills/artifact-management/SKILL.md +12 -5
- package/src/skills/auto-continue/SKILL.md +54 -95
- package/src/skills/code-review-checklist/SKILL.md +5 -5
- package/src/skills/context-compression/SKILL.md +26 -879
- package/src/skills/session-goal/SKILL.md +14 -5
- package/src/skills/spec-driven-development/SKILL.md +3 -1
- package/src/skills/tdd-with-agents/SKILL.md +18 -10
- package/.pantheon/code-mode/eval-llm-judge.py +0 -255
- package/.pantheon/code-mode/eval-monte-carlo.py +0 -368
- package/.pantheon/code-mode/eval-run.py +0 -197
- package/.pantheon/code-mode/eval-static.py +0 -321
- package/.pantheon/code-mode/session-end-save.py +0 -186
- package/.pantheon/code-mode/session-end-save.sh +0 -18
- package/opencode.json +0 -58
- package/scripts/recover-zenodo-deposition.mjs +0 -146
- package/scripts/test-opencode-v1-v2-sandbox.sh +0 -862
- package/scripts/validate-zenodo-release.mjs +0 -154
- package/scripts/zenodo-artifact.mjs +0 -94
- package/scripts/zenodo-release-state.mjs +0 -53
- package/src/skills/orchestration-workflow/SKILL.md +0 -311
|
@@ -1,42 +1,109 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* build-agents-md.mjs — Generate
|
|
3
|
+
* build-agents-md.mjs — Generate BOTH AGENTS.md artifacts from canonical sources.
|
|
4
4
|
*
|
|
5
|
-
* AGENTS.md
|
|
6
|
-
*
|
|
7
|
-
* key is accepted-but-ignored by V2).
|
|
8
|
-
*
|
|
5
|
+
* AGENTS.md files are instruction files BOTH OpenCode V1 and V2 load (V1
|
|
6
|
+
* auto-discovers them, V2 loads them explicitly; the `instructions` config
|
|
7
|
+
* key is accepted-but-ignored by V2). Every session pays for global AND
|
|
8
|
+
* project AGENTS.md content, so a single duplicated file is paid twice per
|
|
9
|
+
* session. To keep the payload disjoint (beta-10 latency slice, phase 1),
|
|
10
|
+
* this script emits TWO artifacts in ONE pass over the same sources:
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
12
|
+
* AGENTS.md — repo-specific setup only (build commands, sandbox
|
|
13
|
+
* rules, conventions). Loaded by sessions in THIS repo.
|
|
14
|
+
* AGENTS.global.md — repo-agnostic content only (agent table + shared
|
|
15
|
+
* instruction bodies embedded under a `## <name>`
|
|
16
|
+
* section marker). The installer/sync emits this into
|
|
17
|
+
* the user's global config dir as AGENTS.md.
|
|
13
18
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
19
|
+
* Zero overlap: each rule lives in exactly one artifact. Agent-scoped
|
|
20
|
+
* instruction bodies are still rendered into only their matching agent
|
|
21
|
+
* prompt. The instruction files remain the source of truth; both artifacts
|
|
22
|
+
* are committed build artifacts, never hand-edited. Each carries a version
|
|
23
|
+
* header (generated-at + short git sha) so a stale artifact is detectable;
|
|
24
|
+
* `--check` ignores that volatile line and byte-compares everything else.
|
|
17
25
|
*
|
|
18
26
|
* Usage:
|
|
19
|
-
* node scripts/build-agents-md.mjs # write
|
|
20
|
-
* node scripts/build-agents-md.mjs --check # exit 1 if
|
|
27
|
+
* node scripts/build-agents-md.mjs # write both artifacts
|
|
28
|
+
* node scripts/build-agents-md.mjs --check # exit 1 if either is stale
|
|
21
29
|
*
|
|
22
30
|
* No runtime dependencies (Node >= 18, stdlib only). Idempotent: running it
|
|
23
|
-
* twice produces byte-identical output
|
|
31
|
+
* twice produces byte-identical output (the version header is derived from
|
|
32
|
+
* git, not the wall clock).
|
|
24
33
|
*/
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
34
|
+
import { execFileSync } from 'node:child_process'
|
|
35
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
36
|
+
import { dirname, join, resolve } from 'node:path'
|
|
27
37
|
import { fileURLToPath } from 'node:url'
|
|
28
38
|
|
|
29
39
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
30
40
|
export const ROOT = join(__dirname, '..')
|
|
31
41
|
const INSTRUCTIONS_DIR = join(ROOT, 'src', 'instructions')
|
|
32
|
-
const AGENTS_MD_PATH = join(ROOT, 'AGENTS.md')
|
|
42
|
+
export const AGENTS_MD_PATH = join(ROOT, 'AGENTS.md')
|
|
43
|
+
export const AGENTS_GLOBAL_MD_PATH = join(ROOT, 'AGENTS.global.md')
|
|
33
44
|
|
|
34
45
|
// ---------------------------------------------------------------------------
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
46
|
+
// Managed region — both artifacts are fully machine-generated; the sentinels
|
|
47
|
+
// let the installer refresh the region while preserving any user content
|
|
48
|
+
// around it (conservative migration: non-managed content is never deleted).
|
|
38
49
|
// ---------------------------------------------------------------------------
|
|
39
|
-
const
|
|
50
|
+
export const MANAGED_BEGIN =
|
|
51
|
+
'<!-- pantheon:generated:begin — Pantheon-managed region; regenerated on install/upgrade, never edit inside -->'
|
|
52
|
+
export const MANAGED_END =
|
|
53
|
+
'<!-- pantheon:generated:end — user content outside this region is preserved verbatim -->'
|
|
54
|
+
|
|
55
|
+
/** Header comment every generator version has emitted, old and new. */
|
|
56
|
+
const GENERATED_BY = '<!-- Generated by scripts/build-agents-md.mjs — do not edit directly. -->'
|
|
57
|
+
const VERSION_LINE_RE = /^<!-- pantheon:agents-md v2 artifact=(?:global|project) [^\n]*-->$/gm
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Normalize the volatile version line so `--check` can byte-compare content
|
|
61
|
+
* across commits (the sha/generated-at intentionally lag HEAD by one commit).
|
|
62
|
+
*
|
|
63
|
+
* @param {string} content - Artifact content
|
|
64
|
+
* @returns {string} Content with the version line reduced to a fixed stub
|
|
65
|
+
*/
|
|
66
|
+
export function stripVersionLine(content) {
|
|
67
|
+
return content.replace(VERSION_LINE_RE, '<!-- pantheon:agents-md v2 -->')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Short git sha + commit date of HEAD — deterministic per commit, so the
|
|
72
|
+
* version header never breaks byte-determinism while still dating artifacts.
|
|
73
|
+
* Falls back to "unknown" outside a git worktree (npm tarball installs).
|
|
74
|
+
*
|
|
75
|
+
* @param {string} [root] - Repository root to run git in
|
|
76
|
+
* @returns {{ sha: string, date: string }}
|
|
77
|
+
*/
|
|
78
|
+
export function readGitInfo(root = ROOT) {
|
|
79
|
+
try {
|
|
80
|
+
const opts = { cwd: root, encoding: 'utf8' }
|
|
81
|
+
return {
|
|
82
|
+
sha: execFileSync('git', ['rev-parse', '--short=7', 'HEAD'], opts).trim(),
|
|
83
|
+
date: execFileSync('git', ['log', '-1', '--format=%cI'], opts).trim(),
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
return { sha: 'unknown', date: 'unknown' }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The `pantheon:agents-md` version line embedded in every artifact.
|
|
92
|
+
*
|
|
93
|
+
* @param {'global' | 'project'} artifact - Which artifact the line stamps
|
|
94
|
+
* @param {{ sha: string, date: string }} git - Git info from readGitInfo()
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
function versionLine(artifact, git) {
|
|
98
|
+
return `<!-- pantheon:agents-md v2 artifact=${artifact} generated-at=${git.date} sha=${git.sha} -->`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Static headers — GLOBAL is repo-agnostic (safe for any project), PROJECT is
|
|
103
|
+
// repo-specific (this repository's setup). Zero overlap by construction.
|
|
104
|
+
// Backticks are escaped for the template literal.
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
const GLOBAL_HEADER = `# Pantheon Agent System — OpenCode
|
|
40
107
|
|
|
41
108
|
This project uses the Pantheon multi-agent framework with 14 specialized agents.
|
|
42
109
|
|
|
@@ -44,21 +111,30 @@ This project uses the Pantheon multi-agent framework with 14 specialized agents.
|
|
|
44
111
|
|
|
45
112
|
| Agent | Role |
|
|
46
113
|
|-------|------|
|
|
47
|
-
| @aphrodite | Frontend specialist — React 19, TypeScript strict, WCAG accessibility, responsive design, TDD, modern API patterns, deprecated npm detection.
|
|
114
|
+
| @aphrodite | Frontend specialist — React 19, TypeScript strict, WCAG accessibility, responsive design, TDD, modern API patterns, deprecated npm detection. Uses discovery/review proportionally to scope and risk. |
|
|
48
115
|
| @apollo | Read-only investigation scout — 3–10 parallel searches across codebase, external docs, and GitHub. Called by: athena, zeus, hermes, aphrodite, demeter. No edits, no commands. |
|
|
49
116
|
| @athena | Strategic planner & architect — research-first, plan-only, never implements. Plans include quality gates (ruff/Biome, dep detection, LTS policy). Calls apollo for discovery. |
|
|
50
|
-
| @demeter | Database specialist — SQLAlchemy 2.0, Alembic, query optimization, N+1 prevention, TDD migrations, modern DB libs.
|
|
117
|
+
| @demeter | Database specialist — SQLAlchemy 2.0, Alembic, query optimization, N+1 prevention, TDD migrations, modern DB libs. Preserves review gates for schema/data changes. |
|
|
51
118
|
| @gaia | Remote sensing domain specialist — satellite image processing, spectral analysis, SAR, change detection, time series, ML/DL classification. Read-only analysis of geospatial data. |
|
|
52
119
|
| @hephaestus | AI tooling & pipelines specialist — LangChain/LangGraph chains, RAG architecture, vector stores, embedding strategies. Forges AI infrastructure. Calls apollo, sends to themis. |
|
|
53
|
-
| @hermes | Backend specialist — FastAPI, Python, async, TDD (RED→GREEN→REFACTOR), modern Python stdlib, obsolete lib detection.
|
|
120
|
+
| @hermes | Backend specialist — FastAPI, Python, async, TDD (RED→GREEN→REFACTOR), modern Python stdlib, obsolete lib detection. Uses discovery/review proportionally to scope and risk. |
|
|
54
121
|
| @iris | GitHub operations specialist — branches, pull requests, issues, releases, tags. Called by zeus after review. Never pushes or merges without explicit human approval. Integrates with VS Code GitHub Pull Requests extension. |
|
|
55
122
|
| @mnemosyne | Memory bank quality owner — initializes .pantheon/memory-bank/, writes ADRs and task records on explicit request. Called by zeus. Never invoked automatically after phases. |
|
|
56
123
|
| @nyx | Observability & monitoring specialist — OpenTelemetry tracing, token/cost tracking, agent performance analytics, LangSmith integration. Calls apollo for discovery, sends to themis. |
|
|
57
124
|
| @prometheus | Infrastructure + model provider specialist — Docker, CI/CD, multi-model routing, cost optimization, provider abstraction |
|
|
58
125
|
| @talos | 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. |
|
|
59
|
-
| @themis | Quality & security gate — ruff/Biome linting, dead/legacy code detection, OWASP Top 10, coverage
|
|
126
|
+
| @themis | Quality & security gate — ruff/Biome linting, dead/legacy code detection, OWASP Top 10, applicable coverage requirements, correctness, deprecation audit. Reviews material/sensitive changes. |
|
|
60
127
|
| @zeus | Central orchestrator — never implements. Delegates to: athena, apollo, hermes, aphrodite, demeter, prometheus, themis, iris, mnemosyne, talos, hephaestus, nyx |
|
|
61
128
|
|
|
129
|
+
Repository-specific setup and conventions live in each project's own AGENTS.md; this global file carries only the rules shared by every Pantheon session, so nothing is ever loaded twice.
|
|
130
|
+
`
|
|
131
|
+
|
|
132
|
+
const PROJECT_HEADER = `# Pantheon — Repository Setup
|
|
133
|
+
|
|
134
|
+
Shared Pantheon rules (agent table, return format, memory protocol, YAGNI,
|
|
135
|
+
communication rules) live in the global \`~/.config/opencode/AGENTS.md\`; this
|
|
136
|
+
file carries only repository-specific setup so no rule is loaded twice.
|
|
137
|
+
|
|
62
138
|
## OpenCode Setup
|
|
63
139
|
|
|
64
140
|
See [INSTALLATION.md](docs/INSTALLATION.md) for setup instructions.
|
|
@@ -78,13 +154,22 @@ Para validar a instalação global do pacote pantheon-opencode COMO UM USUÁRIO
|
|
|
78
154
|
|
|
79
155
|
## Conventions
|
|
80
156
|
|
|
81
|
-
- TDD:
|
|
82
|
-
-
|
|
157
|
+
- TDD: RED→GREEN→REFACTOR for testable behavior; use focused checks for micro-edits
|
|
158
|
+
- Honor repository coverage thresholds when relevant; preserve stronger auth/security/data-integrity gates
|
|
83
159
|
- Async/await on all I/O
|
|
84
160
|
- Type hints on all functions
|
|
85
161
|
- PRs always update the README — every pull request must document newly added features, behaviors, env vars, or commands in the README before being opened.
|
|
86
162
|
`
|
|
87
163
|
|
|
164
|
+
const INSTRUCTIONS_PREAMBLE = `---
|
|
165
|
+
|
|
166
|
+
# Agent Instructions
|
|
167
|
+
|
|
168
|
+
The following sections are consolidated from \`src/instructions/*.instructions.md\`
|
|
169
|
+
so both OpenCode V1 (auto-loads AGENTS.md) and V2 (loads AGENTS.md, ignores the
|
|
170
|
+
\`instructions\` config key) receive identical shared rules. Agent-scoped rules
|
|
171
|
+
are rendered into only the matching installed agent prompts.`
|
|
172
|
+
|
|
88
173
|
// ---------------------------------------------------------------------------
|
|
89
174
|
// Frontmatter parsing (stdlib only — no yaml dependency)
|
|
90
175
|
// ---------------------------------------------------------------------------
|
|
@@ -92,21 +177,42 @@ Para validar a instalação global do pacote pantheon-opencode COMO UM USUÁRIO
|
|
|
92
177
|
/**
|
|
93
178
|
* Parse YAML-ish frontmatter from an instruction file.
|
|
94
179
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
180
|
+
* Preserve the file-scope `applyTo` glob separately from the explicit
|
|
181
|
+
* `agents` selectors. In particular, a Python workspace glob is never
|
|
182
|
+
* interpreted as an agent name. The frontmatter block is stripped from
|
|
183
|
+
* the embedded body.
|
|
97
184
|
*
|
|
98
185
|
* @param {string} content - Raw file content
|
|
99
|
-
* @returns {{ name: string, body: string
|
|
186
|
+
* @returns {{ name: string, body: string, applyTo: string | null,
|
|
187
|
+
* agents: string[], fallback: string | null, appliesToAgent: (name: string) => boolean } | null}
|
|
100
188
|
*/
|
|
101
|
-
function parseInstruction(content) {
|
|
189
|
+
export function parseInstruction(content) {
|
|
102
190
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/)
|
|
103
191
|
if (!match) return null
|
|
104
192
|
const fm = match[1]
|
|
105
193
|
const nameMatch = fm.match(/^name:\s*["']?([^"'\r\n]+)["']?\s*$/m)
|
|
106
194
|
if (!nameMatch) return null
|
|
195
|
+
const applyToMatch = fm.match(/^applyTo:\s*["']?([^"'\r\n]+)["']?\s*$/m)
|
|
196
|
+
const agentsMatch = fm.match(/^agents:\s*\[([^\]\r\n]*)\]\s*$/m)
|
|
197
|
+
const agents = agentsMatch
|
|
198
|
+
? agentsMatch[1]
|
|
199
|
+
.split(',')
|
|
200
|
+
.map((agent) => agent.trim().replace(/^(["'])(.*)\1$/, '$2'))
|
|
201
|
+
.filter(Boolean)
|
|
202
|
+
: fm.match(/^agents:/m)
|
|
203
|
+
? [] // Malformed explicit selector: fail closed; never make it global.
|
|
204
|
+
: applyToMatch
|
|
205
|
+
? [] // File-scoped instructions need an explicit agent association.
|
|
206
|
+
: ['*'] // Legacy instructions without selectors remain shared.
|
|
207
|
+
const fallbackMatch = fm.match(/^fallback:\s*["']?([^"'\r\n]+)["']?\s*$/m)
|
|
208
|
+
const appliesToAgent = (agent) => agents.includes('*') || agents.includes(agent)
|
|
107
209
|
return {
|
|
108
210
|
name: nameMatch[1].trim(),
|
|
109
|
-
|
|
211
|
+
applyTo: applyToMatch?.[1].trim() ?? null,
|
|
212
|
+
agents,
|
|
213
|
+
fallback: fallbackMatch?.[1].trim() ?? null,
|
|
214
|
+
appliesToAgent,
|
|
215
|
+
body: `${content.slice(match[0].length).trimEnd()}\n`,
|
|
110
216
|
}
|
|
111
217
|
}
|
|
112
218
|
|
|
@@ -114,7 +220,8 @@ function parseInstruction(content) {
|
|
|
114
220
|
* Read all src/instructions/*.instructions.md files, sorted by filename for
|
|
115
221
|
* deterministic output.
|
|
116
222
|
*
|
|
117
|
-
* @returns {Array<{ file: string, name: string, body: string
|
|
223
|
+
* @returns {Array<{ file: string, name: string, body: string, applyTo: string | null,
|
|
224
|
+
* agents: string[], fallback: string | null, appliesToAgent: (name: string) => boolean }>}
|
|
118
225
|
*/
|
|
119
226
|
export function readInstructions() {
|
|
120
227
|
if (!existsSync(INSTRUCTIONS_DIR)) return []
|
|
@@ -135,28 +242,29 @@ export function readInstructions() {
|
|
|
135
242
|
// ---------------------------------------------------------------------------
|
|
136
243
|
|
|
137
244
|
/**
|
|
138
|
-
* Generate the
|
|
245
|
+
* Generate the GLOBAL AGENTS.md content (repo-agnostic): agent table + the
|
|
246
|
+
* shared instruction bodies. This is the content installed into the user's
|
|
247
|
+
* global config dir as AGENTS.md.
|
|
139
248
|
*
|
|
140
|
-
* @param {Array<{ file: string, name: string, body: string }>} instructions
|
|
249
|
+
* @param {Array<{ file: string, name: string, body: string, agents?: string[] }>} instructions
|
|
250
|
+
* @param {{ sha: string, date: string }} git - Git info from readGitInfo()
|
|
141
251
|
* @returns {string}
|
|
142
252
|
*/
|
|
143
|
-
export function
|
|
253
|
+
export function generateGlobalAgentsMd(instructions, git = readGitInfo()) {
|
|
254
|
+
const sharedInstructions = instructions.filter(
|
|
255
|
+
(instr) => !instr.agents || instr.agents.includes('*'),
|
|
256
|
+
)
|
|
144
257
|
const sections = [
|
|
145
|
-
|
|
146
|
-
|
|
258
|
+
MANAGED_BEGIN,
|
|
259
|
+
GENERATED_BY,
|
|
260
|
+
'<!-- Source of truth: src/instructions/*.instructions.md + the static sections in this script. -->',
|
|
261
|
+
versionLine('global', git),
|
|
147
262
|
'',
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
'---',
|
|
151
|
-
'',
|
|
152
|
-
'# Agent Instructions',
|
|
153
|
-
'',
|
|
154
|
-
'The following sections are consolidated from `src/instructions/*.instructions.md`',
|
|
155
|
-
'so both OpenCode V1 (auto-loads AGENTS.md) and V2 (loads AGENTS.md, ignores the',
|
|
156
|
-
'`instructions` config key) receive identical instruction content.',
|
|
263
|
+
GLOBAL_HEADER.trimEnd(),
|
|
264
|
+
INSTRUCTIONS_PREAMBLE,
|
|
157
265
|
'',
|
|
158
266
|
]
|
|
159
|
-
for (const instr of
|
|
267
|
+
for (const instr of sharedInstructions) {
|
|
160
268
|
sections.push(
|
|
161
269
|
`<!-- Source: src/instructions/${instr.file} -->`,
|
|
162
270
|
`## ${instr.name}`,
|
|
@@ -165,7 +273,194 @@ export function generateAgentsMd(instructions) {
|
|
|
165
273
|
'',
|
|
166
274
|
)
|
|
167
275
|
}
|
|
168
|
-
|
|
276
|
+
sections.push(MANAGED_END)
|
|
277
|
+
return `${sections.join('\n').trimEnd()}\n`
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Generate the PROJECT AGENTS.md content (repo-specific setup only). Shared
|
|
282
|
+
* rules deliberately live in the global artifact so a session never pays for
|
|
283
|
+
* the same rule twice.
|
|
284
|
+
*
|
|
285
|
+
* @param {{ sha: string, date: string }} git - Git info from readGitInfo()
|
|
286
|
+
* @returns {string}
|
|
287
|
+
*/
|
|
288
|
+
export function generateProjectAgentsMd(git = readGitInfo()) {
|
|
289
|
+
const sections = [
|
|
290
|
+
MANAGED_BEGIN,
|
|
291
|
+
GENERATED_BY,
|
|
292
|
+
'<!-- Source of truth: the static project sections in scripts/build-agents-md.mjs. -->',
|
|
293
|
+
versionLine('project', git),
|
|
294
|
+
'',
|
|
295
|
+
PROJECT_HEADER.trimEnd(),
|
|
296
|
+
MANAGED_END,
|
|
297
|
+
]
|
|
298
|
+
return `${sections.join('\n').trimEnd()}\n`
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
// Managed-region writer — shared by the installer (scripts/install/opencode.mjs)
|
|
303
|
+
// and the postinstall freshness sync (scripts/sync-artifacts.mjs).
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Write an AGENTS.md artifact conservatively:
|
|
308
|
+
*
|
|
309
|
+
* - missing target → write generated content ('created')
|
|
310
|
+
* - managed region present → replace ONLY the region; any user content
|
|
311
|
+
* around it is preserved ('updated' when the
|
|
312
|
+
* file is pure managed content, 'merged' when
|
|
313
|
+
* foreign content was kept)
|
|
314
|
+
* - legacy generated file → the whole file is machine-generated per its
|
|
315
|
+
* own header; upgrade in place ('upgraded')
|
|
316
|
+
* - foreign file → never clobber: append the managed region
|
|
317
|
+
* below the user's content ('merged')
|
|
318
|
+
*
|
|
319
|
+
* Byte-compare idempotent: identical content returns 'skipped' untouched.
|
|
320
|
+
*
|
|
321
|
+
* @param {string} filePath - Destination AGENTS.md path
|
|
322
|
+
* @param {string} generated - Generated artifact content (incl. sentinels)
|
|
323
|
+
* @param {boolean} [dryRun=false] - Do not write when true
|
|
324
|
+
* @returns {'created' | 'skipped' | 'updated' | 'upgraded' | 'merged'}
|
|
325
|
+
*/
|
|
326
|
+
export function writeAgentsMdManaged(filePath, generated, dryRun = false) {
|
|
327
|
+
const existing = existsSync(filePath) ? readFileSync(filePath, 'utf8') : null
|
|
328
|
+
if (existing === null) {
|
|
329
|
+
if (!dryRun) {
|
|
330
|
+
mkdirSync(dirname(filePath), { recursive: true })
|
|
331
|
+
writeFileSync(filePath, generated, 'utf8')
|
|
332
|
+
}
|
|
333
|
+
return 'created'
|
|
334
|
+
}
|
|
335
|
+
if (existing === generated) return 'skipped'
|
|
336
|
+
|
|
337
|
+
const begin = existing.indexOf(MANAGED_BEGIN)
|
|
338
|
+
const end = existing.indexOf(MANAGED_END)
|
|
339
|
+
if (begin !== -1 && end !== -1 && end > begin) {
|
|
340
|
+
const before = existing.slice(0, begin)
|
|
341
|
+
const after = existing.slice(end + MANAGED_END.length)
|
|
342
|
+
const next = `${before}${generated}${after}`
|
|
343
|
+
if (next === existing) return 'skipped'
|
|
344
|
+
if (!dryRun) writeFileSync(filePath, next, 'utf8')
|
|
345
|
+
return before.trim() === '' && after.trim() === '' ? 'updated' : 'merged'
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (existing.startsWith(GENERATED_BY)) {
|
|
349
|
+
// Legacy format: the header declares the whole file machine-generated.
|
|
350
|
+
if (!dryRun) writeFileSync(filePath, generated, 'utf8')
|
|
351
|
+
return 'upgraded'
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Foreign content: preserve it verbatim and append the managed region.
|
|
355
|
+
if (!dryRun) {
|
|
356
|
+
const separator = existing.endsWith('\n') ? '\n' : '\n\n'
|
|
357
|
+
writeFileSync(filePath, `${existing}${separator}${generated}`, 'utf8')
|
|
358
|
+
}
|
|
359
|
+
return 'merged'
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const GENERATED_AGENT_BLOCK =
|
|
363
|
+
/\n?<!-- Pantheon selective instructions: generated -->[\s\S]*?<!-- \/Pantheon selective instructions -->\n?/g
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Render the agent-specific instruction set into the supported agent prompt.
|
|
367
|
+
* The shared rules remain in AGENTS.md; file globs are retained as scope
|
|
368
|
+
* guidance and are never used as agent selectors.
|
|
369
|
+
*
|
|
370
|
+
* @param {string} agentName - Canonical agent name from the prompt filename
|
|
371
|
+
* @param {string} prompt - Canonical source agent prompt
|
|
372
|
+
* @param {Array<{ file: string, name: string, body: string, applyTo?: string | null,
|
|
373
|
+
* agents?: string[], fallback?: string | null }>} instructions
|
|
374
|
+
* @returns {string}
|
|
375
|
+
*/
|
|
376
|
+
export function generateAgentPrompt(agentName, prompt, instructions = readInstructions()) {
|
|
377
|
+
const basePrompt = prompt.replace(GENERATED_AGENT_BLOCK, '').trimEnd()
|
|
378
|
+
const selected = instructions.filter(
|
|
379
|
+
(instr) => instr.agents && !instr.agents.includes('*') && instr.agents.includes(agentName),
|
|
380
|
+
)
|
|
381
|
+
if (selected.length === 0) {
|
|
382
|
+
return [
|
|
383
|
+
basePrompt,
|
|
384
|
+
'',
|
|
385
|
+
'<!-- Pantheon selective instructions: generated -->',
|
|
386
|
+
'No agent-specific instructions are assigned; follow the shared rules in AGENTS.md.',
|
|
387
|
+
'<!-- /Pantheon selective instructions -->',
|
|
388
|
+
'',
|
|
389
|
+
].join('\n')
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const sections = selected.map((instr) => {
|
|
393
|
+
const scope = instr.applyTo
|
|
394
|
+
? `\n\n> Apply only when a target workspace file matches \`${instr.applyTo}\`.`
|
|
395
|
+
: ''
|
|
396
|
+
const fallback =
|
|
397
|
+
instr.fallback === 'shared'
|
|
398
|
+
? '\n\n> For work outside this scope, use the shared rules in AGENTS.md.'
|
|
399
|
+
: ''
|
|
400
|
+
return [
|
|
401
|
+
`<!-- Source: src/instructions/${instr.file} -->`,
|
|
402
|
+
`## ${instr.name}${scope}`,
|
|
403
|
+
'',
|
|
404
|
+
`${instr.body.trimEnd()}${fallback}`,
|
|
405
|
+
].join('\n')
|
|
406
|
+
})
|
|
407
|
+
return [
|
|
408
|
+
basePrompt,
|
|
409
|
+
'',
|
|
410
|
+
'<!-- Pantheon selective instructions: generated -->',
|
|
411
|
+
sections.join('\n\n'),
|
|
412
|
+
'<!-- /Pantheon selective instructions -->',
|
|
413
|
+
'',
|
|
414
|
+
].join('\n')
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Copy agent prompts with scoped instructions embedded in each actual prompt.
|
|
419
|
+
* Used by both init and postinstall sync so the installed runtime receives the
|
|
420
|
+
* same selection without a per-turn injection hook.
|
|
421
|
+
*
|
|
422
|
+
* @param {string} sourceDir - Canonical src/agents directory
|
|
423
|
+
* @param {string} targetDir - OpenCode agents directory
|
|
424
|
+
* @param {boolean} dryRun - Do not write when true
|
|
425
|
+
* @param {boolean} existingOnly - Refresh installed prompts without adding missing agents
|
|
426
|
+
* @returns {{ created: number, skipped: number }}
|
|
427
|
+
*/
|
|
428
|
+
export function copyAgentPrompts(sourceDir, targetDir, dryRun = false, existingOnly = false) {
|
|
429
|
+
const instructions = readInstructions()
|
|
430
|
+
if (!dryRun && !existingOnly) mkdirSync(targetDir, { recursive: true })
|
|
431
|
+
let created = 0
|
|
432
|
+
let skipped = 0
|
|
433
|
+
for (const file of readdirSync(sourceDir)
|
|
434
|
+
.filter((entry) => entry.endsWith('.md'))
|
|
435
|
+
.sort()) {
|
|
436
|
+
if (file.toLowerCase() === 'readme.md') continue
|
|
437
|
+
const sourcePath = join(sourceDir, file)
|
|
438
|
+
if (!statSync(sourcePath).isFile()) continue
|
|
439
|
+
const agentName = file.replace(/\.md$/, '')
|
|
440
|
+
const output = generateAgentPrompt(agentName, readFileSync(sourcePath, 'utf8'), instructions)
|
|
441
|
+
const targetPath = join(targetDir, file)
|
|
442
|
+
if (existingOnly && !existsSync(targetPath)) continue
|
|
443
|
+
const existing = existsSync(targetPath) ? readFileSync(targetPath, 'utf8') : null
|
|
444
|
+
if (existing === output) {
|
|
445
|
+
skipped++
|
|
446
|
+
} else {
|
|
447
|
+
if (!dryRun) writeFileSync(targetPath, output, 'utf8')
|
|
448
|
+
created++
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return { created, skipped }
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Count LF-delimited logical lines without assuming a trailing newline.
|
|
456
|
+
*
|
|
457
|
+
* @param {string} content - File content
|
|
458
|
+
* @returns {number}
|
|
459
|
+
*/
|
|
460
|
+
export function logicalLineCount(content) {
|
|
461
|
+
if (content.length === 0) return 0
|
|
462
|
+
const separators = content.match(/\n/g)?.length ?? 0
|
|
463
|
+
return separators + (content.endsWith('\n') ? 0 : 1)
|
|
169
464
|
}
|
|
170
465
|
|
|
171
466
|
// ---------------------------------------------------------------------------
|
|
@@ -175,23 +470,46 @@ export function generateAgentsMd(instructions) {
|
|
|
175
470
|
function main() {
|
|
176
471
|
const checkOnly = process.argv.includes('--check')
|
|
177
472
|
const instructions = readInstructions()
|
|
178
|
-
const
|
|
473
|
+
const sharedCount = instructions.filter(
|
|
474
|
+
(instr) => !instr.agents || instr.agents.includes('*'),
|
|
475
|
+
).length
|
|
476
|
+
const global = generateGlobalAgentsMd(instructions)
|
|
477
|
+
const project = generateProjectAgentsMd()
|
|
179
478
|
|
|
180
479
|
if (checkOnly) {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
480
|
+
const stale = []
|
|
481
|
+
const existingGlobal = existsSync(AGENTS_GLOBAL_MD_PATH)
|
|
482
|
+
? readFileSync(AGENTS_GLOBAL_MD_PATH, 'utf8')
|
|
483
|
+
: ''
|
|
484
|
+
const existingProject = existsSync(AGENTS_MD_PATH) ? readFileSync(AGENTS_MD_PATH, 'utf8') : ''
|
|
485
|
+
if (stripVersionLine(existingGlobal) !== stripVersionLine(global)) {
|
|
486
|
+
stale.push('AGENTS.global.md')
|
|
487
|
+
}
|
|
488
|
+
if (stripVersionLine(existingProject) !== stripVersionLine(project)) {
|
|
489
|
+
stale.push('AGENTS.md')
|
|
490
|
+
}
|
|
491
|
+
if (stale.length > 0) {
|
|
492
|
+
console.error(
|
|
493
|
+
`❌ ${stale.join(' and ')} ${
|
|
494
|
+
stale.length > 1 ? 'are' : 'is'
|
|
495
|
+
} stale — run: node scripts/build-agents-md.mjs`,
|
|
496
|
+
)
|
|
184
497
|
process.exit(1)
|
|
185
498
|
}
|
|
186
|
-
console.log(
|
|
499
|
+
console.log(
|
|
500
|
+
`✅ AGENTS.md and AGENTS.global.md are up to date (${sharedCount} shared instruction sections)`,
|
|
501
|
+
)
|
|
187
502
|
return
|
|
188
503
|
}
|
|
189
504
|
|
|
190
|
-
writeFileSync(
|
|
191
|
-
|
|
505
|
+
writeFileSync(AGENTS_GLOBAL_MD_PATH, global, 'utf8')
|
|
506
|
+
writeFileSync(AGENTS_MD_PATH, project, 'utf8')
|
|
192
507
|
console.log(
|
|
193
|
-
`✅ Generated AGENTS.md (${
|
|
508
|
+
`✅ Generated AGENTS.md (${logicalLineCount(project)} lines) + AGENTS.global.md ` +
|
|
509
|
+
`(${logicalLineCount(global)} lines, ${sharedCount} shared instruction sections)`,
|
|
194
510
|
)
|
|
195
511
|
}
|
|
196
512
|
|
|
197
|
-
|
|
513
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
514
|
+
main()
|
|
515
|
+
}
|