opencode-ship 1.1.5 → 1.1.8

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/dist/core.js CHANGED
@@ -1,4 +1,4 @@
1
- // opencode-ship/core v1.1.5
1
+ // opencode-ship/core v1.1.8
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((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) => resolve9({ status: status ?? -1, stdout, stderr }));
866
+ proc.on("close", (status) => resolve11({ status: status ?? -1, stdout, stderr }));
867
867
  });
868
868
  }
869
869
  function viewFields() {
@@ -1532,6 +1532,9 @@ function createWorktreeTool(deps) {
1532
1532
  expectedRoot: resolve7(deps.repoRoot, worktreeRoot)
1533
1533
  };
1534
1534
  }
1535
+ if (m.schemaVersion >= 2 && !m.workflowId) {
1536
+ return { kind: "missing-workflow-link", taskId: m.taskId };
1537
+ }
1535
1538
  const remote = deps.remote ?? "origin";
1536
1539
  const hasRemote = remoteExists(remote, deps.repoRoot);
1537
1540
  if (hasRemote) {
@@ -2527,6 +2530,42 @@ function createMergeTool(deps) {
2527
2530
  } else if (m.schemaVersion >= 2) {
2528
2531
  return { kind: "missing-workflow-link", taskId: m.taskId };
2529
2532
  }
2533
+ if (pr.headSha !== (m.lastPrHeadSha ?? pr.headSha)) {
2534
+ return {
2535
+ kind: "head-changed",
2536
+ headSha: pr.headSha,
2537
+ manifestSha: m.lastPrHeadSha ?? ""
2538
+ };
2539
+ }
2540
+ async function recordMerged(merged2, reason) {
2541
+ const t = transition(
2542
+ { ...m, lastPrHeadSha: merged2.headSha },
2543
+ "merged",
2544
+ { reason }
2545
+ );
2546
+ if (!t.ok) return { kind: "lifecycle", reason: t.reason };
2547
+ const next = {
2548
+ ...m,
2549
+ lastPrHeadSha: merged2.headSha,
2550
+ state: t.to,
2551
+ transitionLog: [
2552
+ ...m.transitionLog,
2553
+ { from: t.from, to: t.to, at: t.at, reason: t.reason }
2554
+ ],
2555
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2556
+ };
2557
+ const path = await writeManifest(deps.repoRoot, next);
2558
+ if (finalEvidence && m.workflowId) {
2559
+ await appendRunEvent(deps.repoRoot, m.workflowId, finalEvidence.runState, {
2560
+ kind: RUN_EVENT_KINDS.MERGE,
2561
+ data: { mergeSha: merged2.mergeSha ?? merged2.mergeCommitSha ?? merged2.headSha }
2562
+ });
2563
+ }
2564
+ return { kind: "merge", contractVersion: 1, manifestPath: path, pr: m.prNumber, taskId: m.taskId };
2565
+ }
2566
+ if (pr.merged) {
2567
+ return recordMerged(pr, `reconciled external merge as ${input.subject}`);
2568
+ }
2530
2569
  if (freshGates) {
2531
2570
  const required = deps.adapter?.ready?.requires ?? [
2532
2571
  "review",
@@ -2553,13 +2592,6 @@ function createMergeTool(deps) {
2553
2592
  });
2554
2593
  if (!result.ok) return gateFailureEnvelope(result);
2555
2594
  }
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
2595
  if (pr.draft) return { kind: "not-mergeable", reason: "PR is still draft" };
2564
2596
  if (pr.mergeable !== "MERGEABLE") {
2565
2597
  return { kind: "not-mergeable", reason: `mergeable=${pr.mergeable}` };
@@ -2569,30 +2601,7 @@ function createMergeTool(deps) {
2569
2601
  number: m.prNumber,
2570
2602
  subject: input.subject
2571
2603
  });
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 };
2604
+ return recordMerged(merged, `squash merged as ${input.subject}`);
2596
2605
  };
2597
2606
  }
2598
2607
 
@@ -2769,8 +2778,433 @@ function createCleanupTool(deps) {
2769
2778
  };
2770
2779
  }
2771
2780
 
