opencode-agent-skills-md 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.beads/.local_version +1 -0
  2. package/.beads/README.md +81 -0
  3. package/.beads/config.yaml +61 -0
  4. package/.beads/deletions.jsonl +1 -0
  5. package/.beads/issues.jsonl +64 -0
  6. package/.beads/metadata.json +4 -0
  7. package/.gitattributes +3 -0
  8. package/.github/CODEOWNERS +1 -0
  9. package/.github/copilot-instructions.md +78 -0
  10. package/.github/dependabot.yml +13 -0
  11. package/.github/workflows/release.yml +51 -0
  12. package/.opencode/command/test-compaction.md +9 -0
  13. package/.opencode/command/test-find-skills.md +7 -0
  14. package/.opencode/command/test-read-skill-file.md +14 -0
  15. package/.opencode/command/test-run-skill-script.md +13 -0
  16. package/.opencode/command/test-skills.md +14 -0
  17. package/.opencode/command/test-use-skill.md +10 -0
  18. package/.opencode/skills/git-helper/SKILL.md +65 -0
  19. package/.opencode/skills/test-skill/SKILL.md +43 -0
  20. package/.opencode/skills/test-skill/example-config.json +16 -0
  21. package/.opencode/skills/test-skill/helper-docs.md +29 -0
  22. package/.opencode/skills/test-skill/scripts/echo-args +14 -0
  23. package/.opencode/skills/test-skill/scripts/greet +6 -0
  24. package/AGENTS.md +43 -0
  25. package/CHANGELOG.md +178 -0
  26. package/Justfile +39 -0
  27. package/LICENSE +9 -0
  28. package/README.md +189 -0
  29. package/openspec/changes/archive/2026-06-14-skills-core-decouple/specs/core-decoupling/spec.md +74 -0
  30. package/openspec/changes/archive/2026-06-14-skills-core-decouple/tasks.md +64 -0
  31. package/openspec/changes/archive/2026-06-14-skills-core-decouple/verify-report.md +75 -0
  32. package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/apply-progress.md +136 -0
  33. package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/archive-report.md +77 -0
  34. package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/design.md +89 -0
  35. package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/proposal.md +65 -0
  36. package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/specs/core-decoupling/spec.md +77 -0
  37. package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/tasks.md +65 -0
  38. package/openspec/changes/archive/2026-06-17-fix-skill-loading-regression/verify-report.md +165 -0
  39. package/openspec/specs/core-decoupling/spec.md +110 -0
  40. package/package.json +35 -0
  41. package/packages/core/package.json +30 -0
  42. package/packages/core/src/content.d.ts +16 -0
  43. package/packages/core/src/content.ts +30 -0
  44. package/packages/core/src/debug.ts +16 -0
  45. package/packages/core/src/discovery.d.ts +86 -0
  46. package/packages/core/src/discovery.ts +257 -0
  47. package/packages/core/src/index.d.ts +20 -0
  48. package/packages/core/src/index.ts +55 -0
  49. package/packages/core/src/match.d.ts +19 -0
  50. package/packages/core/src/match.ts +75 -0
  51. package/packages/core/src/parse.d.ts +26 -0
  52. package/packages/core/src/parse.ts +141 -0
  53. package/packages/core/src/scripts.d.ts +17 -0
  54. package/packages/core/src/scripts.ts +79 -0
  55. package/packages/core/src/search.d.ts +83 -0
  56. package/packages/core/src/search.ts +188 -0
  57. package/packages/core/src/types.d.ts +82 -0
  58. package/packages/core/src/types.ts +131 -0
  59. package/packages/core/src/walk.ts +109 -0
  60. package/packages/core/tests/agnostic.test.ts +346 -0
  61. package/packages/core/tests/content.test.ts +65 -0
  62. package/packages/core/tests/discovery.test.ts +370 -0
  63. package/packages/core/tests/package-boundary.test.ts +310 -0
  64. package/packages/core/tests/parse-trigger.test.ts +282 -0
  65. package/packages/core/tests/search.test.ts +374 -0
  66. package/packages/core/tests/subpath.test.ts +87 -0
  67. package/packages/core/tsconfig.json +10 -0
  68. package/packages/opencode-agent-skills-md/package.json +42 -0
  69. package/packages/opencode-agent-skills-md/rolldown.config.js +48 -0
  70. package/packages/opencode-agent-skills-md/src/cli/config.ts +522 -0
  71. package/packages/opencode-agent-skills-md/src/cli/install.ts +111 -0
  72. package/packages/opencode-agent-skills-md/src/cli/main.ts +201 -0
  73. package/packages/opencode-agent-skills-md/src/cli/real-fs.ts +51 -0
  74. package/packages/opencode-agent-skills-md/src/cli/status.ts +183 -0
  75. package/packages/opencode-agent-skills-md/src/cli/uninstall.ts +157 -0
  76. package/packages/opencode-agent-skills-md/src/host.ts +119 -0
  77. package/packages/opencode-agent-skills-md/src/index.ts +25 -0
  78. package/packages/opencode-agent-skills-md/src/plugin.ts +343 -0
  79. package/packages/opencode-agent-skills-md/src/sdk.ts +71 -0
  80. package/packages/opencode-agent-skills-md/src/tools.ts +373 -0
  81. package/packages/opencode-agent-skills-md/tests/cli-commands.test.ts +1423 -0
  82. package/packages/opencode-agent-skills-md/tests/e2e/startup-smoke.test.ts +66 -0
  83. package/packages/opencode-agent-skills-md/tests/fixtures/skills/home/.claude/skills/claude-user-only-skill/SKILL.md +8 -0
  84. package/packages/opencode-agent-skills-md/tests/fixtures/skills/home/.config/opencode/skills/shared-skill/SKILL.md +8 -0
  85. package/packages/opencode-agent-skills-md/tests/fixtures/skills/home/.config/opencode/skills/user-only-skill/SKILL.md +8 -0
  86. package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.claude/skills/claude-project-only-skill/SKILL.md +8 -0
  87. package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/go-tester/SKILL.md +12 -0
  88. package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/nested/team/nested-skill/SKILL.md +8 -0
  89. package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/rust-tester/SKILL.md +11 -0
  90. package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/scripted-skill/SKILL.md +8 -0
  91. package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/scripted-skill/bin/echo.sh +2 -0
  92. package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/scripted-skill/docs/reference.md +1 -0
  93. package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/shared-skill/SKILL.md +8 -0
  94. package/packages/opencode-agent-skills-md/tests/fixtures/skills/project/.opencode/skills/using-superpowers/SKILL.md +8 -0
  95. package/packages/opencode-agent-skills-md/tests/integration/helpers/mock-opencode.ts +114 -0
  96. package/packages/opencode-agent-skills-md/tests/integration/plugin.test.ts +316 -0
  97. package/packages/opencode-agent-skills-md/tests/integration/skill-discovery.test.ts +315 -0
  98. package/packages/opencode-agent-skills-md/tests/opencode/host.test.ts +179 -0
  99. package/packages/opencode-agent-skills-md/tests/opencode/plugin.test.ts +551 -0
  100. package/packages/opencode-agent-skills-md/tests/opencode/subpath.test.ts +66 -0
  101. package/packages/opencode-agent-skills-md/tests/opencode/tools.test.ts +213 -0
  102. package/packages/opencode-agent-skills-md/tests/package-boundary.test.ts +346 -0
  103. package/packages/opencode-agent-skills-md/tests/tools-security.test.ts +72 -0
  104. package/packages/opencode-agent-skills-md/tsconfig.build.json +11 -0
  105. package/packages/opencode-agent-skills-md/tsconfig.json +10 -0
  106. package/plans/001-ci-gate.md +177 -0
  107. package/plans/002-is-path-safe.md +243 -0
  108. package/plans/003-escape-prompts.md +310 -0
  109. package/plans/004-test-security-paths.md +228 -0
  110. package/plans/005-stop-swallowing-errors.md +246 -0
  111. package/plans/006-preserve-jsonc-commas.md +144 -0
  112. package/plans/007-write-before-purge.md +144 -0
  113. package/plans/008-reuse-walkdir-for-list-skill-files.md +164 -0
  114. package/plans/README.md +43 -0
  115. package/pnpm-workspace.yaml +6 -0
  116. package/tests/workspace.test.ts +367 -0
  117. package/tsconfig.json +15 -0
