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.
Files changed (146) hide show
  1. package/.pantheon/code-mode/README.md +4 -2
  2. package/.pantheon/code-mode/checkpoint_session.py +138 -102
  3. package/.pantheon/code-mode/compress-inline.py +102 -57
  4. package/.pantheon/code-mode/manifest.json +2 -8
  5. package/AGENTS.global.md +182 -0
  6. package/AGENTS.md +10 -891
  7. package/CHANGELOG.md +343 -0
  8. package/README.md +577 -79
  9. package/README.pt-BR.md +345 -60
  10. package/bin/pantheon-init.mjs +26 -3
  11. package/commands/pantheon-audit.md +7 -1
  12. package/commands/pantheon-consolidate.md +1 -1
  13. package/commands/pantheon-deepwork.md +14 -3
  14. package/commands/pantheon-focus.md +17 -0
  15. package/commands/pantheon-optimize.md +2 -2
  16. package/commands/pantheon-vision.md +61 -12
  17. package/package.json +30 -11
  18. package/plugin.json +2 -2
  19. package/pyproject.toml +11 -16
  20. package/scripts/build-agents-md.mjs +376 -58
  21. package/scripts/changelog-merge.mjs +161 -0
  22. package/scripts/check-plugin-v2-coverage.mjs +213 -0
  23. package/scripts/ci-validate-yaml.py +7 -5
  24. package/scripts/{code_mode_server.py → code_mode.py} +119 -27
  25. package/scripts/cost.mjs +244 -41
  26. package/scripts/doctor.mjs +236 -29
  27. package/scripts/install/agents-md.mjs +4 -4
  28. package/scripts/install/cli-ui.mjs +1 -1
  29. package/scripts/install/config-migration.mjs +319 -6
  30. package/scripts/install/health-check.mjs +26 -37
  31. package/scripts/install/interactive.mjs +7 -7
  32. package/scripts/install/migrate.mjs +1 -1
  33. package/scripts/install/opencode-version.mjs +246 -4
  34. package/scripts/install/opencode.mjs +627 -179
  35. package/scripts/install/plugin.mjs +43 -9
  36. package/scripts/install/shared.mjs +120 -12
  37. package/scripts/install/vision-prompt.mjs +161 -0
  38. package/scripts/install-mcp.mjs +28 -9
  39. package/scripts/{mcp_persistence_server.py → mcp_persistence.py} +392 -78
  40. package/{src/mcp/mcp_resources_server.py → scripts/mcp_resources.py} +2 -2
  41. package/scripts/{memory_mcp_server.py → memory_mcp.py} +372 -198
  42. package/scripts/postinstall.mjs +0 -7
  43. package/scripts/probe-context-rehydrate.mjs +36 -13
  44. package/scripts/probe-pantheon-cost.mjs +71 -15
  45. package/scripts/prune.mjs +2 -2
  46. package/scripts/run-node-tests.mjs +189 -0
  47. package/scripts/sync-artifacts.mjs +26 -13
  48. package/scripts/sync-tui.mjs +24 -3
  49. package/scripts/test-opencode-v2-sandbox.sh +1913 -0
  50. package/scripts/validate-routing.mjs +43 -3
  51. package/scripts/validate-sandbox-install.mjs +138 -0
  52. package/scripts/versioning.mjs +13 -1
  53. package/scripts/vision-status.mjs +682 -0
  54. package/skills-lock.json +15 -16
  55. package/src/agents/aphrodite.md +20 -37
  56. package/src/agents/apollo.md +7 -3
  57. package/src/agents/athena.md +63 -35
  58. package/src/agents/demeter.md +8 -28
  59. package/src/agents/gaia.md +1 -1
  60. package/src/agents/hephaestus.md +10 -21
  61. package/src/agents/hermes.md +15 -38
  62. package/src/agents/iris.md +1 -1
  63. package/src/agents/mnemosyne.md +23 -60
  64. package/src/agents/nyx.md +1 -1
  65. package/src/agents/prometheus.md +16 -20
  66. package/src/agents/talos.md +10 -2
  67. package/src/agents/themis.md +5 -5
  68. package/src/agents/zeus.md +99 -130
  69. package/src/instructions/agent-return-format.instructions.md +18 -3
  70. package/src/instructions/backend-standards.instructions.md +2 -0
  71. package/src/instructions/frontend-standards.instructions.md +3 -1
  72. package/src/instructions/memory-protocol.instructions.md +11 -110
  73. package/src/instructions/zeus-anti-stall.instructions.md +14 -61
  74. package/src/instructions/zeus-communication-rules.instructions.md +0 -1
  75. package/src/instructions/zeus-council-synthesis.instructions.md +32 -260
  76. package/src/instructions/zeus-memory-operations.instructions.md +26 -0
  77. package/src/instructions/zeus-timeout-retry.instructions.md +40 -87
  78. package/src/mcp/{code_mode_server.py → code_mode.py} +119 -27
  79. package/src/mcp/eval_store.py +2 -2
  80. package/src/mcp/mcp_codemap_module.py +559 -129
  81. package/src/mcp/{mcp_persistence_server.py → mcp_persistence.py} +392 -78
  82. package/{scripts/mcp_resources_server.py → src/mcp/mcp_resources.py} +2 -2
  83. package/src/mcp/{memory_mcp_server.py → memory_mcp.py} +373 -250
  84. package/src/mcp/{pantheon_vision_server.py → pantheon_vision.py} +396 -40
  85. package/src/mcp/requirements-mcp.txt +10 -6
  86. package/src/mcp/requirements-vision.txt +4 -2
  87. package/src/pantheon/cost-command.ts +399 -101
  88. package/src/pantheon/delegation-enforce.ts +10 -1
  89. package/src/pantheon/model-command.ts +18 -7
  90. package/src/pantheon/presets.d.mts +0 -8
  91. package/src/pantheon/presets.mjs +5 -32
  92. package/src/pantheon/todo-enforcer.ts +1 -1
  93. package/src/pantheon/v2-bounded-read.ts +640 -0
  94. package/src/pantheon/v2-delegation-enforce.ts +214 -0
  95. package/src/pantheon/v2-execute-after.ts +186 -0
  96. package/src/pantheon/v2-tools.ts +429 -0
  97. package/src/pantheon/v2-unsupported.d.mts +11 -0
  98. package/src/pantheon/v2-unsupported.mjs +90 -0
  99. package/src/pantheon/v2-vision.ts +622 -0
  100. package/src/pantheon/vision-local.ts +531 -0
  101. package/src/pantheon/vision.ts +1161 -116
  102. package/src/plugin-v2.ts +1244 -288
  103. package/src/plugin.ts +12 -9
  104. package/src/plugins/tui/dist/server.d.ts +1 -2
  105. package/src/plugins/tui/dist/server.d.ts.map +1 -1
  106. package/src/plugins/tui/dist/tui-v2.d.ts +154 -0
  107. package/src/plugins/tui/dist/tui-v2.d.ts.map +1 -0
  108. package/src/plugins/tui/dist/tui-v2.js +659 -0
  109. package/src/plugins/tui/dist/tui-v2.js.map +1 -0
  110. package/src/plugins/tui/dist/tui.d.ts +85 -82
  111. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  112. package/src/plugins/tui/dist/tui.js +72 -59
  113. package/src/plugins/tui/index-v2.ts +6 -0
  114. package/src/plugins/tui/package-lock.json +5504 -1341
  115. package/src/plugins/tui/package.json +25 -15
  116. package/src/plugins/tui/src/index-v2.tsx +734 -0
  117. package/src/plugins/tui/src/index.tsx +106 -85
  118. package/src/plugins/tui/src/opencode-v2.d.ts +13 -0
  119. package/src/plugins/tui/tsconfig.build.json +5 -0
  120. package/src/plugins/tui/tsconfig.typecheck.json +5 -0
  121. package/src/plugins/tui/tsdown.config.ts +22 -2
  122. package/src/plugins/tui/tui-v2.ts +2 -0
  123. package/src/plugins/tui/tui.ts +3 -0
  124. package/src/routing.yml +11 -22
  125. package/src/skills/README.md +34 -39
  126. package/src/skills/agent-coordination/SKILL.md +7 -2
  127. package/src/skills/artifact-management/SKILL.md +12 -5
  128. package/src/skills/auto-continue/SKILL.md +54 -95
  129. package/src/skills/code-review-checklist/SKILL.md +5 -5
  130. package/src/skills/context-compression/SKILL.md +26 -879
  131. package/src/skills/session-goal/SKILL.md +14 -5
  132. package/src/skills/spec-driven-development/SKILL.md +3 -1
  133. package/src/skills/tdd-with-agents/SKILL.md +18 -10
  134. package/.pantheon/code-mode/eval-llm-judge.py +0 -255
  135. package/.pantheon/code-mode/eval-monte-carlo.py +0 -368
  136. package/.pantheon/code-mode/eval-run.py +0 -197
  137. package/.pantheon/code-mode/eval-static.py +0 -321
  138. package/.pantheon/code-mode/session-end-save.py +0 -186
  139. package/.pantheon/code-mode/session-end-save.sh +0 -18
  140. package/opencode.json +0 -58
  141. package/scripts/recover-zenodo-deposition.mjs +0 -146
  142. package/scripts/test-opencode-v1-v2-sandbox.sh +0 -862
  143. package/scripts/validate-zenodo-release.mjs +0 -154
  144. package/scripts/zenodo-artifact.mjs +0 -94
  145. package/scripts/zenodo-release-state.mjs +0 -53
  146. 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 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
 
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
- // 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.
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 STATIC_HEADER = `# Pantheon Agent System — OpenCode
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. Calls apollo for discovery, sends to themis for review. |
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. Calls apollo for discovery, sends to themis. |
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. Calls apollo for discovery, sends to themis. |
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 >80%, correctness, deprecation audit. Called by implementers; escalates blockers to zeus. |
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: Write failing test first, then implement
82
- - Coverage minimum: 80%
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
- * Only the `name` field is extracted (single-line scalar). The frontmatter
96
- * block itself is stripped from the embedded body.
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 } | null}
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
- body: content.slice(match[0].length).trimEnd() + '\n',
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 full AGENTS.md content.
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 generateAgentsMd(instructions) {
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
- '<!-- 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). -->',
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
- STATIC_HEADER.trimEnd(),
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 instructions) {
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
- return sections.join('\n').trimEnd() + '\n'
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 content = generateAgentsMd(instructions)
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 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`)
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(`✅ AGENTS.md is up to date (${instructions.length} instruction sections)`)
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(AGENTS_MD_PATH, content, 'utf8')
191
- const lines = content.split('\n').length
505
+ writeFileSync(AGENTS_GLOBAL_MD_PATH, global, 'utf8')
506
+ writeFileSync(AGENTS_MD_PATH, project, 'utf8')
192
507
  console.log(
193
- `✅ Generated AGENTS.md (${instructions.length} instruction sections, ${lines} lines)`,
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
- main()
513
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
514
+ main()
515
+ }