2772
- // src/index.js
2773
- var PACKAGE_VERSION = "0.1.3";
2781
+ // src/tools/delivery-abandon.js
2782
+ import { join as join10 } from "node:path";
2783
+ import { spawnSync as spawnSync4 } from "node:child_process";
2784
+ import { existsSync as existsSync8 } from "node:fs";
2785
+
2786
+ // src/state/abandon-store.js
2787
+ import { readFile as readFile7 } from "node:fs/promises";
2788
+ import { join as join8 } from "node:path";
2789
+ import { createHash as createHash7 } from "node:crypto";
2790
+ var SAFE_ID_RE2 = /^[A-Za-z0-9._-]{1,128}$/;
2791
+ var HASH_RE2 = /^[0-9a-f]{64}$/;
2792
+ function abandonDir(commonDir, taskId) {
2793
+ return join8(commonDir, "opencode-ship", "delivery", "abandoned", taskId);
2794
+ }
2795
+ function intentPathFor(commonDir, taskId) {
2796
+ return join8(abandonDir(commonDir, taskId), "intent.json");
2797
+ }
2798
+ function completionPathFor(commonDir, taskId) {
2799
+ return join8(abandonDir(commonDir, taskId), "completion.json");
2800
+ }
2801
+ async function readJsonOrNull2(path) {
2802
+ try {
2803
+ return JSON.parse(await readFile7(path, "utf8"));
2804
+ } catch (err) {
2805
+ if (err?.code === "ENOENT") return null;
2806
+ throw err;
2807
+ }
2808
+ }
2809
+ function withoutIntentHash(record) {
2810
+ const copy = { ...record ?? {} };
2811
+ delete copy.intentHash;
2812
+ return copy;
2813
+ }
2814
+ function hashAbandonIntent(record) {
2815
+ return createHash7("sha256").update(canonicalJson(withoutIntentHash(record)), "utf8").digest("hex");
2816
+ }
2817
+ async function readAbandon(repoRoot, taskId) {
2818
+ const commonDir = await resolveGitCommonDir(repoRoot);
2819
+ if (!SAFE_ID_RE2.test(String(taskId ?? ""))) {
2820
+ return { intent: null, completion: null };
2821
+ }
2822
+ return {
2823
+ intent: await readJsonOrNull2(intentPathFor(commonDir, taskId)),
2824
+ completion: await readJsonOrNull2(completionPathFor(commonDir, taskId))
2825
+ };
2826
+ }
2827
+ async function publishOrReuse(path, record) {
2828
+ try {
2829
+ await publishImmutableJson(path, record);
2830
+ return { ok: true, record, idempotent: false };
2831
+ } catch (err) {
2832
+ const message = String(err?.message ?? err);
2833
+ if (!message.includes("already exists")) throw err;
2834
+ const existing = await readJsonOrNull2(path);
2835
+ if (existing && canonicalJson(existing) === canonicalJson(record)) {
2836
+ return { ok: true, record: existing, idempotent: true };
2837
+ }
2838
+ return { ok: false, kind: "abandon-conflict" };
2839
+ }
2840
+ }
2841
+ async function publishAbandonIntent(repoRoot, record) {
2842
+ const taskId = String(record?.taskId ?? "");
2843
+ if (!SAFE_ID_RE2.test(taskId)) {
2844
+ return { ok: false, kind: "invalid-task-id" };
2845
+ }
2846
+ const sealed = { ...withoutIntentHash(record), intentHash: hashAbandonIntent(record) };
2847
+ const commonDir = await resolveGitCommonDir(repoRoot);
2848
+ return publishOrReuse(intentPathFor(commonDir, taskId), sealed);
2849
+ }
2850
+ async function publishAbandonCompletion(repoRoot, record) {
2851
+ const taskId = String(record?.taskId ?? "");
2852
+ const intentHash = String(record?.intentHash ?? "");
2853
+ if (!SAFE_ID_RE2.test(taskId)) {
2854
+ return { ok: false, kind: "invalid-task-id" };
2855
+ }
2856
+ if (!HASH_RE2.test(intentHash)) {
2857
+ return { ok: false, kind: "invalid-intent-hash" };
2858
+ }
2859
+ const commonDir = await resolveGitCommonDir(repoRoot);
2860
+ return publishOrReuse(completionPathFor(commonDir, taskId), record);
2861
+ }
2862
+
2863
+ // src/skills/worktree.js
2864
+ import { execFile } from "node:child_process";
2865
+ import { promises as fs, existsSync as existsSync7 } from "node:fs";
2866
+ import { resolve as resolve9, dirname as dirname3, sep, isAbsolute, join as join9 } from "node:path";
2867
+ function listRegisteredWorktrees(mainRepo) {
2868
+ return new Promise((resolveP, rejectP) => {
2869
+ execFile(
2870
+ "git",
2871
+ ["-C", mainRepo, "worktree", "list", "--porcelain", "-z"],
2872
+ { shell: false, maxBuffer: 1024 * 1024 },
2873
+ (err, stdout) => {
2874
+ if (err) return rejectP(err);
2875
+ const records = parsePorcelain(stdout);
2876
+ const mainRecord = records.shift();
2877
+ const mainPath = mainRecord?.worktree ? resolve9(mainRecord.worktree) : null;
2878
+ const linked = [];
2879
+ for (const r of records) {
2880
+ if (!r.worktree) continue;
2881
+ const p = resolve9(r.worktree);
2882
+ if (mainPath && p === mainPath) continue;
2883
+ linked.push({ path: p, branch: r.HEAD ?? null });
2884
+ }
2885
+ resolveP(linked);
2886
+ }
2887
+ );
2888
+ });
2889
+ }
2890
+ function parsePorcelain(text) {
2891
+ const tokens = text.split("\0");
2892
+ const out = [];
2893
+ let current = {};
2894
+ for (const tok of tokens) {
2895
+ if (tok.length === 0) {
2896
+ if (Object.keys(current).length > 0) {
2897
+ out.push(current);
2898
+ current = {};
2899
+ }
2900
+ continue;
2901
+ }
2902
+ const idx = tok.indexOf(" ");
2903
+ const key = idx === -1 ? tok : tok.slice(0, idx);
2904
+ const value = idx === -1 ? "" : tok.slice(idx + 1);
2905
+ if (key === "branch") {
2906
+ current.HEAD = value.startsWith("refs/heads/") ? value : `refs/heads/${value}`;
2907
+ } else {
2908
+ current[key] = value;
2909
+ }
2910
+ }
2911
+ if (Object.keys(current).length > 0) out.push(current);
2912
+ return out;
2913
+ }
2914
+ async function validateLinkedWorktree(mainRepo, worktreePath, options = {}) {
2915
+ const main = resolve9(mainRepo);
2916
+ if (!existsSync7(main)) {
2917
+ return { ok: false, kind: "missing", message: `main repository ${main} does not exist` };
2918
+ }
2919
+ if (!worktreePath) {
2920
+ return { ok: false, kind: "unlinked", message: "worktreePath is required" };
2921
+ }
2922
+ const wt = resolve9(worktreePath);
2923
+ if (!existsSync7(wt)) {
2924
+ return { ok: false, kind: "missing", message: `worktree ${wt} does not exist` };
2925
+ }
2926
+ const isCurrent = wt === main;
2927
+ if (isCurrent) {
2928
+ const gitEntry = options.allowCurrentLinked ? await fs.lstat(join9(main, ".git")).catch(() => null) : null;
2929
+ if (!gitEntry?.isFile()) {
2930
+ return { ok: false, kind: "main", message: "installs into the main worktree are forbidden" };
2931
+ }
2932
+ }
2933
+ let cursor = wt;
2934
+ while (cursor !== dirname3(cursor)) {
2935
+ const stat2 = await fs.lstat(cursor).catch(() => null);
2936
+ if (stat2?.isSymbolicLink()) {
2937
+ return {
2938
+ ok: false,
2939
+ kind: "ancestor-symlink",
2940
+ message: `worktree path contains a symlink at ${cursor}`
2941
+ };
2942
+ }
2943
+ cursor = dirname3(cursor);
2944
+ }
2945
+ const real = await fs.realpath(wt).catch(() => null);
2946
+ if (real && real !== wt) {
2947
+ return {
2948
+ ok: false,
2949
+ kind: "symlink",
2950
+ message: `worktree ${wt} resolves through a symlink to ${real}`
2951
+ };
2952
+ }
2953
+ const linked = await listRegisteredWorktrees(main);
2954
+ const matched = linked.find((entry) => entry.path === wt);
2955
+ if (!matched) {
2956
+ return {
2957
+ ok: false,
2958
+ kind: "unlinked",
2959
+ message: `worktree ${wt} is not registered (git worktree list)`
2960
+ };
2961
+ }
2962
+ return { ok: true, path: wt, registered: !!matched };
2963
+ }
2964
+
2965
+ // src/tools/envelope.js
2966
+ import { randomBytes as randomBytes2 } from "node:crypto";
2967
+ var CONTRACT_VERSION = 2;
2968
+ function operationId(prefix = "op") {
2969
+ return `${prefix}-${Date.now().toString(36)}-${randomBytes2(4).toString("hex")}`;
2970
+ }
2971
+ function success(kind, data, options = {}) {
2972
+ if (typeof kind !== "string" || kind.length === 0) {
2973
+ throw new Error("envelope.success: kind must be a non-empty string");
2974
+ }
2975
+ return {
2976
+ contractVersion: CONTRACT_VERSION,
2977
+ ok: true,
2978
+ kind,
2979
+ operationId: options.operationId ?? operationId(kind),
2980
+ idempotent: options.idempotent !== false,
2981
+ data
2982
+ };
2983
+ }
2984
+ function failure(kind, message, options = {}) {
2985
+ if (typeof kind !== "string" || kind.length === 0) {
2986
+ throw new Error("envelope.failure: kind must be a non-empty string");
2987
+ }
2988
+ if (typeof message !== "string" || message.length === 0) {
2989
+ throw new Error("envelope.failure: message must be a non-empty string");
2990
+ }
2991
+ const details = options.details ?? {};
2992
+ return {
2993
+ contractVersion: CONTRACT_VERSION,
2994
+ ok: false,
2995
+ kind,
2996
+ operationId: options.operationId ?? operationId(`${kind}-err`),
2997
+ retryable: options.retryable === true,
2998
+ message,
2999
+ details
3000
+ };
3001
+ }
3002
+
3003
+ // src/tools/delivery-abandon.js
3004
+ import { readFile as readFile8 } from "node:fs/promises";
3005
+ function runGit2(args, cwd) {
3006
+ return spawnSync4("git", args, {
3007
+ cwd,
3008
+ encoding: "utf8",
3009
+ stdio: ["ignore", "pipe", "pipe"],
3010
+ env: process.env
3011
+ });
3012
+ }
3013
+ function defaultRemoveWorktree(repoRoot, path) {
3014
+ return runGit2(["worktree", "remove", path], repoRoot);
3015
+ }
3016
+ function defaultDeleteBranch(repoRoot, branch, expectedSha) {
3017
+ const args = ["update-ref", "-d", `refs/heads/${branch}`];
3018
+ if (expectedSha && /^[0-9a-f]{7,}$/i.test(expectedSha)) args.push(expectedSha);
3019
+ return runGit2(args, repoRoot);
3020
+ }
3021
+ function branchExists(repoRoot, branch) {
3022
+ return runGit2(["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], repoRoot).status === 0;
3023
+ }
3024
+ function remoteBranchHead(repoRoot, remote, branch) {
3025
+ const r = runGit2(["ls-remote", "--heads", remote, branch], repoRoot);
3026
+ if (r.status !== 0) return { ok: false, sha: null, present: false };
3027
+ const line = r.stdout.split("\n").find((row) => row.includes(`refs/heads/${branch}`));
3028
+ if (!line) return { ok: true, sha: null, present: false };
3029
+ return { ok: true, sha: line.split(/\s+/)[0], present: true };
3030
+ }
3031
+ function unpublishedAhead(repoRoot, remote, branch) {
3032
+ const r = runGit2(["rev-list", "--count", `${remote}/${branch}..${branch}`], repoRoot);
3033
+ if (r.status !== 0) return null;
3034
+ const n = parseInt(r.stdout.trim(), 10);
3035
+ return Number.isFinite(n) ? n : null;
3036
+ }
3037
+ function refuse(opId, kind, extras = {}) {
3038
+ return failure("abandon", kind, {
3039
+ operationId: opId,
3040
+ retryable: false,
3041
+ details: { kind, ...extras }
3042
+ });
3043
+ }
3044
+ async function readRunSnapshot(repoRoot, workflowId) {
3045
+ try {
3046
+ const common = await resolveGitCommonDir(repoRoot);
3047
+ const path = join10(opencodeShipStateDir(common), "runs", workflowId, "run.json");
3048
+ if (!existsSync8(path)) return null;
3049
+ return JSON.parse(await readFile8(path, "utf8"));
3050
+ } catch {
3051
+ return null;
3052
+ }
3053
+ }
3054
+ async function validateLiveAttempt({ deps, manifest, subject, opId }) {
3055
+ if (!manifest.prNumber) return refuse(opId, "missing-pr");
3056
+ if (!manifest.worktreePath) return refuse(opId, "missing-worktree-path");
3057
+ const pr = await deps.driver.readPullRequest({
3058
+ repo: deps.repoSlug,
3059
+ number: manifest.prNumber
3060
+ });
3061
+ if (pr.merged || pr.state === "MERGED") return refuse(opId, "pr-merged");
3062
+ if (pr.state !== "CLOSED") return refuse(opId, "pr-open");
3063
+ if (pr.headRefName && pr.headRefName !== manifest.branch) {
3064
+ return refuse(opId, "branch-mismatch", { expected: manifest.branch, received: pr.headRefName });
3065
+ }
3066
+ if (pr.baseRefName && pr.baseRefName !== manifest.baseBranch) {
3067
+ return refuse(opId, "base-mismatch", { expected: manifest.baseBranch, received: pr.baseRefName });
3068
+ }
3069
+ const linked = await validateLinkedWorktree(deps.repoRoot, manifest.worktreePath);
3070
+ if (!linked.ok) {
3071
+ return refuse(opId, "invalid-worktree", { reason: linked.kind, message: linked.message });
3072
+ }
3073
+ if (!isWorktreeClean(linked.path)) return refuse(opId, "dirty-worktree");
3074
+ if (isRebaseInProgress(linked.path)) return refuse(opId, "rebase-in-progress");
3075
+ const head = currentHead(linked.path);
3076
+ if (!head || head !== manifest.lastPrHeadSha || head !== pr.headSha) {
3077
+ return refuse(opId, "head-mismatch", {
3078
+ local: head ?? "",
3079
+ manifest: manifest.lastPrHeadSha ?? "",
3080
+ pr: pr.headSha ?? ""
3081
+ });
3082
+ }
3083
+ const remote = deps.remote ?? "origin";
3084
+ const remoteHead = remoteBranchHead(linked.path, remote, manifest.branch);
3085
+ if (remoteHead.ok && remoteHead.present && remoteHead.sha !== head) {
3086
+ return refuse(opId, "remote-diverged", { local: head, remote: remoteHead.sha });
3087
+ }
3088
+ const ahead = unpublishedAhead(linked.path, remote, manifest.branch);
3089
+ if (ahead !== null && ahead > 0) {
3090
+ return refuse(opId, "has-unpublished-commits", { ahead, branch: manifest.branch, remote });
3091
+ }
3092
+ if (manifest.workflowId) {
3093
+ const snapshot = await readRunSnapshot(deps.repoRoot, manifest.workflowId);
3094
+ if (snapshot?.state === "ready") return refuse(opId, "workflow-ready");
3095
+ if (snapshot?.state === "merged") return refuse(opId, "workflow-merged");
3096
+ }
3097
+ return {
3098
+ ok: true,
3099
+ intent: {
3100
+ schemaVersion: 1,
3101
+ taskId: manifest.taskId,
3102
+ issueNumber: manifest.issueNumber,
3103
+ prNumber: manifest.prNumber,
3104
+ branch: manifest.branch,
3105
+ worktreePath: linked.path,
3106
+ headSha: head,
3107
+ workflowId: manifest.workflowId ?? null,
3108
+ subject,
3109
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString()
3110
+ }
3111
+ };
3112
+ }
3113
+ async function resumeCleanup({ deps, intent, opId }) {
3114
+ const removeWorktree = deps.removeWorktree ?? defaultRemoveWorktree;
3115
+ const deleteBranch = deps.deleteBranch ?? defaultDeleteBranch;
3116
+ const removeManifest = deps.deleteManifest ?? deleteManifest;
3117
+ let removedWorktree = !existsSync8(intent.worktreePath);
3118
+ if (!removedWorktree) {
3119
+ let removed;
3120
+ try {
3121
+ removed = await Promise.resolve(removeWorktree(deps.repoRoot, intent.worktreePath));
3122
+ } catch (err) {
3123
+ return refuse(opId, "remove-failed", { stderr: String(err?.message ?? err) });
3124
+ }
3125
+ if (removed?.status !== 0 && existsSync8(intent.worktreePath)) {
3126
+ return refuse(opId, "remove-failed", { stderr: removed?.stderr ?? "" });
3127
+ }
3128
+ removedWorktree = !existsSync8(intent.worktreePath);
3129
+ }
3130
+ let deletedBranch = !branchExists(deps.repoRoot, intent.branch);
3131
+ if (!deletedBranch) {
3132
+ let deleted;
3133
+ try {
3134
+ deleted = await Promise.resolve(deleteBranch(deps.repoRoot, intent.branch, intent.headSha));
3135
+ } catch (err) {
3136
+ return refuse(opId, "branch-delete-failed", { stderr: String(err?.message ?? err) });
3137
+ }
3138
+ if (deleted?.status !== 0 && branchExists(deps.repoRoot, intent.branch)) {
3139
+ return refuse(opId, "branch-delete-failed", { stderr: deleted?.stderr ?? "" });
3140
+ }
3141
+ deletedBranch = !branchExists(deps.repoRoot, intent.branch);
3142
+ }
3143
+ try {
3144
+ await Promise.resolve(removeManifest(deps.repoRoot, intent.taskId));
3145
+ } catch (err) {
3146
+ return refuse(opId, "manifest-delete-failed", { stderr: String(err?.message ?? err) });
3147
+ }
3148
+ const remaining = await readManifest(deps.repoRoot, intent.taskId);
3149
+ const deletedManifest = remaining === null;
3150
+ const completion = {
3151
+ schemaVersion: 1,
3152
+ taskId: intent.taskId,
3153
+ intentHash: intent.intentHash,
3154
+ removedWorktree,
3155
+ deletedBranch,
3156
+ deletedManifest,
3157
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
3158
+ };
3159
+ const published = await publishAbandonCompletion(deps.repoRoot, completion);
3160
+ if (!published.ok) return refuse(opId, published.kind);
3161
+ return success("abandon", {
3162
+ taskId: intent.taskId,
3163
+ intentHash: intent.intentHash,
3164
+ removedWorktree,
3165
+ deletedBranch,
3166
+ deletedManifest
3167
+ }, { operationId: opId, idempotent: published.idempotent === true });
3168
+ }
3169
+ function createAbandonTool(deps) {
3170
+ return async function abandon(input) {
3171
+ const opId = input.operationId ?? `abandon-${Date.now().toString(36)}`;
3172
+ const taskId = String(input.taskId ?? "");
3173
+ const subject = String(input.subject ?? "").trim();
3174
+ if (!taskId) return refuse(opId, "missing-input", { field: "taskId" });
3175
+ if (!subject) return refuse(opId, "missing-input", { field: "subject" });
3176
+ const existing = await readAbandon(deps.repoRoot, taskId);
3177
+ if (existing.completion) {
3178
+ return success("abandon", {
3179
+ taskId,
3180
+ intentHash: existing.completion.intentHash,
3181
+ removedWorktree: existing.completion.removedWorktree,
3182
+ deletedBranch: existing.completion.deletedBranch,
3183
+ deletedManifest: existing.completion.deletedManifest
3184
+ }, { operationId: opId, idempotent: true });
3185
+ }
3186
+ if (existing.intent) {
3187
+ if (existing.intent.subject !== subject || existing.intent.taskId !== taskId) {
3188
+ return refuse(opId, "abandon-conflict");
3189
+ }
3190
+ return resumeCleanup({ deps, intent: existing.intent, opId });
3191
+ }
3192
+ const manifest = await readManifest(deps.repoRoot, taskId);
3193
+ if (!manifest) return refuse(opId, "missing-manifest", { taskId });
3194
+ const validated = await validateLiveAttempt({ deps, manifest, subject, opId });
3195
+ if (!validated.ok) return validated;
3196
+ const publishedIntent = await publishAbandonIntent(deps.repoRoot, validated.intent);
3197
+ if (!publishedIntent.ok) return refuse(opId, publishedIntent.kind);
3198
+ return resumeCleanup({ deps, intent: publishedIntent.record, opId });
3199
+ };
3200
+ }
3201
+
3202
+ // src/version.js
3203
+ import { readFileSync, existsSync as existsSync9 } from "node:fs";
3204
+ import { dirname as dirname4, resolve as resolve10 } from "node:path";
3205
+ import { fileURLToPath } from "node:url";
3206
+ var PACKAGE_VERSION = "1.1.8";
3207
+ var TEMPLATE_SET = `v${PACKAGE_VERSION}`;
2774
3208
  export {
2775
3209
  ADAPTER_CONTRACT_VERSION,
2776
3210
  ADAPTER_FILENAME,
@@ -2783,6 +3217,7 @@ export {
2783
3217
  bucketFor,
2784
3218
  canTransition,
2785
3219
  checkGates,
3220
+ createAbandonTool,
2786
3221
  createCleanupTool,
2787
3222
  createGhDriver,
2788
3223
  createGhStub,