opencode-ship 1.1.7 → 1.1.9-rc.1

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 (44) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/README.md +13 -13
  3. package/THIRD_PARTY_NOTICES.md +1 -1
  4. package/assets/agents/ship-controller.md +47 -22
  5. package/assets/agents/ship-final-spec-reviewer.md +11 -11
  6. package/assets/agents/ship-final-standards-reviewer.md +11 -11
  7. package/assets/agents/ship-plan.md +24 -5
  8. package/assets/agents/ship-planner.md +46 -20
  9. package/assets/agents/{delivery-reviewer.md → ship-reviewer.md} +16 -16
  10. package/assets/agents/ship-task-builder.md +11 -11
  11. package/assets/agents/ship-task-reviewer.md +12 -12
  12. package/assets/agents/{delivery-verifier.md → ship-verifier.md} +13 -13
  13. package/assets/commands/setup-ship-workflow.md +6 -5
  14. package/assets/commands/ship-deliver.md +27 -30
  15. package/assets/defaults/workflow-models.history.json +7 -0
  16. package/assets/defaults/workflow-models.json +5 -0
  17. package/assets/skills/brainstorming/SKILL.md +10 -7
  18. package/assets/skills/dispatching-parallel-agents/SKILL.md +1 -1
  19. package/assets/skills/engineering-workflow/SKILL.md +10 -4
  20. package/assets/skills/executing-plans/SKILL.md +18 -63
  21. package/assets/skills/planning-research-checkpoint/SKILL.md +16 -13
  22. package/assets/skills/receiving-code-review/SKILL.md +1 -1
  23. package/assets/skills/requesting-code-review/SKILL.md +1 -1
  24. package/assets/skills/setup-ship-workflow/SKILL.md +27 -33
  25. package/assets/skills/ship-workflow/SKILL.md +83 -0
  26. package/assets/skills/skill-discovery/SKILL.md +17 -89
  27. package/assets/skills/subagent-driven-development/SKILL.md +6 -2
  28. package/assets/skills/systematic-debugging/SKILL.md +1 -1
  29. package/assets/skills/test-driven-development/SKILL.md +1 -1
  30. package/assets/skills/verification-before-completion/SKILL.md +1 -1
  31. package/assets/skills/wayfinder/SKILL.md +7 -1
  32. package/assets/skills/writing-plans/SKILL.md +31 -20
  33. package/dist/cli.js +1482 -175
  34. package/dist/core.d.ts +18 -0
  35. package/dist/core.js +528 -11
  36. package/dist/plugin.js +18613 -17483
  37. package/package.json +2 -1
  38. package/schema/project-adapter.example.json +2 -2
  39. package/schema/project-opencode-shim.json +2 -0
  40. package/schema/ship-lock.schema.json +23 -2
  41. package/tests/plugin/expected-tools.mjs +48 -5
  42. package/tests/plugin/plugin-load.test.mjs +27 -5
  43. package/vendor/sources.json +21 -21
  44. package/assets/skills/delivery-workflow/SKILL.md +0 -64
package/dist/core.d.ts CHANGED
@@ -100,6 +100,7 @@ export interface PullRequestSummary {
100
100
  draft: boolean;
101
101
  mergeable: "MERGEABLE" | "CONFLICTING" | "UNKNOWN";
102
102
  mergeStateStatus: string;
103
+ state: "OPEN" | "CLOSED" | "MERGED" | "UNKNOWN";
103
104
  merged: boolean;
104
105
  mergedAt: string | null;
105
106
  }
@@ -330,6 +331,12 @@ export interface CleanupToolInput {
330
331
  taskId: string;
331
332
  }
332
333
 
334
+ export interface AbandonToolInput {
335
+ taskId: string;
336
+ subject: string;
337
+ operationId?: string;
338
+ }
339
+
333
340
  export type Envelope<TKind extends string, TData> = { kind: TKind } & TData;
334
341
 
335
342
  // ---------------------------------------------------------------------------
