pantheon-opencode 1.6.0-beta.1 → 1.6.0-beta.11

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 (133) hide show
  1. package/AGENTS.global.md +206 -0
  2. package/AGENTS.md +10 -891
  3. package/CHANGELOG.md +350 -0
  4. package/README.md +561 -52
  5. package/README.pt-BR.md +285 -20
  6. package/bin/pantheon-init.mjs +70 -4
  7. package/commands/pantheon-audit.md +7 -1
  8. package/commands/pantheon-consolidate.md +1 -1
  9. package/commands/pantheon-deepwork.md +14 -3
  10. package/commands/pantheon-focus.md +17 -0
  11. package/commands/pantheon-vision.md +61 -12
  12. package/package.json +24 -17
  13. package/plugin.json +2 -2
  14. package/pyproject.toml +8 -6
  15. package/scripts/build-agents-md.mjs +428 -56
  16. package/scripts/changelog-merge.mjs +161 -0
  17. package/scripts/{code_mode_server.py → code_mode.py} +3 -3
  18. package/scripts/cost.mjs +244 -41
  19. package/scripts/doctor.mjs +151 -1
  20. package/scripts/install/agents-md.mjs +4 -4
  21. package/scripts/install/backup.mjs +475 -0
  22. package/scripts/install/cli-ui.mjs +6 -1
  23. package/scripts/install/config-migration.mjs +317 -4
  24. package/scripts/install/exit-codes.mjs +26 -0
  25. package/scripts/install/health-check.mjs +21 -19
  26. package/scripts/install/interactive.mjs +16 -2
  27. package/scripts/install/migrate.mjs +41 -22
  28. package/scripts/install/opencode-version.mjs +246 -4
  29. package/scripts/install/opencode.mjs +1175 -215
  30. package/scripts/install/plan.mjs +287 -0
  31. package/scripts/install/plugin.mjs +62 -14
  32. package/scripts/install/shared.mjs +190 -48
  33. package/scripts/install/strings.mjs +20 -0
  34. package/scripts/install/venv.mjs +13 -3
  35. package/scripts/install/vision-prompt.mjs +171 -0
  36. package/scripts/install-mcp.mjs +28 -9
  37. package/scripts/{mcp_persistence_server.py → mcp_persistence.py} +52 -17
  38. package/{src/mcp/mcp_resources_server.py → scripts/mcp_resources.py} +2 -2
  39. package/scripts/{memory_mcp_server.py → memory_mcp.py} +2 -2
  40. package/scripts/probe-context-rehydrate.mjs +8 -2
  41. package/scripts/probe-pantheon-cost.mjs +71 -15
  42. package/scripts/prune.mjs +9 -3
  43. package/scripts/restore.mjs +222 -0
  44. package/scripts/run-node-tests.mjs +189 -0
  45. package/scripts/sync-artifacts.mjs +26 -13
  46. package/scripts/sync-tui.mjs +24 -3
  47. package/scripts/test-opencode-v2-sandbox.sh +180 -26
  48. package/scripts/validate-routing.mjs +3 -3
  49. package/scripts/validate-sandbox-install.mjs +138 -0
  50. package/scripts/versioning.mjs +13 -1
  51. package/scripts/vision-status.mjs +682 -0
  52. package/skills-lock.json +8 -8
  53. package/src/agents/aphrodite.md +20 -37
  54. package/src/agents/apollo.md +7 -3
  55. package/src/agents/athena.md +63 -35
  56. package/src/agents/demeter.md +8 -28
  57. package/src/agents/gaia.md +1 -1
  58. package/src/agents/hephaestus.md +10 -21
  59. package/src/agents/hermes.md +15 -38
  60. package/src/agents/iris.md +7 -1
  61. package/src/agents/mnemosyne.md +18 -54
  62. package/src/agents/nyx.md +1 -1
  63. package/src/agents/prometheus.md +16 -20
  64. package/src/agents/talos.md +10 -2
  65. package/src/agents/themis.md +5 -5
  66. package/src/agents/zeus.md +99 -172
  67. package/src/instructions/agent-return-format.instructions.md +34 -17
  68. package/src/instructions/backend-standards.instructions.md +2 -0
  69. package/src/instructions/frontend-standards.instructions.md +3 -1
  70. package/src/instructions/memory-protocol.instructions.md +12 -110
  71. package/src/instructions/test-economy.instructions.md +21 -0
  72. package/src/instructions/zeus-anti-stall.instructions.md +20 -67
  73. package/src/instructions/zeus-communication-rules.instructions.md +0 -1
  74. package/src/instructions/zeus-council-synthesis.instructions.md +31 -260
  75. package/src/instructions/zeus-memory-operations.instructions.md +30 -0
  76. package/src/instructions/zeus-timeout-retry.instructions.md +61 -89
  77. package/src/mcp/{code_mode_server.py → code_mode.py} +3 -3
  78. package/src/mcp/eval_store.py +2 -2
  79. package/src/mcp/{mcp_persistence_server.py → mcp_persistence.py} +52 -17
  80. package/{scripts/mcp_resources_server.py → src/mcp/mcp_resources.py} +2 -2
  81. package/src/mcp/{memory_mcp_server.py → memory_mcp.py} +2 -2
  82. package/src/mcp/{pantheon_vision_server.py → pantheon_vision.py} +286 -42
  83. package/src/mcp/requirements-mcp.txt +6 -4
  84. package/src/mcp/requirements-vision.txt +2 -2
  85. package/src/pantheon/cost-command.ts +403 -102
  86. package/src/pantheon/delegation-enforce.ts +10 -1
  87. package/src/pantheon/model-agents.ts +19 -0
  88. package/src/pantheon/model-command.ts +218 -56
  89. package/src/pantheon/presets.d.mts +0 -8
  90. package/src/pantheon/presets.mjs +5 -32
  91. package/src/pantheon/v2-bounded-read.ts +640 -0
  92. package/src/pantheon/v2-delegation-enforce.ts +214 -0
  93. package/src/pantheon/v2-execute-after.ts +186 -0
  94. package/src/pantheon/v2-model-overrides.ts +275 -0
  95. package/src/pantheon/v2-tools.ts +418 -0
  96. package/src/pantheon/v2-unsupported.d.mts +11 -0
  97. package/src/pantheon/v2-unsupported.mjs +90 -0
  98. package/src/pantheon/v2-vision.ts +622 -0
  99. package/src/pantheon/vision-local.ts +531 -0
  100. package/src/pantheon/vision.ts +1160 -115
  101. package/src/plugin-v2.ts +1158 -219
  102. package/src/plugin.ts +12 -9
  103. package/src/plugins/tui/dist/server.d.ts +1 -2
  104. package/src/plugins/tui/dist/server.d.ts.map +1 -1
  105. package/src/plugins/tui/dist/tui-v2.d.ts +154 -0
  106. package/src/plugins/tui/dist/tui-v2.d.ts.map +1 -0
  107. package/src/plugins/tui/dist/tui-v2.js +659 -0
  108. package/src/plugins/tui/dist/tui-v2.js.map +1 -0
  109. package/src/plugins/tui/dist/tui.d.ts +85 -82
  110. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  111. package/src/plugins/tui/dist/tui.js +72 -59
  112. package/src/plugins/tui/index-v2.ts +6 -0
  113. package/src/plugins/tui/package-lock.json +5516 -1353
  114. package/src/plugins/tui/package.json +22 -12
  115. package/src/plugins/tui/src/index-v2.tsx +734 -0
  116. package/src/plugins/tui/src/index.tsx +106 -85
  117. package/src/plugins/tui/src/opencode-v2.d.ts +13 -0
  118. package/src/plugins/tui/tsconfig.build.json +5 -0
  119. package/src/plugins/tui/tsconfig.typecheck.json +5 -0
  120. package/src/plugins/tui/tsdown.config.ts +22 -2
  121. package/src/plugins/tui/tui-v2.ts +2 -0
  122. package/src/plugins/tui/tui.ts +3 -0
  123. package/src/routing.yml +11 -21
  124. package/src/skills/README.md +3 -3
  125. package/src/skills/agent-coordination/SKILL.md +7 -2
  126. package/src/skills/artifact-management/SKILL.md +12 -5
  127. package/src/skills/auto-continue/SKILL.md +52 -35
  128. package/src/skills/code-review-checklist/SKILL.md +5 -5
  129. package/src/skills/context-compression/SKILL.md +26 -879
  130. package/src/skills/session-goal/SKILL.md +14 -5
  131. package/src/skills/spec-driven-development/SKILL.md +3 -1
  132. package/src/skills/tdd-with-agents/SKILL.md +18 -10
  133. package/opencode.json +0 -58
