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
@@ -29,6 +29,8 @@ import { homedir } from 'node:os'
29
29
  import { basename, dirname, isAbsolute, join, resolve } from 'node:path'
30
30
  import { fileURLToPath } from 'node:url'
31
31
 
32
+ import { listV2UnsupportedFeatures } from '../src/pantheon/v2-unsupported.mjs'
33
+
32
34
  // ---------------------------------------------------------------------------
33
35
  // Paths
34
36
  // ---------------------------------------------------------------------------
@@ -88,15 +90,25 @@ function parseArgs(argv) {
88
90
  args.target = process.cwd()
89
91
  }
90
92
 
91
- if (args.profile === 'auto') {
92
- args.profile = args.target.includes('pantheon-sandbox') ? 'sandbox' : 'global'
93
- }
94
-
95
93
  // Resolve to absolute
96
94
  if (!args.target.startsWith('/')) {
97
95
  args.target = join(process.cwd(), args.target)
98
96
  }
99
97
 
98
+ if (args.profile === 'auto') {
99
+ const target = resolve(args.target)
100
+ // A checkout is not a managed OpenCode installation: its repository
101
+ // config intentionally contains only development MCPs. Use the lite
102
+ // policy for `npm run doctor` so a fresh checkout does not require a
103
+ // user's global MCP registry or pre-existing venv.
104
+ args.profile =
105
+ target === resolve(ROOT)
106
+ ? 'lite'
107
+ : args.target.includes('pantheon-sandbox')
108
+ ? 'sandbox'
109
+ : 'global'
110
+ }
111
+
100
112
  return args
101
113
  }
102
114
 
@@ -159,7 +171,7 @@ function showHelp() {
159
171
  C. Permission Checks — frontmatter validation
160
172
  D. OpenCode Status — OpenCode artifact layout
161
173
  E. Git Status — uncommitted changes
162
- F. Runtime Layer — venv python + pinned pip dependencies
174
+ F. Runtime Layer — runtime Python + pinned pip dependencies
163
175
  H3. Plugin Drift — registered plugin version vs this package
164
176
  K. Node Runtime — node:sqlite availability for pantheon_cost
165
177
 
@@ -818,12 +830,14 @@ function checkGitStatus(args) {
818
830
  }
819
831
 
820
832
  // ---------------------------------------------------------------------------
821
- // Check F: Runtime Layer — Venv, Python & pinned dependencies
833
+ // Check F: Runtime Layer — Runtime Python & pinned dependencies
822
834
  // ---------------------------------------------------------------------------
823
835
  // The MCP servers run on the venv Python under ~/.config/opencode/.venv
824
- // (same resolution as scripts/install-mcp.mjs). This layer verifies the venv
825
- // exists, the interpreter responds, and the installed packages satisfy the
826
- // EXACT pins in src/mcp/requirements-mcp.txt and requirements-vision.txt
836
+ // (same resolution as scripts/install-mcp.mjs) after installation. A
837
+ // repository checkout/CI job is not an installed target, however, so it may
838
+ // intentionally have no venv. In that case the doctor uses PANTHEON_PYTHON or
839
+ // python3 and still validates the exact pins. Missing or mismatched packages
840
+ // remain blocking errors; the fallback never makes the runtime check advisory
827
841
  // (issue #18 / #21).
828
842
 
829
843
  /** Parse `pip freeze` output into a normalized name → version map. */
@@ -906,31 +920,53 @@ function checkRequirementLine(line, installed) {
906
920
  }
907
921
  }
908
922
 