@@ -581,3 +588,14 @@ export declare function createCleanupTool(deps: BaseToolDeps): (input: CleanupTo
581
588
  | Envelope<"branch-delete-failed", { stderr: string }>
582
589
  | Envelope<"unsafe-cleanup", { signals: readonly string[] }>
583
590
  >;
591
+
592
+ export declare function createAbandonTool(deps: BaseToolDeps): (input: AbandonToolInput) => Promise<
593
+ | { contractVersion: 2; ok: true; kind: "abandon"; operationId: string; idempotent: boolean; data: {
594
+ taskId: string;
595
+ intentHash: string;
596
+ removedWorktree: boolean;
597
+ deletedBranch: boolean;
598
+ deletedManifest: boolean;
599
+ } }
600
+ | { contractVersion: 2; ok: false; kind: "abandon"; operationId: string; retryable: boolean; message: string; details: { kind: string } }
601
+ >;
package/dist/core.js CHANGED
@@ -1,4 +1,4 @@
1
- // opencode-ship/core v1.1.7
1
+ // opencode-ship/core v1.1.9-rc.1
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((resolve10, reject) => {
854
+ return (args) => new Promise((resolve11, 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) => resolve10({ status: status ?? -1, stdout, stderr }));
866
+ proc.on("close", (status) => resolve11({ status: status ?? -1, stdout, stderr }));
867
867
  });
868
868
  }
869
869
  function viewFields() {
@@ -1100,6 +1100,19 @@ Closes #${issueNumber}`;
1100
1100
  ]);
1101
1101
  return fields.headRefOid;
1102
1102
  },
1103
+ async readIssue({ repo, number }) {
1104
+ if (typeof number !== "number") throw new Error("readIssue: number is required");
1105
+ if (!parseRepoSlug(repo)) throw new Error(`readIssue: invalid repo slug ${repo}`);
1106
+ return ghJson(run, [
1107
+ "issue",
1108
+ "view",
1109
+ String(number),
1110
+ "--repo",
1111
+ repo,
1112
+ "--json",
1113
+ "title,body"
1114
+ ]);
1115
+ },
1103
1116
  async runCommand(argv) {
1104
1117
  if (!Array.isArray(argv) || argv.length === 0) {
1105
1118
  throw new Error("runCommand: argv must be a non-empty array");
@@ -1400,6 +1413,60 @@ function createInspectTool(deps) {
1400
1413
  };
1401
1414
  }
1402
1415
 
1416
+ // src/runtime/stages.js
1417
+ function progressLine(stage, extra = {}) {
1418
+ switch (stage) {
1419
+ case "setup":
1420
+ return "Setup: ready.";
1421
+ case "discover":
1422
+ return extra.count > 0 ? `Discover: ${extra.count} skills installed.` : "Discover: none (catalog only).";
1423
+ case "plan":
1424
+ return `Plan: ${extra.path}`;
1425
+ case "track":
1426
+ return `Track: issue #${extra.number}.`;
1427
+ case "build":
1428
+ return `Build: task ${extra.k}/${extra.n} ${extra.title}.`;
1429
+ case "review":
1430
+ return extra.ok ? "Review: pass." : "Review: fail (see notes).";
1431
+ case "verify":
1432
+ return extra.ok ? "Verify: pass." : "Verify: fail.";
1433
+ case "ready":
1434
+ return `Ready: PR #${extra.number}.`;
1435
+ case "merge":
1436
+ return `Merge: ${extra.sha}.`;
1437
+ case "cleanup":
1438
+ return "Cleanup: done.";
1439
+ default:
1440
+ return null;
1441
+ }
1442
+ }
1443
+ function nextLine(stage) {
1444
+ switch (stage) {
1445
+ case "setup":
1446
+ return "Next: tell me what you want to build.";
1447
+ case "discover":
1448
+ return "Next: I will use these skills while building.";
1449
+ case "plan":
1450
+ return "Next: say if this is the product you want.";
1451
+ case "approve":
1452
+ return "Next: start building now, or continue later.";
1453
+ case "track":
1454
+ return "Next: building starts.";
1455
+ case "build":
1456
+ case "review":
1457
+ case "verify":
1458
+ return "Next: I keep going.";
1459
+ case "ready":
1460
+ return "Next: say if I should merge it.";
1461
+ case "merge":
1462
+ return "Next: cleanup.";
1463
+ case "cleanup":
1464
+ return "Next: tell me what to build next.";
1465
+ default:
1466
+ return null;
1467
+ }
1468
+ }
1469
+
1403
1470
  // src/tools/delivery-issue.js
1404
1471
  function createIssueTool(deps) {
1405
1472
  return async function issue(input) {
@@ -1409,13 +1476,16 @@ function createIssueTool(deps) {
1409
1476
  if (!input.branch) return { kind: "missing-input", field: "branch" };
1410
1477
  const existing = await readManifest(deps.repoRoot, input.taskId);
1411
1478
  if (existing) {
1479
+ const stage2 = "track";
1412
1480
  return {
1413
1481
  contractVersion: 1,
1414
1482
  created: false,
1415
1483
  issueNumber: existing.issueNumber,
1416
1484
  issueUrl: `https://github.com/${deps.repoSlug}/issues/${existing.issueNumber}`,
1417
1485
  manifestPath: "preserved",
1418
- preserved: true
1486
+ preserved: true,
1487
+ progress: progressLine(stage2, { number: existing.issueNumber }),
1488
+ next: nextLine(stage2)
1419
1489
  };
1420
1490
  }
1421
1491
  const ensured = await deps.driver.ensureIssue({
@@ -1453,12 +1523,15 @@ function createIssueTool(deps) {
1453
1523
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1454
1524
  };
1455
1525
  const path = await writeManifest(deps.repoRoot, next);
1526
+ const stage = "track";
1456
1527
  return {
1457
1528
  contractVersion: 1,
1458
1529
  created: ensured.created,
1459
1530
  issueNumber: ensured.summary.number,
1460
1531
  issueUrl: ensured.summary.url,
1461
- manifestPath: path
1532
+ manifestPath: path,
1533
+ progress: progressLine(stage, { number: ensured.summary.number }),
1534
+ next: nextLine(stage)
1462
1535
  };
1463
1536
  };
1464
1537
  }
@@ -1532,6 +1605,9 @@ function createWorktreeTool(deps) {
1532
1605
  expectedRoot: resolve7(deps.repoRoot, worktreeRoot)
1533
1606
  };
1534
1607
  }
1608
+ if (m.schemaVersion >= 2 && !m.workflowId) {
1609
+ return { kind: "missing-workflow-link", taskId: m.taskId };
1610
+ }
1535
1611
  const remote = deps.remote ?? "origin";
1536
1612
  const hasRemote = remoteExists(remote, deps.repoRoot);
1537
1613
  if (hasRemote) {
@@ -2496,7 +2572,15 @@ function createReadyTool(deps) {
2496
2572
  data: { headSha: prHead, taskId: input.taskId }
2497
2573
  });
2498
2574
  }
2499
- return { contractVersion: 1, manifestPath: path, pr: m.prNumber, workflowId };
2575
+ const stage = "ready";
2576
+ return {
2577
+ contractVersion: 1,
2578
+ manifestPath: path,
2579
+ pr: m.prNumber,
2580
+ workflowId,
2581
+ progress: progressLine(stage, { number: m.prNumber }),
2582
+ next: nextLine(stage)
2583
+ };
2500
2584
  };
2501
2585
  }
