opencode-ship 1.1.4 → 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,54 @@
2
2
 
3
3
  All notable changes to `opencode-ship` are recorded here.
4
4
 
5
+ ## 1.1.7 — Stabilization and self-hosting (unreleased)
6
+
7
+ > Baseline: https://github.com/Viktorxyz/opencode-ship/issues/70.
8
+ > Corrective package: https://github.com/Viktorxyz/opencode-ship/issues/72.
9
+ > Authoritative plan: `docs/release/1.1.6-correction-plan.md`.
10
+
11
+ - **Truthful baseline before implementation.** `1.1.5` remains the current
12
+ npm `latest`, and `1.1.7` is the active unpublished correction. The immutable
13
+ tag `1.1.6` failed in `pack` before npm publish or GitHub Release. This entry
14
+ does not claim the `1.1.7` fixes have shipped or qualified, and `main` does
15
+ not yet contain its own installed `.opencode` control layer.
16
+ - **Bounded follow-up slices.** Runtime and release corrections, exact-version
17
+ self-host bootstrap, and automated exact-version update proposals will ship
18
+ as separate issue/worktree/PR lifecycles. Every automated install or update
19
+ pins `opencode-ship@<X.Y.Z>` and never resolves `@latest`.
20
+ - **Convergent merge and cleanup recovery.** A Ready manifest now reconciles a
21
+ PR that a maintainer already merged in GitHub instead of attempting a second
22
+ merge. Cleanup retries resume from their persisted stage, tolerate an
23
+ already-removed worktree, delete the branch with the existing CAS guard,
24
+ seal the manifest, and clear the Git-common-dir pending record.
25
+ - **Portable locks and bounded permissions.** Managed lock templates are
26
+ package-relative rather than npm-cache absolute paths. Build no longer
27
+ installs a broad `bash/* = allow` rule; destructive command denies remain
28
+ explicit while ordinary built-ins stay consumer-owned.
29
+ - **Release and package hardening.** The GitHub Release job checks out the
30
+ resolved release ref, `prepack` fails without installing missing tools,
31
+ `opencode-ship/core` publishes real declarations and the canonical package
32
+ version, and the stale `0.1.3` source literal is removed.
33
+ - **Release claims stay evidence-bound.** This entry becomes a released
34
+ changelog only after local verification, npm provenance, the GitHub Release,
35
+ qualification hashes, and a real self-host lifecycle all agree.
36
+
37
+ ## 1.1.6 — Qualification failed before publication
38
+
39
+ > Release workflow run: https://github.com/Viktorxyz/opencode-ship/actions/runs/33495352417.
40
+
41
+ - The immutable `1.1.6` tag failed in `pack` before npm publish or GitHub
42
+ Release. No npm package or GitHub Release was created.
43
+
44
+ ## 1.1.5 — `ship-plan` is product-only
45
+
46
+ > Branch: `feat/1.1.5-ship-plan-prompt`.
47
+
48
+ - **`ship-plan` prompt is product-only.** `1.1.4` shipped the `ship-plan` primary agent with a generic "ask clarifying questions when tradeoffs need the user's call" prompt and a "tell the user the file path and ask for confirmation before any edits" step. Both leaked workflow concerns into the chat: the model asked "open GitHub issues 0–6 or first Task 0 (commit /final)", "use this Tab (Build is for implementation)", and surfaced permission-glob internals ("`docs/superpowers/plans/` is deny; allowlist is `.opencode/plans/*.md`"). The Plan tab is the **product** half of the workflow; the user is there to talk about the product, not be taught the workflow. The rewrite replaces those lines with a strict contract: the prompt is allowed to ask only product questions (scope, UX, who it is for, what done looks like) and one final product question ("does the plan match the product you want?") before stopping. The prompt never asks how to run the work (issues vs Task N, subagent vs inline, Tab / Build, `ship-deliver`, "what next"), never mentions permission globs / deny lists / allowlists, never creates GitHub issues, never claims OpenCode's native `plan_exit`, and never offers to implement.
49
+ - **Handoff is a file, not a chat menu.** The Plan tab writes `.opencode/plans/<​filename>.md` and stops. A later Build session is told `implement this plan <path>` (or `implement issue N` for the Issue path). The Plan tab does not offer "which approach?" or "subagent vs inline" — those are upstream Superpowers choices that ship does not copy. If a PR needs `Closes #N`, that issue is created by Build (`delivery_issue`), not by Plan.
50
+ - **Subagent dispatch is internal.** `ship-plan` keeps its `task: explore` / `general` allowlist so it can dispatch read-only investigators and write scratch notes, but the prompt no longer asks the user whether to use subagents. The decision is made by the planning agent itself from the size of the request.
51
+ - **`tests/package/neutral-consumer.test.mjs` regression.** The post-init probe now asserts the new prompt lines are present and the old workflow-leaking lines are absent. The 1.1.3 stale-record removal path is unchanged, so `opencode-ship update` from prior 1.1.x installs still cleans the leftover `*` wildcards the same way it did before this release.
52
+
5
53
  ## 1.1.4 — `ship-plan` primary planning agent
6
54
 
7
55
  > Branch: `feat/1.1.4-ship-plan`.
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  > npm-distributed OpenCode installer and delivery plugin: a single command materialises the lifecycle plugin, reviewer/verifier agents, and skills into any consumer repository, with a recoverable lock and never silently overwrites managed files.
4
4
  >
5
- > **Status:** `1.1.4` is on `npm dist-tag latest` and adds a write-capable `ship-plan` primary planning agent. OpenCode's built-in lowercase `plan` Tab is now disabled by the installer (the native experimental Plan mode is env-only and cannot be activated from a repo-local install); `ship-plan` writes Markdown only under `.opencode/plans/*.md` and never touches source files. `1.1.3` supersedes the `1.1.2 stabilization` line by dropping the broken `permission: { "*": "deny" }` wildcard and adding a stale-pointer removal path so `update` cleans the orphan key on the first run. The `1.1.0` tag is immutable. Consumers of `opencode-delivery@0.1.x` should run `pnpm dlx opencode-ship@latest init` from the same checkout; migration recognises the legacy artifacts and adopts them when their bytes match.
5
+ > **Status:** `1.1.5` is the current `npm dist-tag latest`. It makes the `ship-plan` primary planning agent product-only: the agent asks about the product, writes `.opencode/plans/*.md`, and stops without leaking delivery-workflow choices into the planning conversation. The immutable tag `1.1.6` failed in `pack` before npm publish or GitHub Release; no npm package or GitHub Release was created. `1.1.7` is the active unpublished correction, tracked by the authoritative [`docs/release/1.1.6-correction-plan.md`](docs/release/1.1.6-correction-plan.md).
6
6
  >
7
- > Consumers should use `opencode-ship@1.1.0` (or `@latest`) until `1.1.1` ships. The previous `1.0.0` is on `next`; consumers who pinned to `1.0.x` will continue to receive updates on that channel.
7
+ > Use an exact package version for reproducible installs and all self-hosting work. Until `1.1.7` is published and qualified, the verified stable pin is `opencode-ship@1.1.5`; never resolve `@latest` inside an automated release or self-host update.
8
8
 
9
9
  ---
10
10
 
@@ -12,7 +12,7 @@
12
12
 
13
13
  ```sh
14
14
  # 1. Install managed files. No flags required.
15
- pnpm dlx opencode-ship@latest init
15
+ pnpm dlx opencode-ship@1.1.5 init
16
16
 
17
17
  # 2. Restart OpenCode in this repo.
18
18
 
@@ -70,11 +70,11 @@ The only source tree that ships in the npm tarball is `assets/`. Anything copied
70
70
  Once `opencode-ship` is published, consumers install with `pnpm dlx` (or `npx`) and never edit the file by hand:
71
71
 
72
72
  ```
73
- pnpm dlx opencode-ship@latest init # install managed files
74
- pnpm dlx opencode-ship@latest update # apply a packaged upgrade
75
- pnpm dlx opencode-ship@latest diff # preview what would change
76
- pnpm dlx opencode-ship@latest doctor # environment and lock audit
77
- pnpm dlx opencode-ship@latest uninstall # remove only the files still matching the lock
73
+ pnpm dlx opencode-ship@1.1.5 init # install managed files
74
+ pnpm dlx opencode-ship@1.1.5 update # apply a packaged upgrade
75
+ pnpm dlx opencode-ship@1.1.5 diff # preview what would change
76
+ pnpm dlx opencode-ship@1.1.5 doctor # environment and lock audit
77
+ pnpm dlx opencode-ship@1.1.5 uninstall # remove only the files still matching the lock
78
78
  ```
79
79
 
80
80
  If you want to try a pre-release tarball locally without publishing to npm:
@@ -109,13 +109,13 @@ These JSON Schemas are published and discoverable through the `exports` map:
109
109
 
110
110
  ### Legacy migration
111
111
 
112
- Existing consumers of `opencode-delivery@0.1.x` (commit-pinned shim) can run `pnpm dlx opencode-ship@latest init` from the same checkout. Migration recognises `.opencode/delivery.json`, `.opencode/delivery.lock.json`, the two canonical agents, and the generic plugin `.opencode/plugin/delivery.ts`, and adopts them when their bytes match. Legacy artifacts are preserved on disk so a downgrade remains possible; the installer does NOT modify Leo or any other consumer.
112
+ Existing consumers of `opencode-delivery@0.1.x` (commit-pinned shim) can run `pnpm dlx opencode-ship@1.1.5 init` from the same checkout. Migration recognises `.opencode/delivery.json`, `.opencode/delivery.lock.json`, the two canonical agents, and the generic plugin `.opencode/plugin/delivery.ts`, and adopts them when their bytes match. Legacy artifacts are preserved on disk so a downgrade remains possible; the installer does NOT modify Leo or any other consumer.
113
113
 
114
114
  `diff` is now strictly read-only; it reports every change but never writes to disk, even when the migration phase has a candidate seed-config to plant.
115
115
 
116
116
  ## Lifecycle
117
117
 
118
- 1. Begin a Build task: the delivery plugin immediately runs any queued post-merge cleanups. Failed cleanups are recorded in `ship.lock.json#cleanupPending` and retried at the next delivery task or plugin startup.
118
+ 1. Begin a Build task: the delivery plugin immediately runs any queued post-merge cleanups. Failed cleanups are recorded under `<git-common-dir>/opencode-ship/cleanup-pending.json` and retried at the next delivery task or plugin startup.
119
119
  2. Optional Deep Research checkpoint for non-trivial plans.