@@ -1,42 +1,111 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * build-agents-md.mjs — Generate the repo AGENTS.md from canonical sources.
3
+ * build-agents-md.mjs — Generate BOTH AGENTS.md artifacts from canonical sources.
4
4
  *
5
- * AGENTS.md is the single instruction file BOTH OpenCode V1 and V2 load
6
- * (V1 auto-discovers it, V2 loads it explicitly; the `instructions` config
7
- * key is accepted-but-ignored by V2). To keep instruction content in parity
8
- * across both versions without duplication, this script consolidates:
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
- * (a) a static header (project description + agent table + setup notes)
11
- * (b) every src/instructions/*.instructions.md body, embedded under a
12
- * `## <name>` section marker (name taken from frontmatter)
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
- * into one generated AGENTS.md. src/instructions/*.instructions.md remains
15
- * the source of truth — AGENTS.md is a committed build artifact, never
16
- * hand-edited.
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 AGENTS.md to repo root
20
- * node scripts/build-agents-md.mjs --check # exit 1 if AGENTS.md is stale
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 { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
26
- import { dirname, join } from 'node:path'
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
 
39
+ import { ACTIONS, isRecording, recordChange } from './install/plan.mjs'
40
+
29
41
  const __dirname = dirname(fileURLToPath(import.meta.url))
30
42
  export const ROOT = join(__dirname, '..')
31
43
  const INSTRUCTIONS_DIR = join(ROOT, 'src', 'instructions')
32
- const AGENTS_MD_PATH = join(ROOT, 'AGENTS.md')
44
+ export const AGENTS_MD_PATH = join(ROOT, 'AGENTS.md')
45
+ export const AGENTS_GLOBAL_MD_PATH = join(ROOT, 'AGENTS.global.md')
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Managed region — both artifacts are fully machine-generated; the sentinels
49
+ // let the installer refresh the region while preserving any user content
50
+ // around it (conservative migration: non-managed content is never deleted).
51
+ // ---------------------------------------------------------------------------
52
+ export const MANAGED_BEGIN =
53
+ '<!-- pantheon:generated:begin — Pantheon-managed region; regenerated on install/upgrade, never edit inside -->'
54
+ export const MANAGED_END =
55
+ '<!-- pantheon:generated:end — user content outside this region is preserved verbatim -->'
56
+
57
+ /** Header comment every generator version has emitted, old and new. */
58
+ const GENERATED_BY = '<!-- Generated by scripts/build-agents-md.mjs — do not edit directly. -->'
59
+ const VERSION_LINE_RE = /^<!-- pantheon:agents-md v2 artifact=(?:global|project) [^\n]*-->$/gm
60
+
61
+ /**
62
+ * Normalize the volatile version line so `--check` can byte-compare content
63
+ * across commits (the sha/generated-at intentionally lag HEAD by one commit).
64
+ *
65
+ * @param {string} content - Artifact content
66
+ * @returns {string} Content with the version line reduced to a fixed stub
67
+ */
68
+ export function stripVersionLine(content) {
69
+ return content.replace(VERSION_LINE_RE, '<!-- pantheon:agents-md v2 -->')
70
+ }
71
+
72
+ /**
73
+ * Short git sha + commit date of HEAD — deterministic per commit, so the
74
+ * version header never breaks byte-determinism while still dating artifacts.
75
+ * Falls back to "unknown" outside a git worktree (npm tarball installs).
76
+ *
77
+ * @param {string} [root] - Repository root to run git in
78
+ * @returns {{ sha: string, date: string }}
79
+ */
80
+ export function readGitInfo(root = ROOT) {
81
+ try {
82
+ const opts = { cwd: root, encoding: 'utf8' }
83
+ return {
84
+ sha: execFileSync('git', ['rev-parse', '--short=7', 'HEAD'], opts).trim(),
85
+ date: execFileSync('git', ['log', '-1', '--format=%cI'], opts).trim(),
86
+ }
87
+ } catch {
88
+ return { sha: 'unknown', date: 'unknown' }
89
+ }
90
+ }
91
+
92
+ /**
93
+ * The `pantheon:agents-md` version line embedded in every artifact.
94
+ *
95
+ * @param {'global' | 'project'} artifact - Which artifact the line stamps
96
+ * @param {{ sha: string, date: string }} git - Git info from readGitInfo()
97
+ * @returns {string}
98
+ */
99
+ function versionLine(artifact, git) {
100
+ return `<!-- pantheon:agents-md v2 artifact=${artifact} generated-at=${git.date} sha=${git.sha} -->`
101
+ }
33
102
 