2502
2586
 
@@ -2558,7 +2642,16 @@ function createMergeTool(deps) {
2558
2642
  data: { mergeSha: merged2.mergeSha ?? merged2.mergeCommitSha ?? merged2.headSha }
2559
2643
  });
2560
2644
  }
2561
- return { kind: "merge", contractVersion: 1, manifestPath: path, pr: m.prNumber, taskId: m.taskId };
2645
+ const stage = "merge";
2646
+ return {
2647
+ kind: "merge",
2648
+ contractVersion: 1,
2649
+ manifestPath: path,
2650
+ pr: m.prNumber,
2651
+ taskId: m.taskId,
2652
+ progress: progressLine(stage, { sha: merged2.headSha.slice(0, 7) }),
2653
+ next: nextLine(stage)
2654
+ };
2562
2655
  }
2563
2656
  if (pr.merged) {
2564
2657
  return recordMerged(pr, `reconciled external merge as ${input.subject}`);
@@ -2770,16 +2863,439 @@ function createCleanupTool(deps) {
2770
2863
  contractVersion: 1,
2771
2864
  manifestPath: null,
2772
2865
  removedPath: wtPath,
2773
- bootstrapRecovery: isBootstrapRecovery
2866
+ bootstrapRecovery: isBootstrapRecovery,
2867
+ progress: progressLine("cleanup"),
2868
+ next: nextLine("cleanup")
2869
+ };
2870
+ };
2871
+ }
2872
+
2873
+ // src/tools/delivery-abandon.js
2874
+ import { join as join10 } from "node:path";
2875
+ import { spawnSync as spawnSync4 } from "node:child_process";
2876
+ import { existsSync as existsSync8 } from "node:fs";
2877
+
2878
+ // src/state/abandon-store.js
2879
+ import { readFile as readFile7 } from "node:fs/promises";
2880
+ import { join as join8 } from "node:path";
2881
+ import { createHash as createHash7 } from "node:crypto";
2882
+ var SAFE_ID_RE2 = /^[A-Za-z0-9._-]{1,128}$/;
2883
+ var HASH_RE2 = /^[0-9a-f]{64}$/;
2884
+ function abandonDir(commonDir, taskId) {
2885
+ return join8(commonDir, "opencode-ship", "delivery", "abandoned", taskId);
2886
+ }
2887
+ function intentPathFor(commonDir, taskId) {
2888
+ return join8(abandonDir(commonDir, taskId), "intent.json");
2889
+ }
2890
+ function completionPathFor(commonDir, taskId) {
2891
+ return join8(abandonDir(commonDir, taskId), "completion.json");
2892
+ }
2893
+ async function readJsonOrNull2(path) {
2894
+ try {
2895
+ return JSON.parse(await readFile7(path, "utf8"));
2896
+ } catch (err) {
2897
+ if (err?.code === "ENOENT") return null;
2898
+ throw err;
2899
+ }
2900
+ }
2901
+ function withoutIntentHash(record) {
2902
+ const copy = { ...record ?? {} };
2903
+ delete copy.intentHash;
2904
+ return copy;
2905
+ }
2906
+ function hashAbandonIntent(record) {
2907
+ return createHash7("sha256").update(canonicalJson(withoutIntentHash(record)), "utf8").digest("hex");
2908
+ }
2909
+ async function readAbandon(repoRoot, taskId) {
2910
+ const commonDir = await resolveGitCommonDir(repoRoot);
2911
+ if (!SAFE_ID_RE2.test(String(taskId ?? ""))) {
2912
+ return { intent: null, completion: null };
2913
+ }
2914
+ return {
2915
+ intent: await readJsonOrNull2(intentPathFor(commonDir, taskId)),
2916
+ completion: await readJsonOrNull2(completionPathFor(commonDir, taskId))
2917
+ };
2918
+ }
2919
+ async function publishOrReuse(path, record) {
2920
+ try {
2921
+ await publishImmutableJson(path, record);
2922
+ return { ok: true, record, idempotent: false };
2923
+ } catch (err) {
2924
+ const message = String(err?.message ?? err);
2925
+ if (!message.includes("already exists")) throw err;
2926
+ const existing = await readJsonOrNull2(path);
2927
+ if (existing && canonicalJson(existing) === canonicalJson(record)) {
2928
+ return { ok: true, record: existing, idempotent: true };
2929
+ }
2930
+ return { ok: false, kind: "abandon-conflict" };
2931
+ }
2932
+ }
2933
+ async function publishAbandonIntent(repoRoot, record) {
2934
+ const taskId = String(record?.taskId ?? "");
2935
+ if (!SAFE_ID_RE2.test(taskId)) {
2936
+ return { ok: false, kind: "invalid-task-id" };
2937
+ }
2938
+ const sealed = { ...withoutIntentHash(record), intentHash: hashAbandonIntent(record) };
2939
+ const commonDir = await resolveGitCommonDir(repoRoot);
2940
+ return publishOrReuse(intentPathFor(commonDir, taskId), sealed);
2941
+ }
2942
+ async function publishAbandonCompletion(repoRoot, record) {
2943
+ const taskId = String(record?.taskId ?? "");
2944
+ const intentHash = String(record?.intentHash ?? "");
2945
+ if (!SAFE_ID_RE2.test(taskId)) {
2946
+ return { ok: false, kind: "invalid-task-id" };
2947
+ }
2948
+ if (!HASH_RE2.test(intentHash)) {
2949
+ return { ok: false, kind: "invalid-intent-hash" };
2950
+ }
2951
+ const commonDir = await resolveGitCommonDir(repoRoot);
2952
+ return publishOrReuse(completionPathFor(commonDir, taskId), record);
2953
+ }
2954
+
2955
+ // src/skills/worktree.js
2956
+ import { execFile } from "node:child_process";
2957
+ import { promises as fs, existsSync as existsSync7 } from "node:fs";
2958
+ import { resolve as resolve9, dirname as dirname3, sep, isAbsolute, join as join9 } from "node:path";
2959
+ function listRegisteredWorktrees(mainRepo) {
2960
+ return new Promise((resolveP, rejectP) => {
2961
+ execFile(
2962
+ "git",
2963
+ ["-C", mainRepo, "worktree", "list", "--porcelain", "-z"],
2964
+ { shell: false, maxBuffer: 1024 * 1024 },
2965
+ (err, stdout) => {
2966
+ if (err) return rejectP(err);
2967
+ const records = parsePorcelain(stdout);
2968
+ const mainRecord = records.shift();
2969
+ const mainPath = mainRecord?.worktree ? resolve9(mainRecord.worktree) : null;
2970
+ const linked = [];
2971
+ for (const r of records) {
2972
+ if (!r.worktree) continue;
2973
+ const p = resolve9(r.worktree);
2974
+ if (mainPath && p === mainPath) continue;
2975
+ linked.push({ path: p, branch: r.HEAD ?? null });
2976
+ }
2977
+ resolveP(linked);
2978
+ }
2979
+ );
2980
+ });
2981
+ }
2982
+ function parsePorcelain(text) {
2983
+ const tokens = text.split("\0");
2984
+ const out = [];
2985
+ let current = {};
2986
+ for (const tok of tokens) {
2987
+ if (tok.length === 0) {
2988
+ if (Object.keys(current).length > 0) {
2989
+ out.push(current);
2990
+ current = {};
2991
+ }
2992
+ continue;
2993
+ }
2994
+ const idx = tok.indexOf(" ");
2995
+ const key = idx === -1 ? tok : tok.slice(0, idx);
2996
+ const value = idx === -1 ? "" : tok.slice(idx + 1);
2997
+ if (key === "branch") {
2998
+ current.HEAD = value.startsWith("refs/heads/") ? value : `refs/heads/${value}`;
2999
+ } else {
3000
+ current[key] = value;
3001
+ }
3002
+ }
3003
+ if (Object.keys(current).length > 0) out.push(current);
3004
+ return out;
3005
+ }
3006
+ async function validateLinkedWorktree(mainRepo, worktreePath, options = {}) {
3007
+ const main = resolve9(mainRepo);
3008
+ if (!existsSync7(main)) {
3009
+ return { ok: false, kind: "missing", message: `main repository ${main} does not exist` };
3010
+ }
3011
+ if (!worktreePath) {
3012
+ return { ok: false, kind: "unlinked", message: "worktreePath is required" };
3013
+ }
3014
+ const wt = resolve9(worktreePath);
3015
+ if (!existsSync7(wt)) {
3016
+ return { ok: false, kind: "missing", message: `worktree ${wt} does not exist` };
3017
+ }
3018
+ const isCurrent = wt === main;
3019
+ if (isCurrent) {
3020
+ const gitEntry = options.allowCurrentLinked ? await fs.lstat(join9(main, ".git")).catch(() => null) : null;
3021
+ if (!gitEntry?.isFile()) {
3022
+ return { ok: false, kind: "main", message: "installs into the main worktree are forbidden" };
3023
+ }
3024
+ }
3025
+ let cursor = wt;
3026
+ while (cursor !== dirname3(cursor)) {
3027
+ const stat2 = await fs.lstat(cursor).catch(() => null);
3028
+ if (stat2?.isSymbolicLink()) {
3029
+ return {
3030
+ ok: false,
3031
+ kind: "ancestor-symlink",
3032
+ message: `worktree path contains a symlink at ${cursor}`
3033
+ };
3034
+ }
3035
+ cursor = dirname3(cursor);
3036
+ }
3037
+ const real = await fs.realpath(wt).catch(() => null);
3038
+ if (real && real !== wt) {
3039
+ return {
3040
+ ok: false,
3041
+ kind: "symlink",
3042
+ message: `worktree ${wt} resolves through a symlink to ${real}`
3043
+ };
3044
+ }
3045
+ const linked = await listRegisteredWorktrees(main);
3046
+ const matched = linked.find((entry) => entry.path === wt);
3047
+ if (!matched) {
3048
+ return {
3049
+ ok: false,
3050
+ kind: "unlinked",
3051
+ message: `worktree ${wt} is not registered (git worktree list)`
2774
3052
  };
3053
+ }
3054
+ return { ok: true, path: wt, registered: !!matched };
3055
+ }
3056
+
3057
+ // src/tools/envelope.js
3058
+ import { randomBytes as randomBytes2 } from "node:crypto";
3059
+ var CONTRACT_VERSION = 2;
3060
+ function operationId(prefix = "op") {
3061
+ return `${prefix}-${Date.now().toString(36)}-${randomBytes2(4).toString("hex")}`;
3062
+ }
3063
+ function success(kind, data, options = {}) {
3064
+ if (typeof kind !== "string" || kind.length === 0) {
3065
+ throw new Error("envelope.success: kind must be a non-empty string");
3066
+ }
3067
+ return {
3068
+ contractVersion: CONTRACT_VERSION,
3069
+ ok: true,
3070
+ kind,
3071
+ operationId: options.operationId ?? operationId(kind),
3072
+ idempotent: options.idempotent !== false,
3073
+ data
3074
+ };
3075
+ }
3076
+ function failure(kind, message, options = {}) {
3077
+ if (typeof kind !== "string" || kind.length === 0) {
3078
+ throw new Error("envelope.failure: kind must be a non-empty string");
3079
+ }
3080
+ if (typeof message !== "string" || message.length === 0) {
3081
+ throw new Error("envelope.failure: message must be a non-empty string");
3082
+ }
3083
+ const details = options.details ?? {};
3084
+ return {
3085
+ contractVersion: CONTRACT_VERSION,
3086
+ ok: false,
3087
+ kind,
3088
+ operationId: options.operationId ?? operationId(`${kind}-err`),
3089
+ retryable: options.retryable === true,
3090
+ message,
3091
+ details
3092
+ };
3093
+ }
3094
+
3095
+ // src/tools/delivery-abandon.js
3096
+ import { readFile as readFile8 } from "node:fs/promises";
3097
+ function runGit2(args, cwd) {
3098
+ return spawnSync4("git", args, {
3099
+ cwd,
3100
+ encoding: "utf8",
3101
+ stdio: ["ignore", "pipe", "pipe"],
3102
+ env: process.env
3103
+ });
3104
+ }
3105
+ function defaultRemoveWorktree(repoRoot, path) {
3106
+ return runGit2(["worktree", "remove", path], repoRoot);
3107
+ }
3108
+ function defaultDeleteBranch(repoRoot, branch, expectedSha) {
3109
+ const args = ["update-ref", "-d", `refs/heads/${branch}`];
3110
+ if (expectedSha && /^[0-9a-f]{7,}$/i.test(expectedSha)) args.push(expectedSha);
3111
+ return runGit2(args, repoRoot);
3112
+ }
3113
+ function branchExists(repoRoot, branch) {
3114
+ return runGit2(["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], repoRoot).status === 0;
3115
+ }
3116
+ function remoteBranchHead(repoRoot, remote, branch) {
3117
+ const r = runGit2(["ls-remote", "--heads", remote, branch], repoRoot);
3118
+ if (r.status !== 0) return { ok: false, sha: null, present: false };
3119
+ const line = r.stdout.split("\n").find((row) => row.includes(`refs/heads/${branch}`));
3120
+ if (!line) return { ok: true, sha: null, present: false };
3121
+ return { ok: true, sha: line.split(/\s+/)[0], present: true };
3122
+ }
3123
+ function unpublishedAhead(repoRoot, remote, branch) {
3124
+ const r = runGit2(["rev-list", "--count", `${remote}/${branch}..${branch}`], repoRoot);
3125
+ if (r.status !== 0) return null;
3126
+ const n = parseInt(r.stdout.trim(), 10);
3127
+ return Number.isFinite(n) ? n : null;
3128
+ }
3129
+ function refuse(opId, kind, extras = {}) {
3130
+ return failure("abandon", kind, {
3131
+ operationId: opId,
3132
+ retryable: false,
3133
+ details: { kind, ...extras }
3134
+ });
3135
+ }
3136
+ async function readRunSnapshot(repoRoot, workflowId) {
3137
+ try {
3138
+ const common = await resolveGitCommonDir(repoRoot);
3139
+ const path = join10(opencodeShipStateDir(common), "runs", workflowId, "run.json");
3140
+ if (!existsSync8(path)) return null;
3141
+ return JSON.parse(await readFile8(path, "utf8"));
3142
+ } catch {
3143
+ return null;
3144
+ }
3145
+ }
3146
+ async function validateLiveAttempt({ deps, manifest, subject, opId }) {
3147
+ if (!manifest.prNumber) return refuse(opId, "missing-pr");
3148
+ if (!manifest.worktreePath) return refuse(opId, "missing-worktree-path");
3149
+ const pr = await deps.driver.readPullRequest({
3150
+ repo: deps.repoSlug,
3151
+ number: manifest.prNumber
3152
+ });
3153
+ if (pr.merged || pr.state === "MERGED") return refuse(opId, "pr-merged");
3154
+ if (pr.state !== "CLOSED") return refuse(opId, "pr-open");
3155
+ if (pr.headRefName && pr.headRefName !== manifest.branch) {
3156
+ return refuse(opId, "branch-mismatch", { expected: manifest.branch, received: pr.headRefName });
3157
+ }
3158
+ if (pr.baseRefName && pr.baseRefName !== manifest.baseBranch) {
3159
+ return refuse(opId, "base-mismatch", { expected: manifest.baseBranch, received: pr.baseRefName });
3160
+ }
3161
+ const linked = await validateLinkedWorktree(deps.repoRoot, manifest.worktreePath);
3162
+ if (!linked.ok) {
3163
+ return refuse(opId, "invalid-worktree", { reason: linked.kind, message: linked.message });
3164
+ }
3165
+ if (!isWorktreeClean(linked.path)) return refuse(opId, "dirty-worktree");
3166
+ if (isRebaseInProgress(linked.path)) return refuse(opId, "rebase-in-progress");
3167
+ const head = currentHead(linked.path);
3168
+ if (!head || head !== manifest.lastPrHeadSha || head !== pr.headSha) {
3169
+ return refuse(opId, "head-mismatch", {
3170
+ local: head ?? "",
3171
+ manifest: manifest.lastPrHeadSha ?? "",
3172
+ pr: pr.headSha ?? ""
3173
+ });
3174
+ }
3175
+ const remote = deps.remote ?? "origin";
3176
+ const remoteHead = remoteBranchHead(linked.path, remote, manifest.branch);
3177
+ if (remoteHead.ok && remoteHead.present && remoteHead.sha !== head) {
3178
+ return refuse(opId, "remote-diverged", { local: head, remote: remoteHead.sha });
3179
+ }
3180
+ const ahead = unpublishedAhead(linked.path, remote, manifest.branch);
3181
+ if (ahead !== null && ahead > 0) {
3182
+ return refuse(opId, "has-unpublished-commits", { ahead, branch: manifest.branch, remote });
3183
+ }
3184
+ if (manifest.workflowId) {
3185
+ const snapshot = await readRunSnapshot(deps.repoRoot, manifest.workflowId);
3186
+ if (snapshot?.state === "ready") return refuse(opId, "workflow-ready");
3187
+ if (snapshot?.state === "merged") return refuse(opId, "workflow-merged");
3188
+ }
3189
+ return {
3190
+ ok: true,
3191
+ intent: {
3192
+ schemaVersion: 1,
3193
+ taskId: manifest.taskId,
3194
+ issueNumber: manifest.issueNumber,
3195
+ prNumber: manifest.prNumber,
3196
+ branch: manifest.branch,
3197
+ worktreePath: linked.path,
3198
+ headSha: head,
3199
+ workflowId: manifest.workflowId ?? null,
3200
+ subject,
3201
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString()
3202
+ }
3203
+ };
3204
+ }
3205
+ async function resumeCleanup({ deps, intent, opId }) {
3206
+ const removeWorktree = deps.removeWorktree ?? defaultRemoveWorktree;
3207
+ const deleteBranch = deps.deleteBranch ?? defaultDeleteBranch;
3208
+ const removeManifest = deps.deleteManifest ?? deleteManifest;
3209
+ let removedWorktree = !existsSync8(intent.worktreePath);
3210
+ if (!removedWorktree) {
3211
+ let removed;
3212
+ try {
3213
+ removed = await Promise.resolve(removeWorktree(deps.repoRoot, intent.worktreePath));
3214
+ } catch (err) {
3215
+ return refuse(opId, "remove-failed", { stderr: String(err?.message ?? err) });
3216
+ }
3217
+ if (removed?.status !== 0 && existsSync8(intent.worktreePath)) {
3218
+ return refuse(opId, "remove-failed", { stderr: removed?.stderr ?? "" });
3219
+ }
3220
+ removedWorktree = !existsSync8(intent.worktreePath);
3221
+ }
3222
+ let deletedBranch = !branchExists(deps.repoRoot, intent.branch);
3223
+ if (!deletedBranch) {
3224
+ let deleted;
3225
+ try {
3226
+ deleted = await Promise.resolve(deleteBranch(deps.repoRoot, intent.branch, intent.headSha));
3227
+ } catch (err) {
3228
+ return refuse(opId, "branch-delete-failed", { stderr: String(err?.message ?? err) });
3229
+ }
3230
+ if (deleted?.status !== 0 && branchExists(deps.repoRoot, intent.branch)) {
3231
+ return refuse(opId, "branch-delete-failed", { stderr: deleted?.stderr ?? "" });
3232
+ }
3233
+ deletedBranch = !branchExists(deps.repoRoot, intent.branch);
3234
+ }
3235
+ try {
3236
+ await Promise.resolve(removeManifest(deps.repoRoot, intent.taskId));
3237
+ } catch (err) {
3238
+ return refuse(opId, "manifest-delete-failed", { stderr: String(err?.message ?? err) });
3239
+ }
3240
+ const remaining = await readManifest(deps.repoRoot, intent.taskId);
3241
+ const deletedManifest = remaining === null;
3242
+ const completion = {
3243
+ schemaVersion: 1,
3244
+ taskId: intent.taskId,
3245
+ intentHash: intent.intentHash,
3246
+ removedWorktree,
3247
+ deletedBranch,
3248
+ deletedManifest,
3249
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
3250
+ };
3251
+ const published = await publishAbandonCompletion(deps.repoRoot, completion);
3252
+ if (!published.ok) return refuse(opId, published.kind);
3253
+ return success("abandon", {
3254
+ taskId: intent.taskId,
3255
+ intentHash: intent.intentHash,
3256
+ removedWorktree,
3257
+ deletedBranch,
3258
+ deletedManifest
3259
+ }, { operationId: opId, idempotent: published.idempotent === true });
3260
+ }
3261
+ function createAbandonTool(deps) {
3262
+ return async function abandon(input) {
3263
+ const opId = input.operationId ?? `abandon-${Date.now().toString(36)}`;
3264
+ const taskId = String(input.taskId ?? "");
3265
+ const subject = String(input.subject ?? "").trim();
3266
+ if (!taskId) return refuse(opId, "missing-input", { field: "taskId" });
3267
+ if (!subject) return refuse(opId, "missing-input", { field: "subject" });
3268
+ const existing = await readAbandon(deps.repoRoot, taskId);
3269
+ if (existing.completion) {
3270
+ return success("abandon", {
3271
+ taskId,
3272
+ intentHash: existing.completion.intentHash,
3273
+ removedWorktree: existing.completion.removedWorktree,
3274
+ deletedBranch: existing.completion.deletedBranch,
3275
+ deletedManifest: existing.completion.deletedManifest
3276
+ }, { operationId: opId, idempotent: true });
3277
+ }
3278
+ if (existing.intent) {
3279
+ if (existing.intent.subject !== subject || existing.intent.taskId !== taskId) {
3280
+ return refuse(opId, "abandon-conflict");
3281
+ }
3282
+ return resumeCleanup({ deps, intent: existing.intent, opId });
3283
+ }
3284
+ const manifest = await readManifest(deps.repoRoot, taskId);
3285
+ if (!manifest) return refuse(opId, "missing-manifest", { taskId });
3286
+ const validated = await validateLiveAttempt({ deps, manifest, subject, opId });
3287
+ if (!validated.ok) return validated;
3288
+ const publishedIntent = await publishAbandonIntent(deps.repoRoot, validated.intent);
3289
+ if (!publishedIntent.ok) return refuse(opId, publishedIntent.kind);
3290
+ return resumeCleanup({ deps, intent: publishedIntent.record, opId });
2775
3291
  };
2776
3292
  }
2777
3293
 
2778
3294
  // src/version.js
2779
- import { readFileSync, existsSync as existsSync7 } from "node:fs";
2780
- import { dirname as dirname3, resolve as resolve9 } from "node:path";
3295
+ import { readFileSync, existsSync as existsSync9 } from "node:fs";
3296
+ import { dirname as dirname4, resolve as resolve10 } from "node:path";
2781
3297
  import { fileURLToPath } from "node:url";
2782
- var PACKAGE_VERSION = "1.1.7";
3298
+ var PACKAGE_VERSION = "1.1.9-rc.1";
2783
3299
  var TEMPLATE_SET = `v${PACKAGE_VERSION}`;
2784
3300
  export {
2785
3301
  ADAPTER_CONTRACT_VERSION,
@@ -2793,6 +3309,7 @@ export {
2793
3309
  bucketFor,
2794
3310
  canTransition,
2795
3311
  checkGates,
3312
+ createAbandonTool,
2796
3313
  createCleanupTool,
2797
3314
  createGhDriver,
2798
3315
  createGhStub,