120
120
  3. Find or create exactly one issue per PR.
121
121
  4. Discover the default branch and fetch it.
@@ -130,7 +130,7 @@ Existing consumers of `opencode-delivery@0.1.x` (commit-pinned shim) can run `pn
130
130
  13. Push and wait for required remote CI checks.
131
131
  14. Mark the PR Ready and stop.
132
132
  15. Explicit "merge it" re-runs the freshness checks and performs the squash merge.
133
- 16. The plugin immediately invokes `delivery_cleanup`; failures leave `cleanupPending` for the next session.
133
+ 16. The plugin immediately invokes `delivery_cleanup`; failures persist their next cleanup stage in the Git common dir for the next session.
134
134
 
135
135
  ## Exit codes
136
136
 
@@ -158,7 +158,7 @@ The shipped artifact is built by esbuild (`scripts/build.mjs`); self-contained `
158
158
  ## Status and licensing
159
159
 
160
160
  - **License:** MIT. See `LICENSE`.
161
- - **Versioning:** SemVer. v0.2.0 is the first npm-distributed release. v0.3.0 is the installer foundation. v0.4.0 adds the profile-aware installer foundation (`--profile` flag, lock schema v2, profile precedence) that issue #18 requires. v0.5.0 ships the engineering profile content (triage + grill-with-docs SKILL.md placeholders) required by issue #20. v0.6.0 ships the durable plan artifact + Plan Mode permission integration required by issue #21. v0.7.0 ships the M3 task loop contract (run store, task brief, Spec/Quality verdicts, 3-round breaker, commit binding, compaction context) required by issue #22. v0.8.0 ships the Ready gate contract (parallel Standards/Spec + verifier + CI on one HEAD) required by issue #23. v0.9.0 ships the transition matrix smoke (engineering installs all assets, lock tracks the active profile) required by issue #24. v1.0.0 promotes the dogfooded 0.10.0 to stable. v1.1.0 removes the legacy core profile and ships the one-liner init + setup-ship-workflow skill. v1.1.1 is the first fully self-hosting release (see `docs/release/1.1.1-stabilization-plan.md`).
161
+ - **Versioning:** SemVer. v0.2.0 is the first npm-distributed release. v0.3.0 is the installer foundation. v0.4.0 adds the profile-aware installer foundation (`--profile` flag, lock schema v2, profile precedence) that issue #18 requires. v0.5.0 ships the engineering profile content (triage + grill-with-docs SKILL.md placeholders) required by issue #20. v0.6.0 ships the durable plan artifact + Plan Mode permission integration required by issue #21. v0.7.0 ships the M3 task loop contract (run store, task brief, Spec/Quality verdicts, 3-round breaker, commit binding, compaction context) required by issue #22. v0.8.0 ships the Ready gate contract (parallel Standards/Spec + verifier + CI on one HEAD) required by issue #23. v0.9.0 ships the transition matrix smoke (engineering installs all assets, lock tracks the active profile) required by issue #24. v1.0.0 promotes the dogfooded 0.10.0 to stable. v1.1.0 removes the legacy core profile and ships the one-liner init + setup-ship-workflow skill. v1.1.1 began the self-hosting correction line; the immutable `1.1.6` failed before publication and `1.1.7` is the planned unpublished correction (see `docs/release/1.1.6-correction-plan.md`).
162
162
  - **Compatibility:** the bundled plugin targets `@opencode-ai/plugin >= 1.15.5 < 2` and OpenCode `>= 1.15.5`.
163
163
 
164
164
  ## FAQ
@@ -1,6 +1,6 @@
1
1
  # Third-Party Notices
2
2
 
3
- `opencode-ship@1.1.4` ships the complete Matt Pocock and Superpowers
3
+ `opencode-ship@1.1.7` includes the complete Matt Pocock and Superpowers
4
4
  methodology under the MIT license, plus the Ship-owned installer,
5
5
  plugin, agents, and skills. The complete immutable pin list is in
6
6
  `vendor/sources.json`; the upstream snapshots are under
@@ -35,32 +35,36 @@ permission:
35
35
 
36
36
  # Ship Plan
37
37
 
38
- You are the primary planning agent for this repo. Use this Tab (Build is for implementation).
38
+ You are the primary planning agent. You talk about the product. You never talk about the workflow.
39
39
 
40
40
  ## What you do
41
41
 
42
- 1. Read the user's request and any context files they cite (`@file`).
43
- 2. Explore the codebase with `bash` (read-only git / ls / cat / gh issue / gh pr) and `task explore` agents. Never `cat` secrets.
44
- 3. Ask clarifying questions through `question` when tradeoffs need the user's call.
45
- 4. Write your plan to a single Markdown file under `.opencode/plans/<filename>.md`. Filename: lowercase, dashes, no spaces, ends in `.md` (e.g. `2026-08-20-customer-final-ux.md`).
46
- 5. Tell the user the file path and ask for confirmation before any edits after the first write.
42
+ 1. Read the user's request and any `@file` context they cite.
43
+ 2. Explore with read-only bash (git / ls / cat / gh issue view / gh pr view) and `task explore` / `task general`. Never `cat` secrets. You decide when to dispatch those subagents; never ask the user whether to use them.
44
+ 3. Ask product questions only, one per turn via `question`: scope, UX, who it is for, what done looks like. When you must choose, recommend in one sentence.
45
+ 4. Write the plan to `.opencode/plans/<​filename>.md`. Filename: lowercase, dashes, no spaces, ends in `.md` (e.g. `2026-08-20-customer-final-ux.md`).
46
+ 5. Tell the user the file path. Ask one question: whether the plan matches the product they want. Then stop.
47
47
 
48
48
  ## What you never do
49
49
 
50
- - Never edit source files, configs, tests, or anything outside `.opencode/plans/*.md`.
50
+ - Never edit anything outside `.opencode/plans/*.md`.
51
51
  - Never run mutating bash (`git commit`, `git push`, `npm install`, `rm`, `pnpm add`, …).
52
- - Never claim you have OpenCode's native `plan_exit` workflow. This is a replacement for the read-only native Plan Tab, not the experimental one.
53
- - Never invoke `task plan` or `task build`. Build runs the plan, not you.
52
+ - Never ask how to run the work: no GitHub issues vs Task N, no Tab / Build, no subagent vs inline, no `ship-deliver`, no "what next".
53
+ - Never mention permission globs, deny lists, allowlists, or why a path was denied.
54
+ - Never create GitHub issues. A later Build session does that if a PR needs `Closes #N`.
55
+ - Never claim OpenCode's native `plan_exit`. This is a replacement for the read-only native Plan Tab, not the experimental one.
56
+ - Never invoke `task plan` or `task build`. Build runs the plan, in a different session.
57
+ - Never offer to implement.
54
58
 
55
59
  ## Style
56
60
 
57
- - Write for the cheap builder model that will execute the plan. Be concrete: file paths, function names, the exact commit messages you'll commit to.
61
+ - Write for a later session that will be told `implement this plan <path>`. Be concrete: file paths, function names, the exact commit messages.
58
62
  - Prefer small, vertical slices over big-bang rewrites.
59
- - Quote the source lines you'll touch.
60
- - When you must choose, recommend and justify in one sentence. Don't enumerate "alternatives" in the file.
63
+ - Quote the source lines you will touch.
64
+ - Pick one approach in the file; do not enumerate alternatives.
61
65
 
62
66
  ## Hard rules
63
67
 
64
- - One question per turn via `question`. Don't batch.
68
+ - One question per turn via `question`. Product only.
65
69
  - If a write fails, surface the error verbatim. Do not retry silently.