34
103
  // ---------------------------------------------------------------------------
35
- // Static header — preserved verbatim from the original AGENTS.md (agent table,
36
- // setup notes, sandbox rules, conventions). Backticks are escaped for the
37
- // template literal.
104
+ // Static headers — GLOBAL is repo-agnostic (safe for any project), PROJECT is
105
+ // repo-specific (this repository's setup). Zero overlap by construction.
106
+ // Backticks are escaped for the template literal.
38
107
  // ---------------------------------------------------------------------------
39
- const STATIC_HEADER = `# Pantheon Agent System — OpenCode
108
+ const GLOBAL_HEADER = `# Pantheon Agent System — OpenCode
40
109
 
41
110
  This project uses the Pantheon multi-agent framework with 14 specialized agents.
42
111
 
@@ -44,21 +113,30 @@ This project uses the Pantheon multi-agent framework with 14 specialized agents.
44
113
 
45
114
  | Agent | Role |
46
115
  |-------|------|
47
- | @aphrodite | Frontend specialist — React 19, TypeScript strict, WCAG accessibility, responsive design, TDD, modern API patterns, deprecated npm detection. Calls apollo for discovery, sends to themis for review. |
116
+ | @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
117
  | @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
118
  | @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. Calls apollo for discovery, sends to themis. |