923
+ /** Classify whether a required runtime file is available to the doctor. */
924
+ export function classifyRequiredRuntimeFile(exists) {
925
+ return exists ? 'present' : 'error'
926
+ }
927
+
928
+ /** Validate a required runtime requirements file without downgrading absence. */
929
+ export function validateRequiredRuntimeFile(filePath) {
930
+ const present = classifyRequiredRuntimeFile(existsSync(filePath)) === 'present'
931
+ return {
932
+ ok: present,
933
+ message: present
934
+ ? `requirements file present: ${filePath}`
935
+ : `requirements file not found: ${filePath}`,
936
+ }
937
+ }
938
+
909
939
  function checkVenvLayer(args) {
910
- section('F. Runtime Layer — Venv, Python & Dependencies')
940
+ section('F. Runtime Layer — Runtime Python & Dependencies')
911
941
 
912
- const venvPython = resolveRuntimePython(args)
942
+ const runtimePython = resolveRuntimePython(args)
913
943
 
914
- if (!existsSync(venvPython)) {
915
- if (args.profile === 'lite') {
916
- info(`Venv python not found — runtime layer skipped for lite profile`)
944
+ if (!isAbsolute(runtimePython) || !existsSync(runtimePython)) {
945
+ if (runtimePython === 'python3') {
946
+ info('No dedicated .venv found — validating the Python interpreter on PATH')
917
947
  } else {
918
- error(`Venv python not found: ${venvPython} (run \`pantheon-opencode init\` to create it)`)
948
+ error(
949
+ `Runtime Python not found: ${runtimePython} (set PANTHEON_PYTHON or run \`pantheon-opencode init\` to create a .venv)`,
950
+ )
951
+ return
919
952
  }
920
- return
921
953
  }
922
- pass(`Venv python exists: ${venvPython}`)
954
+ if (isAbsolute(runtimePython) && existsSync(runtimePython)) {
955
+ pass(`Runtime Python exists: ${runtimePython}`)
956
+ }
923
957
 
924
- const probe = spawn(venvPython, ['-c', 'import sys; print(sys.version.split()[0])'])
958
+ const probe = spawn(runtimePython, ['-c', 'import sys; print(sys.version.split()[0])'])
925
959
  if (probe.status !== 0) {
926
- error(`Venv python does not respond (exit ${probe.status}): ${probe.stderr}`)
960
+ error(
961
+ `Runtime Python does not respond (exit ${probe.status}): ${probe.stderr || 'interpreter is unavailable'}`,
962
+ )
927
963
  return
928
964
  }
929
- pass(`Venv python responds (Python ${probe.stdout})`)
965
+ pass(`Runtime Python responds (Python ${probe.stdout})`)
930
966
 
931
- const freeze = spawn(venvPython, ['-m', 'pip', 'freeze'])
967
+ const freeze = spawn(runtimePython, ['-m', 'pip', 'freeze'])
932
968
  if (freeze.status !== 0) {
933
- warn(`pip freeze failed (exit ${freeze.status}) — cannot verify pinned versions`)
969
+ error(`pip freeze failed (exit ${freeze.status}) — cannot verify pinned versions`)
934
970
  return
935
971
  }
936
972
  const installed = parsePipFreeze(freeze.stdout)
@@ -940,8 +976,9 @@ function checkVenvLayer(args) {
940
976
  join(ROOT, 'src', 'mcp', 'requirements-vision.txt'),
941
977
  ]
942
978
  for (const reqFile of reqFiles) {
943
- if (!existsSync(reqFile)) {
944
- warn(`requirements file not found: ${reqFile}`)
979
+ const requirement = validateRequiredRuntimeFile(reqFile)
980
+ if (!requirement.ok) {
981
+ error(requirement.message)
945
982
  continue
946
983
  }
947
984
  const base = basename(reqFile)
@@ -965,7 +1002,7 @@ function checkVenvLayer(args) {
965
1002
  /**
966
1003
  * Return whether *dir* contains an executable code-mode target.
967
1004
  *
968
- * Mirrors `_has_usable_scripts` in `scripts/code_mode_server.py`: a directory
1005
+ * Mirrors `_has_usable_scripts` in `scripts/code_mode.py`: a directory
969
1006
  * is only a valid resolver candidate when it is a real (non-symlink) directory
970
1007
  * containing a regular `.py`/`.sh` direct child. This prevents an empty project
971
1008
  * overlay from masking a lower-priority installation that actually ships
@@ -1073,7 +1110,9 @@ export function validateCodeModeManifest(codeModeDir) {
1073
1110
 
1074
1111
  const actualScripts = readdirSync(codeModeDir).filter(
1075
1112
  (name) =>
1076
- (name.endsWith('.py') || name.endsWith('.sh')) && statSync(join(codeModeDir, name)).isFile(),
1113
+ !name.startsWith('eval-') &&
1114
+ (name.endsWith('.py') || name.endsWith('.sh')) &&
1115
+ statSync(join(codeModeDir, name)).isFile(),
1077
1116
  )
1078
1117
  const missing = listed.filter((name) => !existsSync(join(codeModeDir, name)))
1079
1118
  const extra = actualScripts.filter((name) => !Object.hasOwn(data.scripts, name))
@@ -1104,12 +1143,32 @@ export function checkCodeModeManifest(args) {
1104
1143
  pass(`✅ Code-mode manifest valid: ${result.count}/${result.total} scripts verified`)
1105
1144
  }
1106
1145
 
1107
- /** Resolve the venv created by init for either a project or global install. */
1146
+ /**
1147
+ * Resolve the Python used by the runtime checks.
1148
+ *
1149
+ * Installed targets use the venv created by init. A checkout in CI does not
1150
+ * have that installation state, however, so the explicit PANTHEON_PYTHON
1151
+ * override and the PATH fallback make the contract reproducible without
1152
+ * silently skipping dependency validation.
1153
+ */
1108
1154
  export function resolveRuntimePython(args) {
1155
+ const env = args.env ?? process.env
1156
+ if (typeof env.PANTHEON_PYTHON === 'string' && env.PANTHEON_PYTHON.trim() !== '') {
1157
+ return env.PANTHEON_PYTHON.trim()
1158
+ }
1159
+
1109
1160
  const target = resolve(args.target)
1110
1161
  const isProject = existsSync(join(target, '.opencode'))
1111
- const runtimeRoot = isProject ? target : resolveOpenCodeConfigDir(args.env ?? process.env)
1112
- return join(runtimeRoot, '.venv', 'bin', 'python3')
1162
+ const runtimeRoot = isProject ? target : resolveOpenCodeConfigDir(env)
1163
+ const venvCandidates =
1164
+ process.platform === 'win32'
1165
+ ? [
1166
+ join(runtimeRoot, '.venv', 'Scripts', 'python.exe'),
1167
+ join(runtimeRoot, '.venv', 'Scripts', 'python'),
1168
+ ]
1169
+ : [join(runtimeRoot, '.venv', 'bin', 'python3'), join(runtimeRoot, '.venv', 'bin', 'python')]
1170
+ const existing = venvCandidates.find((candidate) => existsSync(candidate))
1171
+ return existing ?? 'python3'
1113
1172
  }
1114
1173
 
1115
1174
  /** Return relative script arguments as absolute paths using an MCP entry cwd. */
@@ -1373,6 +1432,153 @@ export function checkPluginVersionDrift(args) {
1373
1432
  return 'sync'
1374
1433
  }
1375
1434
 
1435
+ // ---------------------------------------------------------------------------
1436
+ // Check H4: V2 feature reduction (the surface a migrating user loses)
1437
+ // ---------------------------------------------------------------------------
1438
+
1439
+ /**
1440
+ * Whether an opencode.json registers the V2 generation of the Pantheon plugin.
1441
+ *
1442
+ * The V2 contract is the PLURAL `plugins` key; V1 is the singular `plugin` key.
1443
+ * So the key alone is not the test — a config can legitimately carry a
1444
+ * third-party entry in `plugins`. What identifies the generation is a Pantheon
1445
+ * V2 entry inside it. Matched on the `plugin-v2` marker so all three real
1446
+ * spellings are recognised: the canonical DIRECTORY (`src/plugin-v2`), an
1447
+ * absolute path to it, and the package export (`pantheon-opencode/plugin-v2`).
1448
+ *
1449
+ * @param {unknown} ref
1450
+ * @returns {boolean}
1451
+ */
1452
+ function isV2PluginRef(ref) {
1453
+ const candidate =
1454
+ typeof ref === 'string'
1455
+ ? ref
1456
+ : ref && typeof ref === 'object'
1457
+ ? (ref.path ?? ref.source ?? ref.id ?? ref.name)
1458
+ : undefined
1459
+ return typeof candidate === 'string' && candidate.includes('plugin-v2')
1460
+ }
1461
+
1462
+ /**
1463
+ * Whether any reachable config registers the V2 generation of the Pantheon
1464
+ * plugin. Split out of {@link collectV2UnsupportedFeatures} because "is the V2
1465
+ * generation registered" and "does the seed list anything" are TWO facts: the
1466
+ * first is about the user's config, the second about this repository's seed. A
1467
+ * single `features.length === 0` cannot tell them apart, and conflating them
1468
+ * let an emptied seed print "no V2 generation registered — nothing reduced".
1469
+ *
1470
+ * @param {{path:string, data:object}[]} configs
1471
+ * @returns {boolean}
1472
+ */
1473
+ export function isV2GenerationRegistered(configs) {
1474
+ return configs.some((cfg) => {
1475
+ const list = cfg.data?.plugins
1476
+ return Array.isArray(list) && list.some(isV2PluginRef)
1477
+ })
1478
+ }
1479
+
1480
+ /**
1481
+ * The V2 feature-reduction markers to report for a set of collected configs.
1482
+ *
1483
+ * Returns the shared seed verbatim when ANY reachable config registers the V2
1484
+ * generation, and an empty list otherwise. The strings come from
1485
+ * `src/pantheon/v2-unsupported.mjs` — the same seed `getUnsupportedFeatures()`
1486
+ * is built from — so doctor cannot report a reduction the plugin does not, or
1487
+ * miss one it does. Nothing is reworded here.
1488
+ *
1489
+ * An empty result is ambiguous on its own and deliberately not interpreted
1490
+ * here: ask {@link isV2GenerationRegistered} for the registration fact, or
1491
+ * {@link describeV2Reduction} to get both at once.
1492
+ *
1493
+ * @param {{path:string, data:object}[]} configs
1494
+ * @returns {string[]}
1495
+ */
1496
+ export function collectV2UnsupportedFeatures(configs) {
1497
+ return isV2GenerationRegistered(configs) ? listV2UnsupportedFeatures() : []
1498
+ }
1499
+
1500
+ /**
1501
+ * Decide what H4 prints, from the two distinct facts rather than from one list
1502
+ * length.
1503
+ *
1504
+ * Three cases, and the middle one used to be silently reported as the first:
1505
+ *
1506
+ * - not registered → info. A V1 install is a supported configuration with
1507
+ * nothing to reduce, and saying so plainly is correct.
1508
+ * - registered, seed EMPTY → warn. This is a packaging fault in THIS repo,
1509
+ * not an absence of reduction: the V2 surface really is reduced and the
1510
+ * report is missing. It must never claim "nothing reduced".
1511
+ * - registered, seed populated → warn, listing every marker.
1512
+ *
1513
+ * Pure and exported so the empty-seed branch is testable without stubbing the
1514
+ * frozen seed module. `status` is returned alongside the text so a caller never
1515
+ * has to recover the case by matching on the message.
1516
+ *
1517
+ * @param {{registered:boolean, features:string[]}} input
1518
+ * @returns {{status:'reported'|'empty-seed'|'no-v2-generation', level:'info'|'warn', message:string}}
1519
+ */
1520
+ export function describeV2Reduction({ registered, features }) {
1521
+ if (!registered) {
1522
+ return {
1523
+ status: 'no-v2-generation',
1524
+ level: 'info',
1525
+ message: 'No V2 plugin generation registered — nothing reduced',
1526
+ }
1527
+ }
1528
+ if (features.length === 0) {
1529
+ return {
1530
+ status: 'empty-seed',
1531
+ level: 'warn',
1532
+ message:
1533
+ 'Pantheon IS registered on the V2 plugin generation, but the shared ' +
1534
+ 'unsupported-feature seed is empty, so the reduction cannot be reported. ' +
1535
+ "That is a fault in this installation's reporting, not an absence of " +
1536
+ 'reduction: the V2 surface still drops the 3 goal tools and the ' +
1537
+ 'caller/target delegation matrix.',
1538
+ }
1539
+ }
1540
+ return {
1541
+ status: 'reported',
1542
+ level: 'warn',
1543
+ message:
1544
+ 'Pantheon is registered on the V2 plugin generation, which registers 3 tools ' +
1545
+ "(hashline_edit, pantheon_cost, pantheon_model) instead of the V1 surface's 6 plus " +
1546
+ `the BackgroundJobBoard and the caller/target delegation matrix. Unsupported: ${features.join(', ')}`,
1547
+ }
1548
+ }
1549
+
1550
+ /**
1551
+ * Report the V2 feature reduction through doctor's own output shape
1552
+ * (section + warn/info), so it lands in the health summary like every other
1553
+ * finding and never changes the exit code: a V2 install is a supported
1554
+ * configuration, not a broken one.
1555
+ *
1556
+ * @param {{target:string, env?:object}} args
1557
+ * @returns {'reported'|'empty-seed'|'no-v2-generation'|'no-config'}
1558
+ */
1559
+ export function checkV2UnsupportedFeatures(args) {
1560
+ section('H4. V2 Feature Reduction')
1561
+
1562
+ const configs = collectMcpConfigs(args)
1563
+ if (configs.length === 0) {
1564
+ info('No opencode.json was found — V2 feature reduction check skipped')
1565
+ return 'no-config'
1566
+ }
1567
+
1568
+ // Branch on REGISTRATION, not on the marker count: an empty seed on a
1569
+ // registered V2 generation is a reporting fault and must not be reported as
1570
+ // the reassuring "nothing reduced".
1571
+ const registered = isV2GenerationRegistered(configs)
1572
+ const verdict = describeV2Reduction({
1573
+ registered,
1574
+ features: collectV2UnsupportedFeatures(configs),
1575
+ })
1576
+ if (verdict.level === 'info') info(verdict.message)
1577
+ else warn(verdict.message)
1578
+
1579
+ return verdict.status
1580
+ }
1581
+
1376
1582
  // ---------------------------------------------------------------------------
1377
1583
  // Check G: AGENTS.md freshness
1378
1584
  // ---------------------------------------------------------------------------
@@ -1745,6 +1951,7 @@ async function main() {
1745
1951
  checkAgentsMdFreshness(args)
1746
1952
  checkSubagentDepthPlacement(args)
1747
1953
  checkPermissionTaskPresence(args)
1954
+ checkV2UnsupportedFeatures(args)
1748
1955
 
1749
1956
  printSummary(args.target)
1750
1957
 
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * agents-md.mjs — Dynamic AGENTS.md generator from canonical agent frontmatter
4
4
  *
5
- * Reads all agents/*.agent.md files, extracts name + description from frontmatter,
5
+ * Reads all canonical src/agents/*.md files, extracts name + description from frontmatter,
6
6
  * and generates AGENTS.md content dynamically. Eliminates hardcoded agent tables
7
7
  * in platform installers (claude.mjs, cursor.mjs, windsurf.mjs, cline.mjs, etc.).
8
8
  *
@@ -21,7 +21,7 @@
21
21
  import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
22
22
  import { dirname, join } from 'node:path'
23
23
  import { fileURLToPath } from 'node:url'
24
- import yaml from 'js-yaml'
24
+ import { load } from 'js-yaml'
25
25
 
26
26
  // ---------------------------------------------------------------------------
27
27
  // Paths
@@ -47,7 +47,7 @@ function parseAgentFrontmatter(content) {
47
47
  if (!match) return null
48
48
 
49
49
  try {
50
- const fm = yaml.load(match[1])
50
+ const fm = load(match[1])
51
51
  if (!fm || typeof fm !== 'object' || !fm.name) return null
52
52
  return {
53
53
  name: String(fm.name),
@@ -64,7 +64,7 @@ function parseAgentFrontmatter(content) {
64
64
  // ---------------------------------------------------------------------------
65
65
 
66
66
  /**
67
- * Read all canonical agents/*.agent.md files, parse frontmatter,
67
+ * Read all canonical src/agents/*.md files, parse frontmatter,
68
68
  * and return an array of agent metadata objects sorted alphabetically by name.
69
69
  *
70
70
  * Skips files that don't match the `*.agent.md` pattern, files without
@@ -159,7 +159,7 @@ export function spinner(message) {
159
159
  }
160
160
  }
161
161
 
162
- export function printSummary(target, platforms, stats) {
162
+ export function printSummary(target, _platforms, stats) {
163
163
  if (_quiet) return
164
164
 
165
165
  const S = strings()