@@ -0,0 +1,43 @@
1
+ # Implementation Plans
2
+
3
+ Generated by the improve skill on 2026-07-03. Execute in the order below
4
+ unless dependencies say otherwise. Each executor: read the plan fully before
5
+ starting, honor its STOP conditions, and update your row when done.
6
+
7
+ ## Execution order & status
8
+
9
+ | Plan | Title | Priority | Effort | Depends on | Status |
10
+ |------|-------|----------|--------|------------|--------|
11
+ | 001 | CI gating before release | P1 | S | — | TODO |
12
+ | 002 | `isPathSafe` symlink hardening | P1 | S | — | DONE |
13
+ | 003 | Escape XML and shell boundaries | P1 | M | — | DONE |
14
+ | 004 | Tests for security-sensitive paths | P1 | M | 002, 003 | DONE |
15
+ | 005 | Surface swallowed discovery errors | P2 | S | — | DONE |
16
+ | 006 | Preserve commas inside JSONC string values | P1 | S | — | DONE |
17
+ | 007 | Write config before purging plugin-owned directories | P1 | S | — | DONE |
18
+ | 008 | Reuse the shared walker in `listSkillFiles` | P2 | S | — | DONE |
19
+
20
+ Status values: TODO | IN PROGRESS | DONE | BLOCKED (with one-line reason) | REJECTED (with one-line rationale)
21
+
22
+ ## Dependency notes
23
+
24
+ - 004 requires 002 and 003 because it adds tests that exercise the helpers
25
+ (`escapeXml`, `escapeShellArg`) and the hardened `isPathSafe` that those
26
+ plans introduce. Run 004 after both are done.
27
+ - Plans 006, 007, and 008 are independent of each other and of 001–005.
28
+ Execute 006 and 007 before 008 because they fix user-visible CLI correctness
29
+ bugs; 008 is characterization plus cleanup.
30
+ - All plans are otherwise independent and can execute in any order.
31
+
32
+ ## Findings considered and rejected
33
+
34
+ - **Lint/format tooling (finding 6)**: Not worth doing independently because
35
+ adding Biome or ESLint without a style guide and CI integration creates
36
+ noise. The CI workflow (plan 001) is a prerequisite for lint gates.
37
+ - **Collapse repeated discovery (finding 7)**: Higher effort (M) for a purely
38
+ performance-oriented fix. Worth doing after the security/correctness surface
39
+ is hardened. Deferred.
40
+ - **Package entrypoint / dependency drift**: Not substantiated by the live
41
+ code — entrypoints are consistent across both packages.
42
+ - **Triple-discovery claim**: Overcounted. It's double discovery per tool call,
43
+ not triple. Still a real performance issue but lower priority.
@@ -0,0 +1,6 @@
1
+ packages:
2
+ - "packages/*"
3
+
4
+ allowBuilds:
5
+ esbuild: true
6
+ msgpackr-extract: true
@@ -0,0 +1,367 @@
1
+ /**
2
+ * Workspace boundary contract for the `opencode-agent-skills-md` repo root.
3
+ *
4
+ * PR 3 of `split-core-opencode-packages` turns the repo root into a pure
5
+ * pnpm workspace manifest: no source, no fixtures, no root-level build
6
+ * config. The two real packages live under `packages/core/` and
7
+ * `packages/opencode-agent-skills-md/`. This test pins the contracts that
8
+ * prove the consolidation actually happened:
9
+ *
10
+ * 1. Root manifest is a workspace manifest (private, no exports of its own,
11
+ * scripts delegate to packages via `pnpm -r`).
12
+ * 2. Legacy root sources (`src/`, root `tests/fixtures/`, root
13
+ * `rolldown.config.js`, root `tsconfig.build.json`) are gone — they
14
+ * are now owned by the per-package directories.
15
+ * 3. Docs (README, CHANGELOG, Justfile, AGENTS.md) route users to the
16
+ * correct package for each harness and reflect the workspace
17
+ * structure.
18
+ * 4. Both packages resolve from the repo root through the pnpm
19
+ * workspace link (the symlink pnpm install wires into
20
+ * `node_modules/`).
21
+ *
22
+ * The test runs from the repo root's `tests/` directory, so the relative
23
+ * paths use `..` to resolve against the repo itself.
24
+ */
25
+
26
+ import assert from "node:assert/strict";
27
+ import { existsSync } from "node:fs";
28
+ import { readFile, stat } from "node:fs/promises";
29
+ import { createRequire } from "node:module";
30
+ import { spawnSync } from "node:child_process";
31
+ import * as path from "node:path";
32
+ import { describe, test } from "node:test";
33
+
34
+ const require = createRequire(import.meta.url);
35
+ const REPO_ROOT = path.resolve(import.meta.dirname, "..");
36
+
37
+ describe("opencode-agent-skills-md workspace root", () => {
38
+ test("root package.json is a private workspace manifest with no exports of its own", async () => {
39
+ const pkgPath = path.join(REPO_ROOT, "package.json");
40
+ const raw = await readFile(pkgPath, "utf8");
41
+ const manifest = JSON.parse(raw) as Record<string, unknown>;
42
+
43
+ // Workspace roots must be private — pnpm treats them as metadata
44
+ // containers, not packages to publish. The plugin package
45
+ // (`opencode-agent-skills-md`) remains the installable artifact.
46
+ assert.equal(
47
+ manifest.private,
48
+ true,
49
+ "root package.json must be private (it is a workspace manifest, not a publishable package)",
50
+ );
51
+
52
+ // The root manifest must not pretend to expose an entrypoint — the
53
+ // packages under `packages/*` own those exports. Carrying a stale
54
+ // `main`/`exports` would invite consumers to import the workspace
55
+ // by mistake.
56
+ assert.equal(
57
+ manifest.main,
58
+ undefined,
59
+ "root package.json must not declare a `main` (the packages own their own entrypoints)",
60
+ );
61
+ assert.equal(
62
+ manifest.exports,
63
+ undefined,
64
+ "root package.json must not declare `exports` (the packages own their own entrypoints)",
65
+ );
66
+
67
+ // Version + author metadata is fine to keep at the root.
68
+ assert.equal(typeof manifest.version, "string", "root package.json must carry a version string");
69
+ });
70
+
71
+ test("root scripts delegate to packages via `pnpm -r` so a single command covers both packages", async () => {
72
+ const raw = await readFile(path.join(REPO_ROOT, "package.json"), "utf8");
73
+ const manifest = JSON.parse(raw) as Record<string, unknown>;
74
+ const scripts = (manifest.scripts ?? {}) as Record<string, string>;
75
+
76
+ for (const name of ["build", "test", "typecheck"]) {
77
+ const script = scripts[name];
78
+ assert.ok(
79
+ typeof script === "string" && script.length > 0,
80
+ `root scripts.${name} must be defined`,
81
+ );
82
+ assert.match(
83
+ script,
84
+ /pnpm\s+-r\b/,
85
+ `root scripts.${name} must delegate to packages via \`pnpm -r\`, got: ${script}`,
86
+ );
87
+ }
88
+ });
89
+
90
+ test("root package.json declares a workspace itself via pnpm-workspace.yaml", async () => {
91
+ // Sanity check: the pnpm workspace declaration exists and lists the
92
+ // two expected package globs. pnpm-workspace.yaml is the source of
93
+ // truth for "this is a workspace".
94
+ const raw = await readFile(path.join(REPO_ROOT, "pnpm-workspace.yaml"), "utf8");
95
+ assert.match(
96
+ raw,
97
+ /packages:\s*\n\s*-\s+["']packages\/\*["']/,
98
+ "pnpm-workspace.yaml must declare the `packages/*` glob so pnpm wires the two packages into the workspace",
99
+ );
100
+ });
101
+
102
+ test("legacy root sources and build config are removed (packages now own them)", async () => {
103
+ // These were the legacy "root owns the build" surfaces from the
104
+ // pre-split layout. After PR 3 each one has a per-package home:
105
+ // - src/core/index.ts -> packages/core/src/index.ts
106
+ // - src/opencode/{4 files} -> packages/opencode-agent-skills-md/src/{4 files}
107
+ // - rolldown.config.js -> packages/opencode-agent-skills-md/rolldown.config.js
108
+ // - tsconfig.build.json -> packages/opencode-agent-skills-md/tsconfig.build.json
109
+ // - tests/fixtures/skills/** -> packages/opencode-agent-skills-md/tests/fixtures/skills/**
110
+ // The root sources/test config that remain (workspace manifest,
111
+ // AGENTS.md, README, etc.) are non-source artifacts.
112
+ for (const legacyPath of [
113
+ "src",
114
+ "rolldown.config.js",
115
+ "tsconfig.build.json",
116
+ "tests/fixtures",
117
+ ]) {
118
+ const fullPath = path.join(REPO_ROOT, legacyPath);
119
+ assert.ok(
120
+ !existsSync(fullPath),
121
+ `legacy root path must be removed by PR 3, but still exists at ${fullPath}`,
122
+ );
123
+ }
124
+
125
+ // The root `tsconfig.json` survives (it is used as a base for the
126
+ // per-package tsconfigs) — confirm it still exists and points at
127
+ // something reasonable.
128
+ const rootTsconfig = path.join(REPO_ROOT, "tsconfig.json");
129
+ assert.ok(existsSync(rootTsconfig), "root tsconfig.json must survive as the base for per-package tsconfigs");
130
+ const rootTsconfigRaw = await readFile(rootTsconfig, "utf8");
131
+ assert.match(
132
+ rootTsconfigRaw,
133
+ /"strict"\s*:\s*true/,
134
+ "root tsconfig.json must keep the strict-mode compiler options",
135
+ );
136
+ });
137
+
138
+ test("root src/core/index.ts compatibility shim has been removed (all callers use the workspace import now)", async () => {
139
+ // The shim only existed to keep legacy relative imports resolvable
140
+ // during PR 1+2; PR 3 deletes it because there are no more legacy
141
+ // callers (every consumer resolves `opencode-agent-skills-md-core`
142
+ // through the workspace link).
143
+ const shimPath = path.join(REPO_ROOT, "src", "core", "index.ts");
144
+ assert.ok(
145
+ !existsSync(shimPath),
146
+ `legacy compatibility shim must be removed by PR 3, but still exists at ${shimPath}`,
147
+ );
148
+ });
149
+
150
+ test("README routes users to the correct package for each harness type", async () => {
151
+ const readme = await readFile(path.join(REPO_ROOT, "README.md"), "utf8");
152
+
153
+ // Both package names must appear so consumers can find each one.
154
+ assert.match(
155
+ readme,
156
+ /opencode-agent-skills-md-core/,
157
+ "README must mention the standalone core package",
158
+ );
159
+ assert.match(
160
+ readme,
161
+ /\bopencode-agent-skills-md\b/,
162
+ "README must mention the OpenCode plugin package",
163
+ );
164
+
165
+ // The README must explicitly tell consumers which package to install
166
+ // for an OpenCode harness vs a custom/non-OpenCode harness. This is
167
+ // the R-3 routing scenario from the spec.
168
+ assert.match(
169
+ readme,
170
+ /OpenCode/i,
171
+ "README must still describe the OpenCode plugin install path",
172
+ );
173
+ assert.match(
174
+ readme,
175
+ /(custom harness|portable engine|standalone|non-OpenCode|without pulling the OpenCode SDK)/i,
176
+ "README must explain how to consume the standalone core package for custom harnesses",
177
+ );
178
+
179
+ // The legacy "Programmatic subpath exports" section that pointed
180
+ // users at `opencode-agent-skills-md/core` must be gone — the spec
181
+ // (R4 REMOVED) explicitly retired that import path in favor of
182
+ // the standalone `opencode-agent-skills-md-core` package.
183
+ assert.doesNotMatch(
184
+ readme,
185
+ /opencode-agent-skills-md\/core/,
186
+ "README must not document the removed `opencode-agent-skills-md/core` subpath (spec R4 REMOVED)",
187
+ );
188
+ });
189
+
190
+ test("CHANGELOG records the workspace split under [Unreleased]", async () => {
191
+ const changelog = await readFile(path.join(REPO_ROOT, "CHANGELOG.md"), "utf8");
192
+
193
+ // The [Unreleased] section must mention the package split so anyone
194
+ // tracking the project knows the install surface changed.
195
+ const unreleasedMatch = changelog.match(/##\s*\[Unreleased\]([\s\S]*?)(?=\n##\s|\n$)/);
196
+ assert.ok(unreleasedMatch, "CHANGELOG.md must contain a populated [Unreleased] section");
197
+
198
+ const unreleased = unreleasedMatch![1]!;
199
+ assert.match(
200
+ unreleased,
201
+ /opencode-agent-skills-md-core/,
202
+ "CHANGELOG [Unreleased] must mention the new core package",
203
+ );
204
+ assert.match(
205
+ unreleased,
206
+ /(workspace|split|extract)/i,
207
+ "CHANGELOG [Unreleased] must describe the workspace split as a Changed/Added entry",
208
+ );
209
+ });
210
+
211
+ test("Justfile recipes align with the two-package workspace layout", async () => {
212
+ const justfile = await readFile(path.join(REPO_ROOT, "Justfile"), "utf8");
213
+
214
+ // `test` and `build` recipes must delegate to pnpm so both packages
215
+ // are covered; hard-coded `npm test` from a single package would
216
+ // silently skip the other. The exact pnpm flag shape varies (e.g.
217
+ // `pnpm -r run build`, `pnpm -r --workspace-concurrency=1 run build`,
218
+ // `pnpm test`) so we accept any `pnpm` invocation that ends in the
219
+ // right subcommand.
220
+ const testRecipe = justfile.match(/^test:\s*\n([\s\S]*?)(?=\n\n|\n[a-z]+\s*:|\n#|$)/m);
221
+ assert.ok(testRecipe, "Justfile must define a `test` recipe");
222
+ assert.match(
223
+ testRecipe![1]!,
224
+ /pnpm\b[\s\S]*\btest\b/,
225
+ `Justfile \`test\` recipe must delegate via pnpm so both packages run, got:\n${testRecipe![1]!}`,
226
+ );
227
+
228
+ const buildRecipe = justfile.match(/^build:\s*\n([\s\S]*?)(?=\n\n|\n[a-z]+\s*:|\n#|$)/m);
229
+ assert.ok(buildRecipe, "Justfile must define a `build` recipe");
230
+ assert.match(
231
+ buildRecipe![1]!,
232
+ /pnpm\b[\s\S]*\bbuild\b/,
233
+ `Justfile \`build\` recipe must delegate via pnpm so both packages build, got:\n${buildRecipe![1]!}`,
234
+ );
235
+ });
236
+
237
+ test("AGENTS.md repo structure section reflects the two-package layout", async () => {
238
+ const agents = await readFile(path.join(REPO_ROOT, "AGENTS.md"), "utf8");
239
+
240
+ // The repo-structure section must point at the package directories,
241
+ // not the old `src/opencode/...` paths.
242
+ assert.match(
243
+ agents,
244
+ /packages\/core\/src/,
245
+ "AGENTS.md must reference packages/core/src in its repo structure section",
246
+ );
247
+ assert.match(
248
+ agents,
249
+ /packages\/opencode-agent-skills-md\/src/,
250
+ "AGENTS.md must reference packages/opencode-agent-skills-md/src in its repo structure section",
251
+ );
252
+ assert.doesNotMatch(
253
+ agents,
254
+ /^src\/opencode\//m,
255
+ "AGENTS.md must not describe legacy src/opencode/ paths (those moved into the plugin package)",
256
+ );
257
+
258
+ // The commands section must use `pnpm -r` for the umbrella commands
259
+ // so a single command covers both packages.
260
+ assert.match(
261
+ agents,
262
+ /pnpm\s+-r/,
263
+ "AGENTS.md must reference `pnpm -r` so the umbrella commands cover both packages",
264
+ );
265
+ });
266
+
267
+ test("both packages resolve from the repo root through the pnpm workspace link", async () => {
268
+ // pnpm install wires the two packages into `node_modules/` as
269
+ // symlinks so they resolve by name from the repo root. This is the
270
+ // end-to-end "the workspace link is alive" check — a stale lockfile
271
+ // or a typo in `pnpm-workspace.yaml` would surface here.
272
+ const coreResolved = require.resolve("opencode-agent-skills-md-core");
273
+ assert.match(
274
+ coreResolved,
275
+ /[\\/]packages[\\/]core[\\/]src[\\/]index\.ts$/,
276
+ `expected opencode-agent-skills-md-core to resolve to packages/core/src/index.ts, got: ${coreResolved}`,
277
+ );
278
+
279
+ const pluginResolved = require.resolve("opencode-agent-skills-md");
280
+ assert.match(
281
+ pluginResolved,
282
+ /[\\/]packages[\\/]opencode-agent-skills-md[\\/]src[\\/]index\.ts$/,
283
+ `expected opencode-agent-skills-md to resolve to packages/opencode-agent-skills-md/src/index.ts, got: ${pluginResolved}`,
284
+ );
285
+
286
+ assert.ok(coreResolved.startsWith(REPO_ROOT), `core resolution must live under the repo root: ${coreResolved}`);
287
+ assert.ok(
288
+ pluginResolved.startsWith(REPO_ROOT),
289
+ `plugin resolution must live under the repo root: ${pluginResolved}`,
290
+ );
291
+ });
292
+
293
+ test("both packages' source trees exist at the documented paths", async () => {
294
+ // Cross-check that the per-package sources are in place — without
295
+ // these the workspace link above would resolve to nothing.
296
+ const coreSrc = path.join(REPO_ROOT, "packages", "core", "src");
297
+ const pluginSrc = path.join(REPO_ROOT, "packages", "opencode-agent-skills-md", "src");
298
+ assert.ok(existsSync(coreSrc), `${coreSrc} must exist`);
299
+ assert.ok(existsSync(pluginSrc), `${pluginSrc} must exist`);
300
+
301
+ const coreSrcStat = await stat(coreSrc);
302
+ const pluginSrcStat = await stat(pluginSrc);
303
+ assert.ok(coreSrcStat.isDirectory(), `${coreSrc} must be a directory`);
304
+ assert.ok(pluginSrcStat.isDirectory(), `${pluginSrc} must be a directory`);
305
+ });
306
+
307
+ test("`pnpm run typecheck` from the repo root exits 0 (both packages typecheck clean)", () => {
308
+ // Triangulation: beyond the static `pnpm -r` script check above, the
309
+ // strongest behavior contract is that the documented root command
310
+ // actually works. A regression in the delegation (e.g. a typo, or a
311
+ // package silently missing a `typecheck` script) would surface here.
312
+ const result = spawnSync("pnpm", ["run", "typecheck"], {
313
+ cwd: REPO_ROOT,
314
+ encoding: "utf8",
315
+ timeout: 180_000,
316
+ });
317
+
318
+ assert.equal(
319
+ result.status,
320
+ 0,
321
+ `pnpm run typecheck from the repo root must exit 0. stdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
322
+ );
323
+ });
324
+
325
+ test("`pnpm test` from the repo root executes both packages' test suites end-to-end", () => {
326
+ // Triangulation: the umbrella `pnpm test` command must actually visit
327
+ // both workspace packages — a typo in the delegation script or a
328
+ // missing per-package `test` script would leave one suite silent.
329
+ //
330
+ // We deliberately do NOT assert exit code 0 here. The plugin package
331
+ // carries two pre-existing env-dependent test failures (21-vs-26
332
+ // user skills and `ast-grep` not installed; documented in the PR 2b
333
+ // apply progress). Those existed before PR 3 and are not part of
334
+ // this work — gating the root test on them would convert a known
335
+ // local-only issue into a workspace-level red. We assert the
336
+ // delegation reaches both packages; the per-package suites own the
337
+ // exit-code contract.
338
+ //
339
+ // `--no-bail` lets pnpm continue past the first failing package so
340
+ // the workspace contract test (this file) still runs even when the
341
+ // plugin's pre-existing failures fire. The overall `pnpm test` exit
342
+ // code is the conjunction of per-package results and the workspace
343
+ // test result, which is the correct gate.
344
+ const result = spawnSync("pnpm", ["-r", "--no-bail", "test"], {
345
+ cwd: REPO_ROOT,
346
+ encoding: "utf8",
347
+ timeout: 240_000,
348
+ });
349
+
350
+ const combined = `${result.stdout}\n${result.stderr}`;
351
+
352
+ // pnpm prefixes each package's output with its directory path
353
+ // (`packages/core test$ ...`, `packages/opencode-agent-skills-md test$ ...`).
354
+ // Both prefixes appearing in the output is the strongest signal that
355
+ // the delegation reached both packages.
356
+ assert.match(
357
+ combined,
358
+ /packages\/core\b/,
359
+ `pnpm test must execute the core package's test suite. output:\n${combined}`,
360
+ );
361
+ assert.match(
362
+ combined,
363
+ /packages\/opencode-agent-skills-md\b/,
364
+ `pnpm test must execute the plugin package's test suite. output:\n${combined}`,
365
+ );
366
+ });
367
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "noUncheckedIndexedAccess": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "types": ["node"]
13
+ },
14
+ "include": ["src"]
15
+ }