119
+ | @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
120
  | @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
121
  | @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. Calls apollo for discovery, sends to themis. |
122
+ | @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
123
  | @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
124
  | @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
125
  | @nyx | Observability & monitoring specialist — OpenTelemetry tracing, token/cost tracking, agent performance analytics, LangSmith integration. Calls apollo for discovery, sends to themis. |
57
126
  | @prometheus | Infrastructure + model provider specialist — Docker, CI/CD, multi-model routing, cost optimization, provider abstraction |
58
127
  | @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 >80%, correctness, deprecation audit. Called by implementers; escalates blockers to zeus. |
128
+ | @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
129
  | @zeus | Central orchestrator — never implements. Delegates to: athena, apollo, hermes, aphrodite, demeter, prometheus, themis, iris, mnemosyne, talos, hephaestus, nyx |
61
130
 
131
+ 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.
132
+ `
133
+
134
+ const PROJECT_HEADER = `# Pantheon — Repository Setup
135
+
136
+ Shared Pantheon rules (agent table, return format, memory protocol, YAGNI,
137
+ communication rules) live in the global \`~/.config/opencode/AGENTS.md\`; this
138
+ file carries only repository-specific setup so no rule is loaded twice.
139
+
62
140
  ## OpenCode Setup
63
141
 
64
142
  See [INSTALLATION.md](docs/INSTALLATION.md) for setup instructions.
@@ -78,13 +156,22 @@ Para validar a instalação global do pacote pantheon-opencode COMO UM USUÁRIO
78
156
 
79
157
  ## Conventions
80
158
 
81
- - TDD: Write failing test first, then implement
82
- - Coverage minimum: 80%
159
+ - TDD: RED→GREEN→REFACTOR for testable behavior; use focused checks for micro-edits
160
+ - Honor repository coverage thresholds when relevant; preserve stronger auth/security/data-integrity gates
83
161
  - Async/await on all I/O
84
162
  - Type hints on all functions
85
163
  - 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
164
  `
87
165
 
166
+ const INSTRUCTIONS_PREAMBLE = `---
167
+
168
+ # Agent Instructions
169
+
170
+ The following sections are consolidated from \`src/instructions/*.instructions.md\`
171
+ so both OpenCode V1 (auto-loads AGENTS.md) and V2 (loads AGENTS.md, ignores the
172
+ \`instructions\` config key) receive identical shared rules. Agent-scoped rules
173
+ are rendered into only the matching installed agent prompts.`
174
+
88
175
  // ---------------------------------------------------------------------------
89
176
  // Frontmatter parsing (stdlib only — no yaml dependency)
90
177
  // ---------------------------------------------------------------------------
@@ -92,20 +179,41 @@ Para validar a instalação global do pacote pantheon-opencode COMO UM USUÁRIO
92
179
  /**
93
180
  * Parse YAML-ish frontmatter from an instruction file.
94
181
  *
95
- * Only the `name` field is extracted (single-line scalar). The frontmatter
96
- * block itself is stripped from the embedded body.
182
+ * Preserve the file-scope `applyTo` glob separately from the explicit
183
+ * `agents` selectors. In particular, a Python workspace glob is never
184
+ * interpreted as an agent name. The frontmatter block is stripped from
185
+ * the embedded body.
97
186
  *
98
187
  * @param {string} content - Raw file content
99
- * @returns {{ name: string, body: string } | null}
188
+ * @returns {{ name: string, body: string, applyTo: string | null,
189
+ * agents: string[], fallback: string | null, appliesToAgent: (name: string) => boolean } | null}
100
190
  */
