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/CHANGELOG.md +53 -0
- package/README.md +12 -12
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/assets/agents/ship-controller.md +9 -6
- package/assets/commands/ship-deliver.md +13 -28
- package/assets/skills/delivery-workflow/SKILL.md +19 -26
- package/dist/cli.js +20 -15
- package/dist/core.d.ts +601 -1
- package/dist/core.js +471 -36
- package/dist/plugin.js +1145 -549
- package/package.json +2 -1
- package/tests/plugin/expected-tools.mjs +6 -4
- package/tests/plugin/plugin-load.test.mjs +4 -4
package/dist/core.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// opencode-ship/core v1.1.
|
|
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((
|
|
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) =>
|
|
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
|
-
|
|
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/
|
|
2773
|
-
|
|
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,
|