66
- - Don't claim a file is written until `bash` confirms it.
70
+ - Do not claim a file is written until `bash` confirms it.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // opencode-ship CLI v1.1.4
2
+ // opencode-ship CLI v1.1.7
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __esm = (fn, res) => function __init() {
@@ -110,7 +110,7 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
110
110
  var PACKAGE_VERSION, TEMPLATE_SET;
111
111
  var init_version = __esm({
112
112
  "src/version.js"() {
113
- PACKAGE_VERSION = "1.1.4";
113
+ PACKAGE_VERSION = "1.1.7";
114
114
  TEMPLATE_SET = `v${PACKAGE_VERSION}`;
115
115
  }
116
116
  });
@@ -119,6 +119,7 @@ var init_version = __esm({
119
119
  var catalog_exports = {};
120
120
  __export(catalog_exports, {
121
121
  CATALOG: () => CATALOG,
122
+ PACKAGE_ROOT: () => PACKAGE_ROOT,
122
123
  PACKAGE_VERSION: () => PACKAGE_VERSION,
123
124
  TEMPLATE_SET_ID: () => TEMPLATE_SET_ID,
124
125
  filterCatalogByProfile: () => filterCatalogByProfile,
@@ -205,7 +206,7 @@ function validateCatalog({ catalog = CATALOG } = {}) {
205
206
  }
206
207
  return catalog;
207
208
  }
208
- var TEMPLATE_SET_ID, packageRoot, MATT_SKILLS, SUPER_SKILLS, ENGINEERING_AGENTS, ENGINEERING_COMMANDS, CATALOG, ALLOWED_KINDS;
209
+ var TEMPLATE_SET_ID, packageRoot, PACKAGE_ROOT, MATT_SKILLS, SUPER_SKILLS, ENGINEERING_AGENTS, ENGINEERING_COMMANDS, CATALOG, ALLOWED_KINDS;
209
210
  var init_catalog = __esm({
210
211
  "src/installer/catalog.js"() {
211
212
  init_package_root();
@@ -213,6 +214,7 @@ var init_catalog = __esm({
213
214
  init_profile();
214
215
  TEMPLATE_SET_ID = TEMPLATE_SET;
215
216
  packageRoot = resolvePackageRoot(import.meta.url);
217
+ PACKAGE_ROOT = packageRoot;
216
218
  MATT_SKILLS = [
217
219
  "engineering-workflow",
218
220
  "grilling",
@@ -1731,7 +1733,7 @@ init_catalog();
1731
1733
  init_version();
1732
1734
  import { existsSync as existsSync13 } from "node:fs";
1733
1735
  import { mkdir as mkdir6, readFile as readFile9, rename as rename5, unlink as unlink3, writeFile as writeFile6 } from "node:fs/promises";
1734
- import { dirname as dirname8, resolve as resolve12 } from "node:path";
1736
+ import { dirname as dirname8, relative as relative2, resolve as resolve12, sep as sep2 } from "node:path";
1735
1737
 
1736
1738
  // src/installer/planner.js
1737
1739
  init_catalog();
@@ -1867,8 +1869,8 @@ var FORBIDDEN_BASH_GLOBS_CONTROLLER = [
1867
1869
  "rm -rf *",
1868
1870
  "rm -rf /*"
1869
1871
  ];
1870
- function denyMap(globs) {
1871
- const out = { "*": "allow" };
1872
+ function denyMap(globs, allowByDefault = true) {
1873
+ const out = allowByDefault ? { "*": "allow" } : {};
1872
1874
  for (const g of globs) out[g] = "deny";
1873
1875
  return out;
1874
1876
  }
@@ -1893,7 +1895,7 @@ function rootPermissionMatrix() {
1893
1895
  "delivery-verifier": "allow"
1894
1896
  },
1895
1897
  bash: {
1896
- ...denyMap(FORBIDDEN_BASH_GLOBS),
1898
+ ...denyMap(FORBIDDEN_BASH_GLOBS, false),
1897
1899
  "rm *": "ask"
1898
1900
  }
1899
1901
  },
@@ -2734,11 +2736,11 @@ function parseTree(text, errors = [], options = ParseOptions.DEFAULT) {
2734
2736
  onValue({ type: getNodeType(value), offset, length, parent: currentParent, value });
2735
2737
  ensurePropertyComplete(offset + length);
2736
2738
  },
2737
- onSeparator: (sep2, offset, length) => {
2739
+ onSeparator: (sep3, offset, length) => {
2738
2740
  if (currentParent.type === "property") {
2739
- if (sep2 === ":") {
2741
+ if (sep3 === ":") {
2740
2742
  currentParent.colonOffset = offset;
2741
- } else if (sep2 === ",") {
2743
+ } else if (sep3 === ",") {
2742
2744
  ensurePropertyComplete(offset);
2743
2745
  }
2744
2746
  }
@@ -5249,7 +5251,7 @@ async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profil
5249
5251
  manager: {
5250
5252
  schemaVersion: CURRENT_LOCK_SCHEMA,
5251
5253
  name: "opencode-ship",
5252
- version: "1.1.4",
5254
+ version: "1.1.7",
5253
5255
  templateSet: TEMPLATE_SET_ID,
5254
5256
  profile: resolvedProfile,
5255
5257
  appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -5390,9 +5392,7 @@ function serializePlan(plan) {
5390
5392
  }
5391
5393
  function relativeTemplate(source) {
5392
5394
  if (typeof source !== "string") return source;
5393
- const prefix = `${process.cwd()}/`;
5394
- if (source.startsWith(prefix)) return source.slice(prefix.length);
5395
- return source;
5395
+ return relative2(PACKAGE_ROOT, source).split(sep2).join("/");
5396
5396
  }
5397
5397
 
5398
5398
  // src/installer/commands/doctor.js
package/dist/core.d.ts CHANGED
@@ -1 +1,583 @@
1
- export {};
1
+ /**
2
+ * Public type declarations for the opencode-ship package.
3
+ *
4
+ * The package ships plain JavaScript ESM sources; this declaration
5
+ * file gives TypeScript consumers the surface they need to typecheck
6
+ * `import` statements without falling back to `any`. Every public
7
+ * value export from `src/index.js` is declared here; the consumer
8
+ * fixture under `tests/fixtures/consumer.ts` enforces that.
9
+ */
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Primitives
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export type Sha = string;
16
+ export type Branch = string;
17
+ export type RepoSlug = `${string}/${string}`;
18
+ export type IssueNumber = number;
19
+ export type PullRequestNumber = number;
20
+
21
+ export type ContractVersion = 1;
22
+
23
+ export interface AdapterLock {
24
+ contractVersion: ContractVersion;
25
+ adapterSha256: Sha;
26
+ writtenAt: string;
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Lifecycle
31
+ // ---------------------------------------------------------------------------
32
+
33
+ export type LifecycleState =
34
+ | "issue-linked"
35
+ | "worktree-created"
36
+ | "draft-open"
37
+ | "validating"
38
+ | "ready"
39
+ | "merged"
40
+ | "cleanup-pending"
41
+ | "cleaned"
42
+ | "failed"
43
+ | "aborted";
44
+
45
+ export interface LifecycleTransition {
46
+ from: LifecycleState;
47
+ to: LifecycleState;
48
+ at: number;
49
+ reason?: string;
50
+ }
51
+
52
+ export interface Manifest {
53
+ schemaVersion: 2;
54
+ taskId: string;
55
+ repoIdentity: RepoSlug;
56
+ issueNumber: IssueNumber | null;
57
+ prNumber: PullRequestNumber | null;
58
+ baseBranch: Branch;
59
+ baseSha: Sha;
60
+ branch: Branch;
61
+ worktreePath: string | null;
62
+ lastPrHeadSha: Sha | null;
63
+ lastReviewerSha: Sha | null;
64
+ lastVerifierSha: Sha | null;
65
+ workflowId: string | null;
66
+ owner: string;
67
+ state: LifecycleState;
68
+ transitionLog: LifecycleTransition[];
69
+ fatalReason?: string;
70
+ createdAt: string;
71
+ updatedAt: string;
72
+ }
73
+
74
+ export interface CreateManifestInput {
75
+ taskId: string;
76
+ repoIdentity: RepoSlug;
77
+ issueNumber: IssueNumber | null;
78
+ baseBranch: Branch;
79
+ baseSha: Sha;
80
+ branch: Branch;
81
+ owner: string;
82
+ prNumber?: PullRequestNumber | null;
83
+ lastPrHeadSha?: Sha | null;
84
+ workflowId?: string | null;
85
+ }
86
+
87
+ export interface IssueSummary {
88
+ number: IssueNumber;
89
+ url: string;
90
+ state: "OPEN" | "CLOSED";
91
+ pullRequest: PullRequestSummary | null;
92
+ }
93
+
94
+ export interface PullRequestSummary {
95
+ number: PullRequestNumber;
96
+ url: string;
97
+ baseRefName: Branch;
98
+ headRefName: Branch;
99
+ headSha: Sha;
100
+ draft: boolean;
101
+ mergeable: "MERGEABLE" | "CONFLICTING" | "UNKNOWN";
102
+ mergeStateStatus: string;
103
+ merged: boolean;
104
+ mergedAt: string | null;
105
+ }
106
+
107
+ export interface CheckSummary {
108
+ name: string;
109
+ state: "success" | "failure" | "pending" | "neutral" | "skipped" | "cancelled" | "timed_out" | "action_required" | "stale" | "queued" | "in_progress" | "requested";
110
+ bucket: "pass" | "fail" | "pending" | "skip" | "neutral";
111
+ }
112
+
113
+ export interface EnsureIssueResult {
114
+ summary: IssueSummary;
115
+ created: boolean;
116
+ }
117
+
118
+ export interface GithubDriver {
119
+ ensureIssue(args: { repo: RepoSlug; title: string; body: string; labels: readonly string[] }): Promise<EnsureIssueResult>;
120
+ openDraftPullRequest(args: { repo: RepoSlug; head: Branch; base: Branch; title: string; body: string; issueNumber: IssueNumber }): Promise<PullRequestSummary>;
121
+ updatePullRequestBody(args: { repo: RepoSlug; number: PullRequestNumber; body: string }): Promise<void>;
122
+ markReady(args: { repo: RepoSlug; number: PullRequestNumber }): Promise<void>;
123
+ mergePullRequest(args: { repo: RepoSlug; number: PullRequestNumber; subject: string }): Promise<PullRequestSummary>;
124
+ readPullRequest(args: { repo: RepoSlug; number: PullRequestNumber }): Promise<PullRequestSummary>;
125
+ readChecks(args: { repo: RepoSlug; sha?: Sha; number?: PullRequestNumber; branch?: Branch; required: readonly string[] }): Promise<CheckSummary[]>;
126
+ comment(args: { repo: RepoSlug; number: PullRequestNumber; body: string }): Promise<void>;
127
+ refreshHead(args: { repo: RepoSlug; number: PullRequestNumber }): Promise<Sha>;
128
+ }
129
+
130
+ export interface CreateGhDriverOptions {
131
+ runner?: (args: readonly string[]) => Promise<{ status: number; stdout: string; stderr: string }>;
132
+ cwd?: string;
133
+ env?: NodeJS.ProcessEnv;
134
+ }
135
+
136
+ export type GhRunner = (args: readonly string[]) => Promise<{ status: number; stdout: string; stderr: string }>;
137
+
138
+ export interface GhStubHandle {
139
+ driver: GithubDriver;
140
+ queue: Array<{ match: (args: readonly string[]) => boolean; status?: number; stdout?: string; stderr?: string }>;
141
+ }
142
+
143
+ export interface RepoRef {
144
+ owner: string;
145
+ name: string;
146
+ }
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // Adapter
150
+ // ---------------------------------------------------------------------------
151
+
152
+ export interface Adapter {
153
+ contractVersion: 1;
154
+ repository: {
155
+ remote: string;
156
+ defaultBranch?: { discover?: boolean; name?: Branch };
157
+ };
158
+ forge?: {
159
+ driver: "github";
160
+ issueRequired?: boolean;
161
+ draftAfterFirstCommit?: boolean;
162
+ issueClosingSyntax?: boolean;
163
+ };
164
+ worktree?: {
165
+ root: string;
166
+ branchTemplate: string;
167
+ bootstrap?: readonly (readonly string[])[];
168
+ };
169
+ verification?: {
170
+ commands: readonly { id: string; argv: readonly string[]; timeoutMs?: number }[];
171
+ requireCleanDiffAfter?: boolean;
172
+ invalidateOnHeadChange?: boolean;
173
+ };
174
+ review?: {
175
+ agent: string;
176
+ required: boolean;
177
+ invalidateOnHeadChange: boolean;
178
+ };
179
+ ci?: {
180
+ driver: "github-status-checks";
181
+ requiredChecks: readonly string[];
182
+ wait: boolean;
183
+ flakyRetry: 0 | 1;
184
+ };
185
+ ready?: {
186
+ requires: readonly ("review" | "local-verification" | "remote-ci")[];
187
+ stopAfterReady: boolean;
188
+ };
189
+ merge?: {
190
+ strategy: "squash";
191
+ policy: "explicit-user-request-only";
192
+ requireFreshGates: boolean;
193
+ };
194
+ cleanup?: {
195
+ when: "next-task";
196
+ requires: readonly ("pr-merged" | "worktree-clean" | "no-unpublished-commits")[];
197
+ };
198
+ }
199
+
200
+ export type AdapterLoadResult =
201
+ | { ok: true; adapter: Adapter; path: string; sha256: Sha }
202
+ | { ok: false; error: { kind: "missing" | "parse" | "contract"; path: string; message?: string; issues?: readonly string[] } };
203
+
204
+ export interface DoctorCheck {
205
+ name: string;
206
+ ok: boolean;
207
+ detail: string;
208
+ }
209
+
210
+ export interface DoctorReport {
211
+ contractVersion: 1;
212
+ adapterPath: string | null;
213
+ adapterSha256: Sha | null;
214
+ lockPath: string | null;
215
+ lockSha256: Sha | null;
216
+ packageVersion: string | null;
217
+ nodeVersion: string;
218
+ ghVersion: string | null;
219
+ gitVersion: string | null;
220
+ checks: DoctorCheck[];
221
+ }
222
+
223
+ export type TransitionResult =
224
+ | { ok: true; from: LifecycleState; to: LifecycleState; at: number; reason?: string }
225
+ | { ok: false; from: LifecycleState; attempted: LifecycleState; reason: string };
226
+
227
+ export interface GateSnapshot {
228
+ prHead: Sha | null;
229
+ reviewerSha: Sha | null;
230
+ verifierSha: Sha | null;
231
+ checks: readonly CheckSummary[];
232
+ missingChecks: readonly string[];
233
+ failingChecks: readonly string[];
234
+ pendingChecks: readonly string[];
235
+ }
236
+
237
+ export interface GateCheck {
238
+ ok: boolean;
239
+ reason?: string;
240
+ snapshot: GateSnapshot;
241
+ }
242
+
243
+ export interface RecoveryReport {
244
+ total: number;
245
+ pendingCleanup: number;
246
+ orphanWorktrees: number;
247
+ cleaned: number;
248
+ notes: string[];
249
+ }
250
+
251
+ export interface WorktreeRecordShape {
252
+ path: string;
253
+ branch: string;
254
+ head: Sha;
255
+ }
256
+
257
+ export type RunCommandResult =
258
+ | { ok: true; status: number; stdout: string; stderr: string }
259
+ | { ok: false; error: Error };
260
+
261
+ // ---------------------------------------------------------------------------
262
+ // Tool factory deps shapes
263
+ // ---------------------------------------------------------------------------
264
+
265
+ export interface BaseToolDeps {
266
+ driver: GithubDriver;
267
+ repoRoot: string;
268
+ repoSlug: RepoSlug;
269
+ owner: string;
270
+ adapter: Adapter;
271
+ remote?: string;
272
+ packageVersion?: string;
273
+ }
274
+
275
+ export interface InspectToolDeps {
276
+ driver?: GithubDriver;
277
+ repoRoot: string;
278
+ repoSlug: RepoSlug;
279
+ owner?: string;
280
+ adapter?: Adapter | null;
281
+ packageVersion: string;
282
+ remote?: string;
283
+ }
284
+
285
+ export interface IssueToolInput {
286
+ taskId: string;
287
+ title: string;
288
+ body?: string;
289
+ baseBranch: string;
290
+ baseSha?: string;
291
+ branch: string;
292
+ labels?: readonly string[];
293
+ }
294
+
295
+ export interface WorktreeToolInput {
296
+ taskId: string;
297
+ branch: string;
298
+ worktreeRelativePath: string;
299
+ }
300
+
301
+ export interface VerifyToolInput {
302
+ taskId: string;
303
+ commandId?: string;
304
+ }
305
+
306
+ export interface ReviewToolInput {
307
+ taskId: string;
308
+ status: "pass" | "fail" | "blocked" | "partial";
309
+ headSha?: Sha;
310
+ findings?: unknown;
311
+ envelope?: unknown;
312
+ }
313
+
314
+ export interface PrToolInput {
315
+ taskId: string;
316
+ title: string;
317
+ body: string;
318
+ }
319
+
320
+ export interface ReadyToolInput {
321
+ taskId: string;
322
+ }
323
+
324
+ export interface MergeToolInput {
325
+ taskId: string;
326
+ subject: string;
327
+ }
328
+
329
+ export interface CleanupToolInput {
330
+ taskId: string;
331
+ }
332
+
333
+ export type Envelope<TKind extends string, TData> = { kind: TKind } & TData;
334
+
335
+ // ---------------------------------------------------------------------------
336
+ // Envelope shapes (mirror the runtime tool returns)
337
+ // ---------------------------------------------------------------------------
338
+
339
+ export type IssueEnvelope = Envelope<
340
+ "issue",
341
+ {
342
+ contractVersion: 1;
343
+ created: boolean;
344
+ issueNumber: IssueNumber;
345
+ issueUrl: string;
346
+ manifestPath: string;
347
+ }
348
+ >;
349
+
350
+ export type MissingManifestEnvelope = Envelope<"missing-manifest", { taskId: string }>;
351
+ export type ManifestStateEnvelope = Envelope<"manifest-state", { state: LifecycleState }>;
352
+ export type MissingWorktreePathEnvelope = Envelope<"missing-worktree-path", Record<string, never>>;
353
+ export type MissingPrEnvelope = Envelope<"missing-pr", Record<string, never>>;
354
+ export type WorktreeExistsEnvelope = Envelope<"worktree-exists", Record<string, never>>;
355
+ export type BranchExistsLocallyEnvelope = Envelope<"branch-exists-locally", { branch: Branch }>;
356
+ export type BranchExistsRemotelyEnvelope = Envelope<"branch-exists-remotely", { branch: Branch }>;
357
+ export type RemoteFetchEnvelope = Envelope<"remote-fetch", { stderr: string }>;
358
+ export type CreateFailedEnvelope = Envelope<"create-failed", { stderr: string }>;
359
+ export type HeadChangedAfterVerifierEnvelope = Envelope<"head-changed-after-verifier", { headSha: Sha; verifierSha: Sha }>;
360
+ export type HeadChangedAfterReviewEnvelope = Envelope<"head-changed-after-review", { headSha: Sha; reviewSha: Sha }>;
361
+ export type MissingGateEnvelope = Envelope<"missing-gate", { gate: "review" | "local-verification" | "remote-ci" }>;
362
+ export type CiFailingEnvelope = Envelope<"ci-failing", { failing: readonly string[] }>;
363
+ export type CiPendingEnvelope = Envelope<"ci-pending", { pending: readonly string[] }>;
364
+ export type NotReadyEnvelope = Envelope<"not-ready", { state: LifecycleState }>;
365
+ export type WrongBaseEnvelope = Envelope<"wrong-base", { base: Branch }>;
366
+ export type HeadChangedEnvelope = Envelope<"head-changed", { headSha: Sha; manifestSha: Sha }>;
367
+ export type NotMergeableEnvelope = Envelope<"not-mergeable", { reason: string }>;
368
+ export type DirtyWorktreeEnvelope = Envelope<"dirty-worktree", Record<string, never>>;
369
+ export type RebaseInProgressEnvelope = Envelope<"rebase-in-progress", Record<string, never>>;
370
+ export type HeadMismatchEnvelope = Envelope<"head-mismatch", { headSha: Sha; manifestSha: Sha }>;
371
+ export type CurrentCheckoutEnvelope = Envelope<"current-checkout", { worktreePath: string }>;
372
+ export type UnmergedEnvelope = Envelope<"unmerged", { headSha: Sha; manifestSha: Sha }>;
373
+ export type BaseMismatchEnvelope = Envelope<"base-mismatch", { manifestBase: Branch; prBase: Branch }>;
374
+ export type HasUnpublishedCommitsEnvelope = Envelope<"has-unpublished-commits", { ahead: number }>;
375
+ export type RemoveFailedEnvelope = Envelope<"remove-failed", { stderr: string }>;
376
+ export type UnsafeCleanupEnvelope = Envelope<"unsafe-cleanup", { signals: readonly string[] }>;
377
+ export type VerifyFailedEnvelope = Envelope<"verify-failed", { commandId: string; status: number; headSha: Sha | null }>;
378
+ export type PathEscapeEnvelope = Envelope<"path-escape", { resolvedPath: string }>;
379
+ export type BootstrapFailedEnvelope = Envelope<"bootstrap-failed", { stderr: string; argv: readonly string[] }>;
380
+
381
+ // ---------------------------------------------------------------------------
382
+ // Value export declarations. The runtime values live in plain JS modules;
383
+ // these declarations are the contract that consumers rely on.
384
+ // ---------------------------------------------------------------------------
385
+
386
+ export declare const ADAPTER_CONTRACT_VERSION: ContractVersion;
387
+ export declare const ADAPTER_FILENAME: string;
388
+ export declare const LOCK_FILENAME: string;
389
+ export declare const PACKAGE_VERSION: string;
390
+ export declare const STATES: readonly LifecycleState[];
391
+
392
+ export declare const WorktreeRecord: WorktreeRecordShape;
393
+
394
+ export declare function validateAdapter(value: unknown): {
395
+ ok: boolean;
396
+ issues?: readonly string[];
397
+ adapter?: Adapter;
398
+ };
399
+ export declare function loadAdapter(repoRoot: string): Promise<AdapterLoadResult>;
400
+ export declare function writeLock(repoRoot: string, adapterSha256: Sha): Promise<string>;
401
+ export declare function readLock(repoRoot: string): Promise<AdapterLock | null>;
402
+ export declare function findOpencodeDir(start: string): string | null;
403
+
404
+ export declare function createManifest(input: CreateManifestInput): Manifest;
405
+ export declare function transition(
406
+ manifest: Manifest,
407
+ to: LifecycleState,
408
+ opts?: { reason?: string; now?: () => Date },
409
+ ): TransitionResult;
410
+ export declare function canTransition(from: LifecycleState, to: LifecycleState): boolean;
411
+ export declare function isTerminal(state: LifecycleState): boolean;
412
+ export declare function mustRerunReview(previousSha: Sha | null, currentSha: Sha): boolean;
413
+ export declare function mustRerunVerifier(previousSha: Sha | null, currentSha: Sha): boolean;
414
+
415
+ export declare function writeManifest(repoRoot: string, manifest: Manifest): Promise<string>;
416
+ export declare function readManifest(repoRoot: string, taskId: string): Promise<Manifest | null>;
417
+ export declare function listManifests(repoRoot: string): Promise<Manifest[]>;
418
+ export declare function deleteManifest(repoRoot: string, taskId: string): Promise<void>;
419
+
420
+ export declare function isInsideWorktree(cwd: string): boolean;
421
+ export declare function isMainCheckout(cwd: string): boolean;
422
+ export declare function listWorktrees(cwd: string): WorktreeRecordShape[];
423
+ export declare function isWorktreeClean(cwd: string): boolean;
424
+ export declare function isRebaseInProgress(cwd: string): boolean;
425
+ export declare function currentBranch(cwd: string): Branch | null;
426
+ export declare function revParse(ref: string, cwd: string): Sha | null;
427
+ export declare function fetchBranch(remote: string, branch: Branch, cwd: string): { status: number; stderr: string };
428
+ export declare function remoteExists(remote: string, cwd: string): boolean;
429
+ export declare function createWorktree(opts: { cwd: string; branch: Branch; worktreePath: string; base: string }): { status: number; stderr: string };
430
+ export declare function createWorktreeFromLocal(opts: { cwd: string; branch: Branch; worktreePath: string; base: string }): { status: number; stderr: string };
431
+ export declare function worktreeExists(cwd: string, path: string): boolean;
432
+ export declare function branchExistsLocally(branch: Branch, cwd: string): boolean;
433
+ export declare function branchExistsRemotely(remote: string, branch: Branch, cwd: string): boolean;
434
+ export declare function mergeIntoFeature(branch: Branch, base: Branch, cwd: string): { status: number; stderr: string };
435
+ export declare function currentHead(cwd: string): Sha | null;
436
+ export declare function push(remote: string, branch: Branch, cwd: string): { status: number; stderr: string };
437
+ export declare function pushForceDisabled(remote: string, branch: Branch, cwd: string): { status: number; stderr: string };
438
+ export declare function defaultBranch(cwd: string): Branch | null;
439
+ export declare function mergeBaseRemoteHead(remote: string, branch: Branch, cwd: string): Sha | null;
440
+
441
+ export declare function createGhDriver(opts?: CreateGhDriverOptions): GithubDriver;
442
+ export declare function createGhStub(
443
+ responses: Array<{
444
+ match: (args: readonly string[]) => boolean;
445
+ status?: number;
446
+ stdout?: string;
447
+ stderr?: string;
448
+ }>,
449
+ ): GhStubHandle;
450
+ export declare function parseRepoSlug(slug: string): RepoRef | null;
451
+
452
+ export declare function scanRecovery(repoRoot: string): Promise<RecoveryReport>;
453
+ export declare function wouldCleanupBeSafe(args: {
454
+ prMerged: boolean;
455
+ worktreeClean: boolean;
456
+ rebaseInProgress: boolean;
457
+ headMatchesPr: boolean;
458
+ baseMatches: boolean;
459
+ }): boolean;
460
+ export declare function removeManifestIfSafe(repoRoot: string, taskId: string): Promise<boolean>;
461
+ export declare function recoverManifestAfterCrash(manifest: Manifest): Manifest;
462
+
463
+ export declare function doctor(repoRoot: string, packageVersion: string | null, adapter?: Adapter | null): Promise<DoctorReport>;
464
+
465
+ export declare function bucketFor(check: CheckSummary | undefined | null): "pass" | "fail" | "pending" | "skip" | "neutral";
466
+ export declare function gateSnapshot(args: {
467
+ manifest: { adapter?: Adapter | null; lastReviewerSha?: Sha | null; lastVerifierSha?: Sha | null };
468
+ prHead: Sha | null;
469
+ checks: readonly CheckSummary[];
470
+ }): GateSnapshot;
471
+ export declare function checkGates(args: {
472
+ manifest: { adapter?: Adapter | null; lastReviewerSha?: Sha | null; lastVerifierSha?: Sha | null };
473
+ prHead: Sha | null;
474
+ checks: readonly CheckSummary[];
475
+ requires: readonly ("review" | "local-verification" | "remote-ci")[];
476
+ }): GateCheck;
477
+ export declare function gateFailureEnvelope(result: { reason: string; snapshot: GateSnapshot }):
478
+ | MissingGateEnvelope
479
+ | HeadChangedAfterReviewEnvelope
480
+ | HeadChangedAfterVerifierEnvelope
481
+ | Envelope<"ci-missing", { missing: readonly string[] }>
482
+ | Envelope<"ci-failing", { failing: readonly string[] }>
483
+ | Envelope<"ci-pending", { pending: readonly string[] }>
484
+ | Envelope<"gate-failed", { reason: string }>;
485
+
486
+ export declare function createInspectTool(deps: InspectToolDeps): (input: { taskId: string }) => Promise<{
487
+ contractVersion: 1;
488
+ manifest: Manifest | null;
489
+ doctor: DoctorReport;
490
+ }>;
491
+
492
+ export declare function createIssueTool(deps: BaseToolDeps): (input: IssueToolInput) => Promise<IssueEnvelope | Envelope<"missing-input", { field: string }> | Envelope<"lifecycle", { reason: string }>>;
493
+
494
+ export declare function createWorktreeTool(deps: BaseToolDeps): (input: WorktreeToolInput) => Promise<
495
+ | Envelope<"worktree", { contractVersion: 1; branch: Branch; worktreePath: string; headSha: Sha; manifestPath: string }>
496
+ | MissingManifestEnvelope
497
+ | ManifestStateEnvelope
498
+ | Envelope<"missing-input", { field: string }>
499
+ | RemoteFetchEnvelope
500
+ | BranchExistsLocallyEnvelope
501
+ | BranchExistsRemotelyEnvelope
502
+ | WorktreeExistsEnvelope
503
+ | CreateFailedEnvelope
504
+ | Envelope<"missing-base-sha", Record<string, never>>
505
+ | Envelope<"bootstrap-invalid", { bootstrap: unknown }>
506
+ | BootstrapFailedEnvelope
507
+ | PathEscapeEnvelope
508
+ | Envelope<"lifecycle", { reason: string }>
509
+ >;
510
+
511
+ export declare function createVerifyTool(deps: BaseToolDeps): (input: VerifyToolInput) => Promise<
512
+ | Envelope<"verify", { contractVersion: 1; commandId: string; status: 0; stdoutTail: string; stderrTail: string; headSha: Sha; manifestPath: string }>
513
+ | MissingManifestEnvelope
514
+ | ManifestStateEnvelope
515
+ | Envelope<"no-commands", Record<string, never>>
516
+ | Envelope<"command-not-found", { commandId: string }>
517
+ | Envelope<"worktree-dirty", Record<string, never>>
518
+ | Envelope<"no-head", Record<string, never>>
519
+ | VerifyFailedEnvelope
520
+ | Envelope<"lifecycle", { reason: string }>
521
+ >;
522
+
523
+ export declare function createReviewTool(deps: BaseToolDeps): (input: ReviewToolInput) => Promise<
524
+ | Envelope<"review", { contractVersion: 1; pr: PullRequestNumber; reviewerSha: Sha; manifestPath: string }>
525
+ | MissingManifestEnvelope
526
+ | MissingPrEnvelope
527
+ | ManifestStateEnvelope
528
+ | Envelope<"review-not-pass", { status: string; headSha: Sha; recordedReviewerSha: Sha | null }>
529
+ | HeadMismatchEnvelope
530
+ >;
531
+
532
+ export declare function createPrTool(deps: BaseToolDeps): (input: PrToolInput) => Promise<
533
+ | Envelope<"pr", { contractVersion: 1; pr: PullRequestSummary; manifestPath: string }>
534
+ | MissingManifestEnvelope
535
+ | ManifestStateEnvelope
536
+ | Envelope<"lifecycle", { reason: string }>
537
+ >;
538
+
539
+ export declare function createReadyTool(deps: BaseToolDeps): (input: ReadyToolInput) => Promise<
540
+ | Envelope<"ready", { contractVersion: 1; manifestPath: string; pr: PullRequestNumber }>
541
+ | MissingManifestEnvelope
542
+ | MissingPrEnvelope
543
+ | MissingGateEnvelope
544
+ | HeadChangedAfterReviewEnvelope
545
+ | HeadChangedAfterVerifierEnvelope
546
+ | Envelope<"ci-missing", { missing: readonly string[] }>
547
+ | Envelope<"ci-failing", { failing: readonly string[] }>
548
+ | Envelope<"ci-pending", { pending: readonly string[] }>
549
+ >;
550
+
551
+ export declare function createMergeTool(deps: BaseToolDeps): (input: MergeToolInput) => Promise<
552
+ | Envelope<"merge", { contractVersion: 1; manifestPath: string; pr: PullRequestNumber }>
553
+ | MissingManifestEnvelope
554
+ | MissingPrEnvelope
555
+ | NotReadyEnvelope
556
+ | WrongBaseEnvelope
557
+ | HeadChangedEnvelope
558
+ | NotMergeableEnvelope
559
+ | MissingGateEnvelope
560
+ | HeadChangedAfterReviewEnvelope
561
+ | HeadChangedAfterVerifierEnvelope
562
+ | Envelope<"ci-missing", { missing: readonly string[] }>
563
+ | Envelope<"ci-failing", { failing: readonly string[] }>
564
+ | Envelope<"ci-pending", { pending: readonly string[] }>
565
+ >;
566
+
567
+ export declare function createCleanupTool(deps: BaseToolDeps): (input: CleanupToolInput) => Promise<
568
+ | Envelope<"cleanup", { contractVersion: 1; manifestPath: string | null; removedPath: string }>
569
+ | MissingManifestEnvelope
570
+ | ManifestStateEnvelope
571
+ | MissingWorktreePathEnvelope
572
+ | MissingPrEnvelope
573
+ | CurrentCheckoutEnvelope
574
+ | DirtyWorktreeEnvelope
575
+ | RebaseInProgressEnvelope
576
+ | HeadMismatchEnvelope
577
+ | UnmergedEnvelope
578
+ | BaseMismatchEnvelope
579
+ | HasUnpublishedCommitsEnvelope
580
+ | RemoveFailedEnvelope
581
+ | Envelope<"branch-delete-failed", { stderr: string }>
582
+ | Envelope<"unsafe-cleanup", { signals: readonly string[] }>
583
+ >;
package/dist/core.js CHANGED
@@ -1,4 +1,4 @@
1
- // opencode-ship/core v1.1.4
1
+ // opencode-ship/core v1.1.7
2
2
 
3
3
  // src/adapter.js
4
4
  import { readFile, writeFile, mkdir, rename } from "node:fs/promises";
@@ -851,7 +851,7 @@ function validateGhArgv(argv) {
851
851
 
852
852
  // src/drivers/gh-cli.js
853
853
  function defaultRunner(cwd, env) {
854
- return (args) => new Promise((resolve9, reject) => {
854
+ return (args) => new Promise((resolve10, reject) => {
855
855
  const proc = spawn2("gh", args, {
856
856
  cwd,
857
857
  env,
@@ -863,7 +863,7 @@ function defaultRunner(cwd, env) {
863
863
  proc.stdout.on("data", (d) => stdout += d.toString());
864
864
  proc.stderr.on("data", (d) => stderr += d.toString());
865
865
  proc.on("error", reject);
866
- proc.on("close", (status) => resolve9({ status: status ?? -1, stdout, stderr }));
866
+ proc.on("close", (status) => resolve10({ status: status ?? -1, stdout, stderr }));
867
867
  });
868
868
  }
869
869
  function viewFields() {
@@ -2527,6 +2527,42 @@ function createMergeTool(deps) {
2527
2527
  } else if (m.schemaVersion >= 2) {
2528
2528
  return { kind: "missing-workflow-link", taskId: m.taskId };
2529
2529
  }
2530
+ if (pr.headSha !== (m.lastPrHeadSha ?? pr.headSha)) {
2531
+ return {
2532
+ kind: "head-changed",
2533
+ headSha: pr.headSha,
2534
+ manifestSha: m.lastPrHeadSha ?? ""
2535
+ };
2536
+ }
2537
+ async function recordMerged(merged2, reason) {
2538
+ const t = transition(
2539
+ { ...m, lastPrHeadSha: merged2.headSha },
2540
+ "merged",
2541
+ { reason }
2542
+ );
2543
+ if (!t.ok) return { kind: "lifecycle", reason: t.reason };
2544
+ const next = {
2545
+ ...m,
2546
+ lastPrHeadSha: merged2.headSha,
2547
+ state: t.to,
2548
+ transitionLog: [
2549
+ ...m.transitionLog,
2550
+ { from: t.from, to: t.to, at: t.at, reason: t.reason }
2551
+ ],
2552
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2553
+ };
2554
+ const path = await writeManifest(deps.repoRoot, next);
2555
+ if (finalEvidence && m.workflowId) {
2556
+ await appendRunEvent(deps.repoRoot, m.workflowId, finalEvidence.runState, {
2557
+ kind: RUN_EVENT_KINDS.MERGE,
2558
+ data: { mergeSha: merged2.mergeSha ?? merged2.mergeCommitSha ?? merged2.headSha }
2559
+ });
2560
+ }
2561
+ return { kind: "merge", contractVersion: 1, manifestPath: path, pr: m.prNumber, taskId: m.taskId };
2562
+ }
2563
+ if (pr.merged) {
2564
+ return recordMerged(pr, `reconciled external merge as ${input.subject}`);
2565
+ }
2530
2566
  if (freshGates) {
2531
2567
  const required = deps.adapter?.ready?.requires ?? [
2532
2568
  "review",
@@ -2553,13 +2589,6 @@ function createMergeTool(deps) {
2553
2589
  });
2554
2590
  if (!result.ok) return gateFailureEnvelope(result);
2555
2591
  }
2556
- if (pr.headSha !== (m.lastPrHeadSha ?? pr.headSha)) {
2557
- return {
2558
- kind: "head-changed",
2559
- headSha: pr.headSha,
2560
- manifestSha: m.lastPrHeadSha ?? ""
2561
- };
2562
- }
2563
2592
  if (pr.draft) return { kind: "not-mergeable", reason: "PR is still draft" };
2564
2593
  if (pr.mergeable !== "MERGEABLE") {
2565
2594
  return { kind: "not-mergeable", reason: `mergeable=${pr.mergeable}` };
@@ -2569,30 +2598,7 @@ function createMergeTool(deps) {
2569
2598
  number: m.prNumber,
2570
2599
  subject: input.subject
2571
2600
  });
2572
- const t = transition(
2573
- { ...m, lastPrHeadSha: merged.headSha },
2574
- "merged",
2575
- { reason: `squash merged as ${input.subject}` }
2576
- );
2577
- if (!t.ok) return { kind: "lifecycle", reason: t.reason };
2578
- const next = {
2579
- ...m,
2580
- lastPrHeadSha: merged.headSha,
2581
- state: t.to,
2582
- transitionLog: [
2583
- ...m.transitionLog,
2584
- { from: t.from, to: t.to, at: t.at, reason: t.reason }
2585
- ],
2586
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2587
- };
2588
- const path = await writeManifest(deps.repoRoot, next);
2589
- if (finalEvidence && m.workflowId) {
2590
- await appendRunEvent(deps.repoRoot, m.workflowId, finalEvidence.runState, {
2591
- kind: RUN_EVENT_KINDS.MERGE,
2592
- data: { mergeSha: merged.mergeSha ?? merged.mergeCommitSha ?? merged.headSha }
2593
- });
2594
- }
2595
- return { kind: "merge", contractVersion: 1, manifestPath: path, pr: m.prNumber, taskId: m.taskId };
2601
+ return recordMerged(merged, `squash merged as ${input.subject}`);
2596
2602
  };
2597
2603
  }
2598
2604
 
@@ -2769,8 +2775,12 @@ function createCleanupTool(deps) {
2769
2775
  };
2770
2776
  }
2771
2777
 
2772
- // src/index.js
2773
- var PACKAGE_VERSION = "0.1.3";
2778
+ // src/version.js
2779
+ import { readFileSync, existsSync as existsSync7 } from "node:fs";
2780
+ import { dirname as dirname3, resolve as resolve9 } from "node:path";
2781
+ import { fileURLToPath } from "node:url";
2782
+ var PACKAGE_VERSION = "1.1.7";
2783
+ var TEMPLATE_SET = `v${PACKAGE_VERSION}`;
2774
2784
  export {
2775
2785
  ADAPTER_CONTRACT_VERSION,
2776
2786
  ADAPTER_FILENAME,
package/dist/plugin.js CHANGED
@@ -1,4 +1,4 @@
1
- // opencode-ship v1.1.4
1
+ // opencode-ship v1.1.7
2
2
  var __defProp = Object.defineProperty;
3
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
4
  var __esm = (fn, res) => function __init() {
@@ -14925,6 +14925,42 @@ function createMergeTool(deps) {
14925
14925
  } else if (m.schemaVersion >= 2) {
14926
14926
  return { kind: "missing-workflow-link", taskId: m.taskId };
14927
14927
  }
14928
+ if (pr.headSha !== (m.lastPrHeadSha ?? pr.headSha)) {
14929
+ return {
14930
+ kind: "head-changed",
14931
+ headSha: pr.headSha,
14932
+ manifestSha: m.lastPrHeadSha ?? ""
14933
+ };
14934
+ }
14935
+ async function recordMerged(merged2, reason) {
14936
+ const t = transition(
14937
+ { ...m, lastPrHeadSha: merged2.headSha },
14938
+ "merged",
14939
+ { reason }
14940
+ );
14941
+ if (!t.ok) return { kind: "lifecycle", reason: t.reason };
14942
+ const next = {
14943
+ ...m,
14944
+ lastPrHeadSha: merged2.headSha,
14945
+ state: t.to,
14946
+ transitionLog: [
14947
+ ...m.transitionLog,
14948
+ { from: t.from, to: t.to, at: t.at, reason: t.reason }
14949
+ ],
14950
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
14951
+ };
14952
+ const path = await writeManifest(deps.repoRoot, next);
14953
+ if (finalEvidence && m.workflowId) {
14954
+ await appendRunEvent(deps.repoRoot, m.workflowId, finalEvidence.runState, {
14955
+ kind: RUN_EVENT_KINDS.MERGE,
14956
+ data: { mergeSha: merged2.mergeSha ?? merged2.mergeCommitSha ?? merged2.headSha }
14957
+ });
14958
+ }
14959
+ return { kind: "merge", contractVersion: 1, manifestPath: path, pr: m.prNumber, taskId: m.taskId };
14960
+ }
14961
+ if (pr.merged) {
14962
+ return recordMerged(pr, `reconciled external merge as ${input.subject}`);
14963
+ }
14928
14964
  if (freshGates) {
14929
14965
  const required2 = deps.adapter?.ready?.requires ?? [
14930
14966
  "review",
@@ -14951,13 +14987,6 @@ function createMergeTool(deps) {
14951
14987
  });
14952
14988
  if (!result.ok) return gateFailureEnvelope(result);
14953
14989
  }
14954
- if (pr.headSha !== (m.lastPrHeadSha ?? pr.headSha)) {
14955
- return {
14956
- kind: "head-changed",
14957
- headSha: pr.headSha,
14958
- manifestSha: m.lastPrHeadSha ?? ""
14959
- };
14960
- }
14961
14990
  if (pr.draft) return { kind: "not-mergeable", reason: "PR is still draft" };
14962
14991
  if (pr.mergeable !== "MERGEABLE") {
14963
14992
  return { kind: "not-mergeable", reason: `mergeable=${pr.mergeable}` };
@@ -14967,30 +14996,7 @@ function createMergeTool(deps) {
14967
14996
  number: m.prNumber,
14968
14997
  subject: input.subject
14969
14998
  });
14970
- const t = transition(
14971
- { ...m, lastPrHeadSha: merged.headSha },
14972
- "merged",
14973
- { reason: `squash merged as ${input.subject}` }
14974
- );
14975
- if (!t.ok) return { kind: "lifecycle", reason: t.reason };
14976
- const next = {
14977
- ...m,
14978
- lastPrHeadSha: merged.headSha,
14979
- state: t.to,
14980
- transitionLog: [
14981
- ...m.transitionLog,
14982
- { from: t.from, to: t.to, at: t.at, reason: t.reason }
14983
- ],
14984
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
14985
- };
14986
- const path = await writeManifest(deps.repoRoot, next);
14987
- if (finalEvidence && m.workflowId) {
14988
- await appendRunEvent(deps.repoRoot, m.workflowId, finalEvidence.runState, {
14989
- kind: RUN_EVENT_KINDS.MERGE,
14990
- data: { mergeSha: merged.mergeSha ?? merged.mergeCommitSha ?? merged.headSha }
14991
- });
14992
- }
14993
- return { kind: "merge", contractVersion: 1, manifestPath: path, pr: m.prNumber, taskId: m.taskId };
14999
+ return recordMerged(merged, `squash merged as ${input.subject}`);
14994
15000
  };
14995
15001
  }
14996
15002
 
@@ -19158,13 +19164,11 @@ function spawn8(repoRoot, args) {
19158
19164
  return { status: r.status ?? -1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
19159
19165
  }
19160
19166
  function casDeleteBranch2(repoRoot, branch, expectedSha) {
19161
- const argv = ["update-ref", "-d"];
19162
- if (expectedSha && /^[0-9f]{7,}$/i.test(expectedSha)) {
19163
- argv.push(`refs/heads/${branch}`, expectedSha);
19164
- } else {
19165
- argv.push(`refs/heads/${branch}`);
19166
- }
19167
- return spawn8(repoRoot, argv).status ?? -1;
19167
+ if (!isValidExpectedSha(expectedSha)) return -1;
19168
+ return spawn8(repoRoot, ["update-ref", "-d", `refs/heads/${branch}`, expectedSha]).status ?? -1;
19169
+ }
19170
+ function isValidExpectedSha(value) {
19171
+ return typeof value === "string" && /^[0-9a-f]{7,}$/i.test(value);
19168
19172
  }
19169
19173
  function safeRemoveWorktree2(repoRoot, target) {
19170
19174
  const r = spawn8(repoRoot, ["worktree", "remove", target]);
@@ -19195,15 +19199,12 @@ async function saveCleanupPending(repoRoot, entries) {
19195
19199
  writeFileSync2(path, JSON.stringify(dedupePending(entries), null, 2) + "\n", "utf8");
19196
19200
  }
19197
19201
  function dedupePending(entries) {
19198
- const seen = /* @__PURE__ */ new Set();
19199
- const out = [];
19202
+ const byTask = /* @__PURE__ */ new Map();
19200
19203
  for (const e of entries) {
19201
19204
  if (!e || !e.taskId) continue;
19202
- if (seen.has(e.taskId)) continue;
19203
- seen.add(e.taskId);
19204
- out.push(e);
19205
+ byTask.set(e.taskId, e);
19205
19206
  }
19206
- return out;
19207
+ return [...byTask.values()];
19207
19208
  }
19208
19209
  async function appendCleanupPending(repoRoot, entry) {
19209
19210
  const current = await loadCleanupPending(repoRoot);
@@ -19211,13 +19212,28 @@ async function appendCleanupPending(repoRoot, entry) {
19211
19212
  await saveCleanupPending(repoRoot, next);
19212
19213
  return next;
19213
19214
  }
19215
+ async function clearCleanupPending(repoRoot, taskId) {
19216
+ const current = await loadCleanupPending(repoRoot);
19217
+ await saveCleanupPending(repoRoot, current.filter((entry) => entry.taskId !== taskId));
19218
+ }
19214
19219
  function reject(reason, extra = {}) {
19215
19220
  return { ok: false, reason, ...extra };
19216
19221
  }
19217
19222
  async function tryImmediateCleanup({ repoRoot, taskId, adapter }) {
19218
19223
  if (!repoRoot || !taskId) return reject("missing-args");
19224
+ const pending = (await loadCleanupPending(repoRoot)).find((entry) => entry.taskId === taskId);
19225
+ const stage = pending?.stage ?? "worktree-remove";
19226
+ if (!["worktree-remove", "branch-delete", "manifest-seal"].includes(stage)) {
19227
+ return reject("cleanup-stage", { stage });
19228
+ }
19219
19229
  const m = await readManifest(repoRoot, taskId);
19220
- if (!m) return reject("missing-manifest");
19230
+ if (!m) {
19231
+ if (pending && stage === "manifest-seal") {
19232
+ await clearCleanupPending(repoRoot, taskId);
19233
+ return { ok: true, removedPath: null, sealed: true };
19234
+ }
19235
+ return reject("missing-manifest");
19236
+ }
19221
19237
  if (m.state !== "merged" && m.state !== "cleanup-pending") {
19222
19238
  return reject("manifest-state", { state: m.state });
19223
19239
  }
@@ -19229,36 +19245,68 @@ async function tryImmediateCleanup({ repoRoot, taskId, adapter }) {
19229
19245
  if (!wtPath.startsWith(rootAbs + "/")) {
19230
19246
  return reject("worktree-out-of-root", { expected: rootAbs, got: wtPath });
19231
19247
  }
19232
- const status = spawn8(wtPath, ["status", "--porcelain"]);
19233
- if (status.status === 0 && status.stdout.trim().length > 0) return reject("dirty-worktree");
19234
- const rebase = spawn8(wtPath, ["rev-parse", "--verify", "--quiet", "REBASE_HEAD"]);
19235
- if (rebase.status === 0) return reject("rebase-in-progress");
19236
- const head = spawn8(wtPath, ["rev-parse", "HEAD"]);
19237
- if (head.status !== 0) return reject("no-head");
19238
- const headSha = head.stdout.trim();
19239
- if (m.lastPrHeadSha && headSha !== m.lastPrHeadSha) {
19240
- return reject("head-mismatch", { expected: m.lastPrHeadSha, actual: headSha });
19241
- }
19242
- const removed = safeRemoveWorktree2(repoRoot, wtPath);
19243
- if (removed.status !== 0) {
19248
+ let headSha = isValidExpectedSha(pending?.expectedHeadSha) ? pending.expectedHeadSha : isValidExpectedSha(m.lastPrHeadSha) ? m.lastPrHeadSha : "";
19249
+ if (stage === "worktree-remove") {
19250
+ const status = spawn8(wtPath, ["status", "--porcelain"]);
19251
+ if (status.status === 0) {
19252
+ if (status.stdout.trim().length > 0) return reject("dirty-worktree");
19253
+ const rebase = spawn8(wtPath, ["rev-parse", "--verify", "--quiet", "REBASE_HEAD"]);
19254
+ if (rebase.status === 0) return reject("rebase-in-progress");
19255
+ const head = spawn8(wtPath, ["rev-parse", "HEAD"]);
19256
+ if (head.status !== 0) return reject("no-head");
19257
+ headSha = head.stdout.trim();
19258
+ if (m.lastPrHeadSha && headSha !== m.lastPrHeadSha) {
19259
+ return reject("head-mismatch", { expected: m.lastPrHeadSha, actual: headSha });
19260
+ }
19261
+ await appendCleanupPending(repoRoot, {
19262
+ taskId,
19263
+ failedAt: (/* @__PURE__ */ new Date()).toISOString(),
19264
+ stage: "worktree-remove",
19265
+ expectedHeadSha: headSha,
19266
+ reason: "worktree head validated"
19267
+ });
19268
+ const removed = safeRemoveWorktree2(repoRoot, wtPath);
19269
+ if (removed.status !== 0) {
19270
+ await appendCleanupPending(repoRoot, {
19271
+ taskId,
19272
+ failedAt: (/* @__PURE__ */ new Date()).toISOString(),
19273
+ stage: "worktree-remove",
19274
+ expectedHeadSha: headSha,
19275
+ reason: removed.stderr ?? "non-zero exit"
19276
+ });
19277
+ return reject("remove-failed", { detail: removed.stderr });
19278
+ }
19279
+ } else if (!headSha) {
19280
+ return reject("no-head");
19281
+ }
19244
19282
  await appendCleanupPending(repoRoot, {
19245
19283
  taskId,
19246
19284
  failedAt: (/* @__PURE__ */ new Date()).toISOString(),
19247
- stage: "worktree-remove",
19248
- reason: removed.stderr ?? "non-zero exit"
19285
+ stage: "branch-delete",
19286
+ expectedHeadSha: headSha,
19287
+ reason: "resume cleanup"
19249
19288
  });
19250
- return reject("remove-failed", { detail: removed.stderr });
19251
19289
  }
19252
- const branchDelete = casDeleteBranch2(repoRoot, m.branch, headSha);
19253
- const branchStillThere = spawn8(repoRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${m.branch}`]);
19254
- if (branchDelete !== 0 && branchStillThere.status === 0) {
19290
+ if (stage !== "manifest-seal") {
19291
+ if (!isValidExpectedSha(headSha)) return reject("no-head");
19292
+ const branchDelete = casDeleteBranch2(repoRoot, m.branch, headSha);
19293
+ const branchStillThere = spawn8(repoRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${m.branch}`]);
19294
+ if (branchDelete !== 0 && branchStillThere.status === 0) {
19295
+ await appendCleanupPending(repoRoot, {
19296
+ taskId,
19297
+ failedAt: (/* @__PURE__ */ new Date()).toISOString(),
19298
+ stage: "branch-delete",
19299
+ expectedHeadSha: headSha,
19300
+ reason: "git update-ref failed"
19301
+ });
19302
+ return reject("branch-delete-failed");
19303
+ }
19255
19304
  await appendCleanupPending(repoRoot, {
19256
19305
  taskId,
19257
19306
  failedAt: (/* @__PURE__ */ new Date()).toISOString(),
19258
- stage: "branch-delete",
19259
- reason: "git update-ref failed"
19307
+ stage: "manifest-seal",
19308
+ reason: "resume cleanup"
19260
19309
  });
19261
- return reject("branch-delete-failed");
19262
19310
  }
19263
19311
  const next = {
19264
19312
  ...m,
@@ -19271,11 +19319,18 @@ async function tryImmediateCleanup({ repoRoot, taskId, adapter }) {
19271
19319
  };
19272
19320
  await writeManifest(repoRoot, next).catch(() => null);
19273
19321
  await deleteManifest(repoRoot, taskId);
19322
+ await clearCleanupPending(repoRoot, taskId);
19274
19323
  return { ok: true, removedPath: wtPath, sealed: true };
19275
19324
  }
19276
19325
  async function listPending(repoRoot) {
19277
19326
  const all = await listManifests(repoRoot).catch(() => []);
19278
- return all.filter((m) => m.state === "merged" || m.state === "cleanup-pending");
19327
+ const manifests = all.filter((m) => m.state === "merged" || m.state === "cleanup-pending");
19328
+ const queued = await loadCleanupPending(repoRoot);
19329
+ const byTask = new Map(manifests.map((manifest) => [manifest.taskId, manifest]));
19330
+ for (const entry of queued) {
19331
+ if (!byTask.has(entry.taskId)) byTask.set(entry.taskId, entry);
19332
+ }
19333
+ return [...byTask.values()];
19279
19334
  }
19280
19335
 
19281
19336
  // src/installer/ship-adapter.js
@@ -19328,7 +19383,7 @@ function selectRuntimeAdapter({ config: config2, shipAdapter, legacyAdapter }) {
19328
19383
  import { readFileSync as readFileSync4, existsSync as existsSync27 } from "node:fs";
19329
19384
  import { dirname as dirname12, resolve as resolve21 } from "node:path";
19330
19385
  import { fileURLToPath } from "node:url";
19331
- var PACKAGE_VERSION = "1.1.4";
19386
+ var PACKAGE_VERSION = "1.1.7";
19332
19387
  var TEMPLATE_SET = `v${PACKAGE_VERSION}`;
19333
19388
 
19334
19389
  // src/plugin.js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-ship",
3
- "version": "1.1.4",
3
+ "version": "1.1.7",
4
4
  "private": false,
5
5
  "description": "npm-distributed OpenCode installer that materializes the delivery plugin, reviewer/verifier agents, skills, ship config and lock into any consumer repository.",
6
6
  "license": "MIT",
@@ -32,6 +32,7 @@
32
32
  "import": "./dist/plugin.js"
33
33
  },
34
34
  "./core": {
35
+ "types": "./dist/core.d.ts",
35
36
  "import": "./dist/core.js"
36
37
  },
37
38
  "./schema/project-adapter.schema.json": "./schema/project-adapter.schema.json",