101
- function parseInstruction(content) {
191
+ export function parseInstruction(content) {
102
192
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/)
103
193
  if (!match) return null
104
194
  const fm = match[1]
105
195
  const nameMatch = fm.match(/^name:\s*["']?([^"'\r\n]+)["']?\s*$/m)
106
196
  if (!nameMatch) return null
197
+ const applyToMatch = fm.match(/^applyTo:\s*["']?([^"'\r\n]+)["']?\s*$/m)
198
+ const agentsMatch = fm.match(/^agents:\s*\[([^\]\r\n]*)\]\s*$/m)
199
+ const agents = agentsMatch
200
+ ? agentsMatch[1]
201
+ .split(',')
202
+ .map((agent) => agent.trim().replace(/^(["'])(.*)\1$/, '$2'))
203
+ .filter(Boolean)
204
+ : fm.match(/^agents:/m)
205
+ ? [] // Malformed explicit selector: fail closed; never make it global.
206
+ : applyToMatch
207
+ ? [] // File-scoped instructions need an explicit agent association.
208
+ : ['*'] // Legacy instructions without selectors remain shared.
209
+ const fallbackMatch = fm.match(/^fallback:\s*["']?([^"'\r\n]+)["']?\s*$/m)
210
+ const appliesToAgent = (agent) => agents.includes('*') || agents.includes(agent)
107
211
  return {
108
212
  name: nameMatch[1].trim(),
213
+ applyTo: applyToMatch?.[1].trim() ?? null,
214
+ agents,
215
+ fallback: fallbackMatch?.[1].trim() ?? null,
216
+ appliesToAgent,
109
217
  body: `${content.slice(match[0].length).trimEnd()}\n`,
110
218
  }
111
219
  }
@@ -114,7 +222,8 @@ function parseInstruction(content) {
114
222
  * Read all src/instructions/*.instructions.md files, sorted by filename for
115
223
  * deterministic output.
116
224
  *
117
- * @returns {Array<{ file: string, name: string, body: string }>}
225
+ * @returns {Array<{ file: string, name: string, body: string, applyTo: string | null,
226
+ * agents: string[], fallback: string | null, appliesToAgent: (name: string) => boolean }>}
118
227
  */
119
228
  export function readInstructions() {
120
229
  if (!existsSync(INSTRUCTIONS_DIR)) return []
@@ -135,28 +244,29 @@ export function readInstructions() {
135
244
  // ---------------------------------------------------------------------------
136
245
 
137
246
  /**
138
- * Generate the full AGENTS.md content.
247
+ * Generate the GLOBAL AGENTS.md content (repo-agnostic): agent table + the
248
+ * shared instruction bodies. This is the content installed into the user's
249
+ * global config dir as AGENTS.md.
139
250
  *
140
- * @param {Array<{ file: string, name: string, body: string }>} instructions
251
+ * @param {Array<{ file: string, name: string, body: string, agents?: string[] }>} instructions
252
+ * @param {{ sha: string, date: string }} git - Git info from readGitInfo()
141
253
  * @returns {string}
142
254
  */
143
- export function generateAgentsMd(instructions) {
255
+ export function generateGlobalAgentsMd(instructions, git = readGitInfo()) {
256
+ const sharedInstructions = instructions.filter(
257
+ (instr) => !instr.agents || instr.agents.includes('*'),
258
+ )
144
259
  const sections = [
145
- '<!-- Generated by scripts/build-agents-md.mjs — do not edit directly. -->',
146
- '<!-- Source of truth: src/instructions/*.instructions.md (AGENTS.md is a committed build artifact). -->',
147
- '',
148
- STATIC_HEADER.trimEnd(),
149
- '',
150
- '---',
260
+ MANAGED_BEGIN,
261
+ GENERATED_BY,
262
+ '<!-- Source of truth: src/instructions/*.instructions.md + the static sections in this script. -->',
263
+ versionLine('global', git),
151
264
  '',
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.',
265
+ GLOBAL_HEADER.trimEnd(),
266
+ INSTRUCTIONS_PREAMBLE,
157
267
  '',
158
268
  ]
159
- for (const instr of instructions) {
269
+ for (const instr of sharedInstructions) {
160
270
  sections.push(
161
271
  `<!-- Source: src/instructions/${instr.file} -->`,
162
272
  `## ${instr.name}`,
@@ -165,9 +275,248 @@ export function generateAgentsMd(instructions) {
165
275
  '',
166
276
  )
167
277
  }
278
+ sections.push(MANAGED_END)
168
279
  return `${sections.join('\n').trimEnd()}\n`
169
280
  }
170
281
 
282
+ /**
283
+ * Generate the PROJECT AGENTS.md content (repo-specific setup only). Shared
284
+ * rules deliberately live in the global artifact so a session never pays for
285
+ * the same rule twice.
286
+ *
287
+ * @param {{ sha: string, date: string }} git - Git info from readGitInfo()
288
+ * @returns {string}
289
+ */
290
+ export function generateProjectAgentsMd(git = readGitInfo()) {
291
+ const sections = [
292
+ MANAGED_BEGIN,
293
+ GENERATED_BY,
294
+ '<!-- Source of truth: the static project sections in scripts/build-agents-md.mjs. -->',
295
+ versionLine('project', git),
296
+ '',
297
+ PROJECT_HEADER.trimEnd(),
298
+ MANAGED_END,
299
+ ]
300
+ return `${sections.join('\n').trimEnd()}\n`
301
+ }
302
+
303
+ // ---------------------------------------------------------------------------
304
+ // Managed-region writer — shared by the installer (scripts/install/opencode.mjs)
305
+ // and the postinstall freshness sync (scripts/sync-artifacts.mjs).
306
+ // ---------------------------------------------------------------------------
307
+
308
+ /**
309
+ * Write an AGENTS.md artifact conservatively:
310
+ *
311
+ * - missing target → write generated content ('created')
312
+ * - managed region present → replace ONLY the region; any user content
313
+ * around it is preserved ('updated' when the
314
+ * file is pure managed content, 'merged' when
315
+ * foreign content was kept)
316
+ * - legacy generated file → the whole file is machine-generated per its
317
+ * own header; upgrade in place ('upgraded')
318
+ * - foreign file → never clobber: append the managed region
319
+ * below the user's content ('merged')
320
+ *
321
+ * Byte-compare idempotent: identical content returns 'skipped' untouched.
322
+ *
323
+ * @param {string} filePath - Destination AGENTS.md path
324
+ * @param {string} generated - Generated artifact content (incl. sentinels)
325
+ * @param {boolean} [dryRun=false] - Do not write when true
326
+ * @returns {'created' | 'skipped' | 'updated' | 'upgraded' | 'merged'}
327
+ */
328
+ export function writeAgentsMdManaged(filePath, generated, dryRun = false, recorder = null) {
329
+ const existing = existsSync(filePath) ? readFileSync(filePath, 'utf8') : null
330
+ if (existing === null) {
331
+ if (!dryRun) {
332
+ mkdirSync(dirname(filePath), { recursive: true })
333
+ writeFileSync(filePath, generated, 'utf8')
334
+ }
335
+ recordChange(
336
+ { path: filePath, action: ACTIONS.CREATE, bytes: Buffer.byteLength(generated, 'utf8') },
337
+ recorder,
338
+ )
339
+ return 'created'
340
+ }
341
+ if (existing === generated) {
342
+ recordChange(
343
+ { path: filePath, action: ACTIONS.UNCHANGED, bytes: Buffer.byteLength(existing, 'utf8') },
344
+ recorder,
345
+ )
346
+ return 'skipped'
347
+ }
348
+
349
+ const begin = existing.indexOf(MANAGED_BEGIN)
350
+ const end = existing.indexOf(MANAGED_END)
351
+ if (begin !== -1 && end !== -1 && end > begin) {
352
+ const before = existing.slice(0, begin)
353
+ const after = existing.slice(end + MANAGED_END.length)
354
+ const next = `${before}${generated}${after}`
355
+ if (next === existing) {
356
+ recordChange(
357
+ { path: filePath, action: ACTIONS.UNCHANGED, bytes: Buffer.byteLength(existing, 'utf8') },
358
+ recorder,
359
+ )
360
+ return 'skipped'
361
+ }
362
+ if (!dryRun) writeFileSync(filePath, next, 'utf8')
363
+ recordChange(
364
+ { path: filePath, action: ACTIONS.UPDATE, bytes: Buffer.byteLength(next, 'utf8') },
365
+ recorder,
366
+ )
367
+ return before.trim() === '' && after.trim() === '' ? 'updated' : 'merged'
368
+ }
369
+
370
+ if (existing.startsWith(GENERATED_BY)) {
371
+ // Legacy format: the header declares the whole file machine-generated.
372
+ if (!dryRun) writeFileSync(filePath, generated, 'utf8')
373
+ recordChange(
374
+ { path: filePath, action: ACTIONS.UPDATE, bytes: Buffer.byteLength(generated, 'utf8') },
375
+ recorder,
376
+ )
377
+ return 'upgraded'
378
+ }
379
+
380
+ // Foreign content: preserve it verbatim and append the managed region.
381
+ const separator = existing.endsWith('\n') ? '\n' : '\n\n'
382
+ const payload = `${existing}${separator}${generated}`
383
+ if (!dryRun) {
384
+ writeFileSync(filePath, payload, 'utf8')
385
+ }
386
+ recordChange(
387
+ { path: filePath, action: ACTIONS.UPDATE, bytes: Buffer.byteLength(payload, 'utf8') },
388
+ recorder,
389
+ )
390
+ return 'merged'
391
+ }
392
+
393
+ const GENERATED_AGENT_BLOCK =
394
+ /\n?<!-- Pantheon selective instructions: generated -->[\s\S]*?<!-- \/Pantheon selective instructions -->\n?/g
395
+
396
+ /**
397
+ * Render the agent-specific instruction set into the supported agent prompt.
398
+ * The shared rules remain in AGENTS.md; file globs are retained as scope
399
+ * guidance and are never used as agent selectors.
400
+ *
401
+ * @param {string} agentName - Canonical agent name from the prompt filename
402
+ * @param {string} prompt - Canonical source agent prompt
403
+ * @param {Array<{ file: string, name: string, body: string, applyTo?: string | null,
404
+ * agents?: string[], fallback?: string | null }>} instructions
405
+ * @returns {string}
406
+ */
407
+ export function generateAgentPrompt(agentName, prompt, instructions = readInstructions()) {
408
+ const basePrompt = prompt.replace(GENERATED_AGENT_BLOCK, '').trimEnd()
409
+ const selected = instructions.filter(
410
+ (instr) => instr.agents && !instr.agents.includes('*') && instr.agents.includes(agentName),
411
+ )
412
+ if (selected.length === 0) {
413
+ return [
414
+ basePrompt,
415
+ '',
416
+ '<!-- Pantheon selective instructions: generated -->',
417
+ 'No agent-specific instructions are assigned; follow the shared rules in AGENTS.md.',
418
+ '<!-- /Pantheon selective instructions -->',
419
+ '',
420
+ ].join('\n')
421
+ }
422
+
423
+ const sections = selected.map((instr) => {
424
+ const scope = instr.applyTo
425
+ ? `\n\n> Apply only when a target workspace file matches \`${instr.applyTo}\`.`
426
+ : ''
427
+ const fallback =
428
+ instr.fallback === 'shared'
429
+ ? '\n\n> For work outside this scope, use the shared rules in AGENTS.md.'
430
+ : ''
431
+ return [
432
+ `<!-- Source: src/instructions/${instr.file} -->`,
433
+ `## ${instr.name}${scope}`,
434
+ '',
435
+ `${instr.body.trimEnd()}${fallback}`,
436
+ ].join('\n')
437
+ })
438
+ return [
439
+ basePrompt,
440
+ '',
441
+ '<!-- Pantheon selective instructions: generated -->',
442
+ sections.join('\n\n'),
443
+ '<!-- /Pantheon selective instructions -->',
444
+ '',
445
+ ].join('\n')
446
+ }
447
+
448
+ /**
449
+ * Copy agent prompts with scoped instructions embedded in each actual prompt.
450
+ * Used by both init and postinstall sync so the installed runtime receives the
451
+ * same selection without a per-turn injection hook.
452
+ *
453
+ * @param {string} sourceDir - Canonical src/agents directory
454
+ * @param {string} targetDir - OpenCode agents directory
455
+ * @param {boolean} dryRun - Do not write when true
456
+ * @param {boolean} existingOnly - Refresh installed prompts without adding missing agents
457
+ * @param {(change: object) => void} [recorder] - optional change recorder for --dry-run preview
458
+ * @returns {{ created: number, skipped: number }}
459
+ */
460
+ export function copyAgentPrompts(
461
+ sourceDir,
462
+ targetDir,
463
+ dryRun = false,
464
+ existingOnly = false,
465
+ recorder = null,
466
+ ) {
467
+ const instructions = readInstructions()
468
+ if (!dryRun && !existingOnly) mkdirSync(targetDir, { recursive: true })
469
+ let created = 0
470
+ let skipped = 0
471
+ for (const file of readdirSync(sourceDir)
472
+ .filter((entry) => entry.endsWith('.md'))
473
+ .sort()) {
474
+ if (file.toLowerCase() === 'readme.md') continue
475
+ const sourcePath = join(sourceDir, file)
476
+ if (!statSync(sourcePath).isFile()) continue
477
+ const agentName = file.replace(/\.md$/, '')
478
+ const output = generateAgentPrompt(agentName, readFileSync(sourcePath, 'utf8'), instructions)
479
+ const targetPath = join(targetDir, file)
480
+ if (existingOnly && !existsSync(targetPath)) continue
481
+ const existing = existsSync(targetPath) ? readFileSync(targetPath, 'utf8') : null
482
+ if (existing === output) {
483
+ skipped++
484
+ if (isRecording(recorder)) {
485
+ recordChange(
486
+ { path: targetPath, action: ACTIONS.UNCHANGED, bytes: Buffer.byteLength(output, 'utf8') },
487
+ recorder,
488
+ )
489
+ }
490
+ } else {
491
+ if (!dryRun) writeFileSync(targetPath, output, 'utf8')
492
+ created++
493
+ if (isRecording(recorder)) {
494
+ recordChange(
495
+ {
496
+ path: targetPath,
497
+ action: existing === null ? ACTIONS.CREATE : ACTIONS.UPDATE,
498
+ bytes: Buffer.byteLength(output, 'utf8'),
499
+ },
500
+ recorder,
501
+ )
502
+ }
503
+ }
504
+ }
505
+ return { created, skipped }
506
+ }
507
+
508
+ /**
509
+ * Count LF-delimited logical lines without assuming a trailing newline.
510
+ *
511
+ * @param {string} content - File content
512
+ * @returns {number}
513
+ */
514
+ export function logicalLineCount(content) {
515
+ if (content.length === 0) return 0
516
+ const separators = content.match(/\n/g)?.length ?? 0
517
+ return separators + (content.endsWith('\n') ? 0 : 1)
518
+ }
519
+
171
520
  // ---------------------------------------------------------------------------
172
521
  // CLI entry point
173
522
  // ---------------------------------------------------------------------------
@@ -175,23 +524,46 @@ export function generateAgentsMd(instructions) {
175
524
  function main() {
176
525
  const checkOnly = process.argv.includes('--check')
177
526
  const instructions = readInstructions()
178
- const content = generateAgentsMd(instructions)
527
+ const sharedCount = instructions.filter(
528
+ (instr) => !instr.agents || instr.agents.includes('*'),
529
+ ).length
530
+ const global = generateGlobalAgentsMd(instructions)
531
+ const project = generateProjectAgentsMd()
179
532
 
180
533
  if (checkOnly) {
181
- const existing = existsSync(AGENTS_MD_PATH) ? readFileSync(AGENTS_MD_PATH, 'utf8') : ''
182
- if (existing !== content) {
183
- console.error(`❌ AGENTS.md is stale — run: node scripts/build-agents-md.mjs`)
534
+ const stale = []
535
+ const existingGlobal = existsSync(AGENTS_GLOBAL_MD_PATH)
536
+ ? readFileSync(AGENTS_GLOBAL_MD_PATH, 'utf8')
537
+ : ''
538
+ const existingProject = existsSync(AGENTS_MD_PATH) ? readFileSync(AGENTS_MD_PATH, 'utf8') : ''
539
+ if (stripVersionLine(existingGlobal) !== stripVersionLine(global)) {
540
+ stale.push('AGENTS.global.md')
541
+ }
542
+ if (stripVersionLine(existingProject) !== stripVersionLine(project)) {
543
+ stale.push('AGENTS.md')
544
+ }
545
+ if (stale.length > 0) {
546
+ console.error(
547
+ `❌ ${stale.join(' and ')} ${
548
+ stale.length > 1 ? 'are' : 'is'
549
+ } stale — run: node scripts/build-agents-md.mjs`,
550
+ )
184
551
  process.exit(1)
185
552
  }
186
- console.log(`✅ AGENTS.md is up to date (${instructions.length} instruction sections)`)
553
+ console.log(
554
+ `✅ AGENTS.md and AGENTS.global.md are up to date (${sharedCount} shared instruction sections)`,
555
+ )
187
556
  return
188
557
  }
189
558
 
190
- writeFileSync(AGENTS_MD_PATH, content, 'utf8')
191
- const lines = content.split('\n').length
559
+ writeFileSync(AGENTS_GLOBAL_MD_PATH, global, 'utf8')
560
+ writeFileSync(AGENTS_MD_PATH, project, 'utf8')
192
561
  console.log(
193
- `✅ Generated AGENTS.md (${instructions.length} instruction sections, ${lines} lines)`,
562
+ `✅ Generated AGENTS.md (${logicalLineCount(project)} lines) + AGENTS.global.md ` +
563
+ `(${logicalLineCount(global)} lines, ${sharedCount} shared instruction sections)`,
194
564
  )
195
565
  }
196
566
 
197
- main()
567
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
568
+ main()
569
+ }