opencode-ship 1.1.7 → 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/plugin.js CHANGED
@@ -1,4 +1,4 @@
1
- // opencode-ship v1.1.7
1
+ // opencode-ship v1.1.8
2
2
  var __defProp = Object.defineProperty;
3
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
4
  var __esm = (fn, res) => function __init() {
@@ -12706,7 +12706,7 @@ tool.schema = external_exports;
12706
12706
 
12707
12707
  // src/plugin.js
12708
12708
  import { resolve as resolve22 } from "node:path";
12709
- import { readFile as readFile24 } from "node:fs/promises";
12709
+ import { readFile as readFile26 } from "node:fs/promises";
12710
12710
 
12711
12711
  // src/adapter.js
12712
12712
  import { readFile, writeFile, mkdir, rename } from "node:fs/promises";
@@ -13740,6 +13740,9 @@ function createWorktreeTool(deps) {
13740
13740
  expectedRoot: resolve7(deps.repoRoot, worktreeRoot)
13741
13741
  };
13742
13742
  }
13743
+ if (m.schemaVersion >= 2 && !m.workflowId) {
13744
+ return { kind: "missing-workflow-link", taskId: m.taskId };
13745
+ }
13743
13746
  const remote = deps.remote ?? "origin";
13744
13747
  const hasRemote = remoteExists(remote, deps.repoRoot);
13745
13748
  if (hasRemote) {
@@ -15173,6 +15176,231 @@ function createCleanupTool(deps) {
15173
15176
  };
15174
15177
  }
15175
15178
 
15179
+ // src/tools/delivery-abandon.js
15180
+ import { join as join10 } from "node:path";
15181
+ import { spawnSync as spawnSync4 } from "node:child_process";
15182
+ import { existsSync as existsSync8 } from "node:fs";
15183
+
15184
+ // src/state/abandon-store.js
15185
+ import { readFile as readFile7 } from "node:fs/promises";
15186
+ import { join as join8 } from "node:path";
15187
+ import { createHash as createHash7 } from "node:crypto";
15188
+ init_durable_store();
15189
+ var SAFE_ID_RE2 = /^[A-Za-z0-9._-]{1,128}$/;
15190
+ var HASH_RE2 = /^[0-9a-f]{64}$/;
15191
+ function abandonDir(commonDir, taskId) {
15192
+ return join8(commonDir, "opencode-ship", "delivery", "abandoned", taskId);
15193
+ }
15194
+ function intentPathFor(commonDir, taskId) {
15195
+ return join8(abandonDir(commonDir, taskId), "intent.json");
15196
+ }
15197
+ function completionPathFor(commonDir, taskId) {
15198
+ return join8(abandonDir(commonDir, taskId), "completion.json");
15199
+ }
15200
+ async function readJsonOrNull2(path) {
15201
+ try {
15202
+ return JSON.parse(await readFile7(path, "utf8"));
15203
+ } catch (err) {
15204
+ if (err?.code === "ENOENT") return null;
15205
+ throw err;
15206
+ }
15207
+ }
15208
+ function withoutIntentHash(record2) {
15209
+ const copy = { ...record2 ?? {} };
15210
+ delete copy.intentHash;
15211
+ return copy;
15212
+ }
15213
+ function hashAbandonIntent(record2) {
15214
+ return createHash7("sha256").update(canonicalJson(withoutIntentHash(record2)), "utf8").digest("hex");
15215
+ }
15216
+ async function readAbandon(repoRoot, taskId) {
15217
+ const commonDir = await resolveGitCommonDir(repoRoot);
15218
+ if (!SAFE_ID_RE2.test(String(taskId ?? ""))) {
15219
+ return { intent: null, completion: null };
15220
+ }
15221
+ return {
15222
+ intent: await readJsonOrNull2(intentPathFor(commonDir, taskId)),
15223
+ completion: await readJsonOrNull2(completionPathFor(commonDir, taskId))
15224
+ };
15225
+ }
15226
+ async function publishOrReuse(path, record2) {
15227
+ try {
15228
+ await publishImmutableJson(path, record2);
15229
+ return { ok: true, record: record2, idempotent: false };
15230
+ } catch (err) {
15231
+ const message = String(err?.message ?? err);
15232
+ if (!message.includes("already exists")) throw err;
15233
+ const existing = await readJsonOrNull2(path);
15234
+ if (existing && canonicalJson(existing) === canonicalJson(record2)) {
15235
+ return { ok: true, record: existing, idempotent: true };
15236
+ }
15237
+ return { ok: false, kind: "abandon-conflict" };
15238
+ }
15239
+ }
15240
+ async function publishAbandonIntent(repoRoot, record2) {
15241
+ const taskId = String(record2?.taskId ?? "");
15242
+ if (!SAFE_ID_RE2.test(taskId)) {
15243
+ return { ok: false, kind: "invalid-task-id" };
15244
+ }
15245
+ const sealed = { ...withoutIntentHash(record2), intentHash: hashAbandonIntent(record2) };
15246
+ const commonDir = await resolveGitCommonDir(repoRoot);
15247
+ return publishOrReuse(intentPathFor(commonDir, taskId), sealed);
15248
+ }
15249
+ async function publishAbandonCompletion(repoRoot, record2) {
15250
+ const taskId = String(record2?.taskId ?? "");
15251
+ const intentHash = String(record2?.intentHash ?? "");
15252
+ if (!SAFE_ID_RE2.test(taskId)) {
15253
+ return { ok: false, kind: "invalid-task-id" };
15254
+ }
15255
+ if (!HASH_RE2.test(intentHash)) {
15256
+ return { ok: false, kind: "invalid-intent-hash" };
15257
+ }
15258
+ const commonDir = await resolveGitCommonDir(repoRoot);
15259
+ return publishOrReuse(completionPathFor(commonDir, taskId), record2);
15260
+ }
15261
+
15262
+ // src/skills/worktree.js
15263
+ import { execFile } from "node:child_process";
15264
+ import { promises as fs, existsSync as existsSync7 } from "node:fs";
15265
+ import { resolve as resolve9, dirname as dirname3, sep, isAbsolute, join as join9 } from "node:path";
15266
+ function listRegisteredWorktrees(mainRepo) {
15267
+ return new Promise((resolveP, rejectP) => {
15268
+ execFile(
15269
+ "git",
15270
+ ["-C", mainRepo, "worktree", "list", "--porcelain", "-z"],
15271
+ { shell: false, maxBuffer: 1024 * 1024 },
15272
+ (err, stdout) => {
15273
+ if (err) return rejectP(err);
15274
+ const records = parsePorcelain(stdout);
15275
+ const mainRecord = records.shift();
15276
+ const mainPath = mainRecord?.worktree ? resolve9(mainRecord.worktree) : null;
15277
+ const linked = [];
15278
+ for (const r of records) {
15279
+ if (!r.worktree) continue;
15280
+ const p = resolve9(r.worktree);
15281
+ if (mainPath && p === mainPath) continue;
15282
+ linked.push({ path: p, branch: r.HEAD ?? null });
15283
+ }
15284
+ resolveP(linked);
15285
+ }
15286
+ );
15287
+ });
15288
+ }
15289
+ function parsePorcelain(text) {
15290
+ const tokens = text.split("\0");
15291
+ const out = [];
15292
+ let current = {};
15293
+ for (const tok of tokens) {
15294
+ if (tok.length === 0) {
15295
+ if (Object.keys(current).length > 0) {
15296
+ out.push(current);
15297
+ current = {};
15298
+ }
15299
+ continue;
15300
+ }
15301
+ const idx = tok.indexOf(" ");
15302
+ const key = idx === -1 ? tok : tok.slice(0, idx);
15303
+ const value = idx === -1 ? "" : tok.slice(idx + 1);
15304
+ if (key === "branch") {
15305
+ current.HEAD = value.startsWith("refs/heads/") ? value : `refs/heads/${value}`;
15306
+ } else {
15307
+ current[key] = value;
15308
+ }
15309
+ }
15310
+ if (Object.keys(current).length > 0) out.push(current);
15311
+ return out;
15312
+ }
15313
+ async function validateLinkedWorktree(mainRepo, worktreePath, options = {}) {
15314
+ const main = resolve9(mainRepo);
15315
+ if (!existsSync7(main)) {
15316
+ return { ok: false, kind: "missing", message: `main repository ${main} does not exist` };
15317
+ }
15318
+ if (!worktreePath) {
15319
+ return { ok: false, kind: "unlinked", message: "worktreePath is required" };
15320
+ }
15321
+ const wt = resolve9(worktreePath);
15322
+ if (!existsSync7(wt)) {
15323
+ return { ok: false, kind: "missing", message: `worktree ${wt} does not exist` };
15324
+ }
15325
+ const isCurrent = wt === main;
15326
+ if (isCurrent) {
15327
+ const gitEntry = options.allowCurrentLinked ? await fs.lstat(join9(main, ".git")).catch(() => null) : null;
15328
+ if (!gitEntry?.isFile()) {
15329
+ return { ok: false, kind: "main", message: "installs into the main worktree are forbidden" };
15330
+ }
15331
+ }
15332
+ let cursor = wt;
15333
+ while (cursor !== dirname3(cursor)) {
15334
+ const stat3 = await fs.lstat(cursor).catch(() => null);
15335
+ if (stat3?.isSymbolicLink()) {
15336
+ return {
15337
+ ok: false,
15338
+ kind: "ancestor-symlink",
15339
+ message: `worktree path contains a symlink at ${cursor}`
15340
+ };
15341
+ }
15342
+ cursor = dirname3(cursor);
15343
+ }
15344
+ const real = await fs.realpath(wt).catch(() => null);
15345
+ if (real && real !== wt) {
15346
+ return {
15347
+ ok: false,
15348
+ kind: "symlink",
15349
+ message: `worktree ${wt} resolves through a symlink to ${real}`
15350
+ };
15351
+ }
15352
+ const linked = await listRegisteredWorktrees(main);
15353
+ const matched = linked.find((entry) => entry.path === wt);
15354
+ if (!matched) {
15355
+ return {
15356
+ ok: false,
15357
+ kind: "unlinked",
15358
+ message: `worktree ${wt} is not registered (git worktree list)`
15359
+ };
15360
+ }
15361
+ return { ok: true, path: wt, registered: !!matched };
15362
+ }
15363
+ function validateRelativeInstallPath(destRel) {
15364
+ if (typeof destRel !== "string" || destRel.length === 0) {
15365
+ return { ok: false, kind: "absolute", message: "destination path required" };
15366
+ }
15367
+ if (isAbsolute(destRel)) {
15368
+ return { ok: false, kind: "absolute", message: `destination must be relative: ${destRel}` };
15369
+ }
15370
+ if (destRel.includes("\\")) {
15371
+ return { ok: false, kind: "parent-relative", message: `destination must use POSIX separators: ${destRel}` };
15372
+ }
15373
+ const parts = destRel.split("/");
15374
+ for (const p of parts) {
15375
+ if (p === "" || p === "." || p === "..") {
15376
+ return { ok: false, kind: "parent-relative", message: `destination escapes worktree: ${destRel}` };
15377
+ }
15378
+ }
15379
+ return { ok: true };
15380
+ }
15381
+ async function validateInstallDestination(worktreeRoot, destRel) {
15382
+ const relativeCheck = validateRelativeInstallPath(destRel);
15383
+ if (!relativeCheck.ok) return relativeCheck;
15384
+ const root = resolve9(worktreeRoot);
15385
+ const destination = resolve9(root, ...destRel.split("/"));
15386
+ if (destination !== root && !destination.startsWith(root + sep)) {
15387
+ return { ok: false, kind: "escape", message: `destination escapes worktree: ${destRel}` };
15388
+ }
15389
+ let cursor = root;
15390
+ for (const part of destRel.split("/")) {
15391
+ cursor = join9(cursor, part);
15392
+ const entry = await fs.lstat(cursor).catch(() => null);
15393
+ if (!entry) continue;
15394
+ if (entry.isSymbolicLink()) {
15395
+ return { ok: false, kind: "symlink", message: `destination path contains a symlink at ${cursor}` };
15396
+ }
15397
+ if (cursor !== destination && !entry.isDirectory()) {
15398
+ return { ok: false, kind: "not-directory", message: `destination ancestor is not a directory: ${cursor}` };
15399
+ }
15400
+ }
15401
+ return { ok: true, path: destination };
15402
+ }
15403
+
15176
15404
  // src/tools/envelope.js
15177
15405
  import { randomBytes as randomBytes2 } from "node:crypto";
15178
15406
  var CONTRACT_VERSION = 2;
@@ -15211,25 +15439,224 @@ function failure(kind, message, options = {}) {
15211
15439
  };
15212
15440
  }
15213
15441
 
15442
+ // src/tools/delivery-abandon.js
15443
+ import { readFile as readFile8 } from "node:fs/promises";
15444
+ function runGit2(args, cwd) {
15445
+ return spawnSync4("git", args, {
15446
+ cwd,
15447
+ encoding: "utf8",
15448
+ stdio: ["ignore", "pipe", "pipe"],
15449
+ env: process.env
15450
+ });
15451
+ }
15452
+ function defaultRemoveWorktree(repoRoot, path) {
15453
+ return runGit2(["worktree", "remove", path], repoRoot);
15454
+ }
15455
+ function defaultDeleteBranch(repoRoot, branch, expectedSha) {
15456
+ const args = ["update-ref", "-d", `refs/heads/${branch}`];
15457
+ if (expectedSha && /^[0-9a-f]{7,}$/i.test(expectedSha)) args.push(expectedSha);
15458
+ return runGit2(args, repoRoot);
15459
+ }
15460
+ function branchExists(repoRoot, branch) {
15461
+ return runGit2(["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], repoRoot).status === 0;
15462
+ }
15463
+ function remoteBranchHead(repoRoot, remote, branch) {
15464
+ const r = runGit2(["ls-remote", "--heads", remote, branch], repoRoot);
15465
+ if (r.status !== 0) return { ok: false, sha: null, present: false };
15466
+ const line = r.stdout.split("\n").find((row) => row.includes(`refs/heads/${branch}`));
15467
+ if (!line) return { ok: true, sha: null, present: false };
15468
+ return { ok: true, sha: line.split(/\s+/)[0], present: true };
15469
+ }
15470
+ function unpublishedAhead(repoRoot, remote, branch) {
15471
+ const r = runGit2(["rev-list", "--count", `${remote}/${branch}..${branch}`], repoRoot);
15472
+ if (r.status !== 0) return null;
15473
+ const n = parseInt(r.stdout.trim(), 10);
15474
+ return Number.isFinite(n) ? n : null;
15475
+ }
15476
+ function refuse(opId, kind, extras = {}) {
15477
+ return failure("abandon", kind, {
15478
+ operationId: opId,
15479
+ retryable: false,
15480
+ details: { kind, ...extras }
15481
+ });
15482
+ }
15483
+ async function readRunSnapshot(repoRoot, workflowId) {
15484
+ try {
15485
+ const common = await resolveGitCommonDir(repoRoot);
15486
+ const path = join10(opencodeShipStateDir(common), "runs", workflowId, "run.json");
15487
+ if (!existsSync8(path)) return null;
15488
+ return JSON.parse(await readFile8(path, "utf8"));
15489
+ } catch {
15490
+ return null;
15491
+ }
15492
+ }
15493
+ async function validateLiveAttempt({ deps, manifest, subject, opId }) {
15494
+ if (!manifest.prNumber) return refuse(opId, "missing-pr");
15495
+ if (!manifest.worktreePath) return refuse(opId, "missing-worktree-path");
15496
+ const pr = await deps.driver.readPullRequest({
15497
+ repo: deps.repoSlug,
15498
+ number: manifest.prNumber
15499
+ });
15500
+ if (pr.merged || pr.state === "MERGED") return refuse(opId, "pr-merged");
15501
+ if (pr.state !== "CLOSED") return refuse(opId, "pr-open");
15502
+ if (pr.headRefName && pr.headRefName !== manifest.branch) {
15503
+ return refuse(opId, "branch-mismatch", { expected: manifest.branch, received: pr.headRefName });
15504
+ }
15505
+ if (pr.baseRefName && pr.baseRefName !== manifest.baseBranch) {
15506
+ return refuse(opId, "base-mismatch", { expected: manifest.baseBranch, received: pr.baseRefName });
15507
+ }
15508
+ const linked = await validateLinkedWorktree(deps.repoRoot, manifest.worktreePath);
15509
+ if (!linked.ok) {
15510
+ return refuse(opId, "invalid-worktree", { reason: linked.kind, message: linked.message });
15511
+ }
15512
+ if (!isWorktreeClean(linked.path)) return refuse(opId, "dirty-worktree");
15513
+ if (isRebaseInProgress(linked.path)) return refuse(opId, "rebase-in-progress");
15514
+ const head = currentHead(linked.path);
15515
+ if (!head || head !== manifest.lastPrHeadSha || head !== pr.headSha) {
15516
+ return refuse(opId, "head-mismatch", {
15517
+ local: head ?? "",
15518
+ manifest: manifest.lastPrHeadSha ?? "",
15519
+ pr: pr.headSha ?? ""
15520
+ });
15521
+ }
15522
+ const remote = deps.remote ?? "origin";
15523
+ const remoteHead = remoteBranchHead(linked.path, remote, manifest.branch);
15524
+ if (remoteHead.ok && remoteHead.present && remoteHead.sha !== head) {
15525
+ return refuse(opId, "remote-diverged", { local: head, remote: remoteHead.sha });
15526
+ }
15527
+ const ahead = unpublishedAhead(linked.path, remote, manifest.branch);
15528
+ if (ahead !== null && ahead > 0) {
15529
+ return refuse(opId, "has-unpublished-commits", { ahead, branch: manifest.branch, remote });
15530
+ }
15531
+ if (manifest.workflowId) {
15532
+ const snapshot = await readRunSnapshot(deps.repoRoot, manifest.workflowId);
15533
+ if (snapshot?.state === "ready") return refuse(opId, "workflow-ready");
15534
+ if (snapshot?.state === "merged") return refuse(opId, "workflow-merged");
15535
+ }
15536
+ return {
15537
+ ok: true,
15538
+ intent: {
15539
+ schemaVersion: 1,
15540
+ taskId: manifest.taskId,
15541
+ issueNumber: manifest.issueNumber,
15542
+ prNumber: manifest.prNumber,
15543
+ branch: manifest.branch,
15544
+ worktreePath: linked.path,
15545
+ headSha: head,
15546
+ workflowId: manifest.workflowId ?? null,
15547
+ subject,
15548
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString()
15549
+ }
15550
+ };
15551
+ }
15552
+ async function resumeCleanup({ deps, intent, opId }) {
15553
+ const removeWorktree = deps.removeWorktree ?? defaultRemoveWorktree;
15554
+ const deleteBranch = deps.deleteBranch ?? defaultDeleteBranch;
15555
+ const removeManifest = deps.deleteManifest ?? deleteManifest;
15556
+ let removedWorktree = !existsSync8(intent.worktreePath);
15557
+ if (!removedWorktree) {
15558
+ let removed;
15559
+ try {
15560
+ removed = await Promise.resolve(removeWorktree(deps.repoRoot, intent.worktreePath));
15561
+ } catch (err) {
15562
+ return refuse(opId, "remove-failed", { stderr: String(err?.message ?? err) });
15563
+ }
15564
+ if (removed?.status !== 0 && existsSync8(intent.worktreePath)) {
15565
+ return refuse(opId, "remove-failed", { stderr: removed?.stderr ?? "" });
15566
+ }
15567
+ removedWorktree = !existsSync8(intent.worktreePath);
15568
+ }
15569
+ let deletedBranch = !branchExists(deps.repoRoot, intent.branch);
15570
+ if (!deletedBranch) {
15571
+ let deleted;
15572
+ try {
15573
+ deleted = await Promise.resolve(deleteBranch(deps.repoRoot, intent.branch, intent.headSha));
15574
+ } catch (err) {
15575
+ return refuse(opId, "branch-delete-failed", { stderr: String(err?.message ?? err) });
15576
+ }
15577
+ if (deleted?.status !== 0 && branchExists(deps.repoRoot, intent.branch)) {
15578
+ return refuse(opId, "branch-delete-failed", { stderr: deleted?.stderr ?? "" });
15579
+ }
15580
+ deletedBranch = !branchExists(deps.repoRoot, intent.branch);
15581
+ }
15582
+ try {
15583
+ await Promise.resolve(removeManifest(deps.repoRoot, intent.taskId));
15584
+ } catch (err) {
15585
+ return refuse(opId, "manifest-delete-failed", { stderr: String(err?.message ?? err) });
15586
+ }
15587
+ const remaining = await readManifest(deps.repoRoot, intent.taskId);
15588
+ const deletedManifest = remaining === null;
15589
+ const completion = {
15590
+ schemaVersion: 1,
15591
+ taskId: intent.taskId,
15592
+ intentHash: intent.intentHash,
15593
+ removedWorktree,
15594
+ deletedBranch,
15595
+ deletedManifest,
15596
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
15597
+ };
15598
+ const published = await publishAbandonCompletion(deps.repoRoot, completion);
15599
+ if (!published.ok) return refuse(opId, published.kind);
15600
+ return success2("abandon", {
15601
+ taskId: intent.taskId,
15602
+ intentHash: intent.intentHash,
15603
+ removedWorktree,
15604
+ deletedBranch,
15605
+ deletedManifest
15606
+ }, { operationId: opId, idempotent: published.idempotent === true });
15607
+ }
15608
+ function createAbandonTool(deps) {
15609
+ return async function abandon(input) {
15610
+ const opId = input.operationId ?? `abandon-${Date.now().toString(36)}`;
15611
+ const taskId = String(input.taskId ?? "");
15612
+ const subject = String(input.subject ?? "").trim();
15613
+ if (!taskId) return refuse(opId, "missing-input", { field: "taskId" });
15614
+ if (!subject) return refuse(opId, "missing-input", { field: "subject" });
15615
+ const existing = await readAbandon(deps.repoRoot, taskId);
15616
+ if (existing.completion) {
15617
+ return success2("abandon", {
15618
+ taskId,
15619
+ intentHash: existing.completion.intentHash,
15620
+ removedWorktree: existing.completion.removedWorktree,
15621
+ deletedBranch: existing.completion.deletedBranch,
15622
+ deletedManifest: existing.completion.deletedManifest
15623
+ }, { operationId: opId, idempotent: true });
15624
+ }
15625
+ if (existing.intent) {
15626
+ if (existing.intent.subject !== subject || existing.intent.taskId !== taskId) {
15627
+ return refuse(opId, "abandon-conflict");
15628
+ }
15629
+ return resumeCleanup({ deps, intent: existing.intent, opId });
15630
+ }
15631
+ const manifest = await readManifest(deps.repoRoot, taskId);
15632
+ if (!manifest) return refuse(opId, "missing-manifest", { taskId });
15633
+ const validated = await validateLiveAttempt({ deps, manifest, subject, opId });
15634
+ if (!validated.ok) return validated;
15635
+ const publishedIntent = await publishAbandonIntent(deps.repoRoot, validated.intent);
15636
+ if (!publishedIntent.ok) return refuse(opId, publishedIntent.kind);
15637
+ return resumeCleanup({ deps, intent: publishedIntent.record, opId });
15638
+ };
15639
+ }
15640
+
15214
15641
  // src/state/github-operation-store.js
15215
- import { readFile as readFile7, writeFile as writeFile4, mkdir as mkdir5, readdir as readdir4, unlink as unlink3 } from "node:fs/promises";
15216
- import { existsSync as existsSync7 } from "node:fs";
15217
- import { join as join8, dirname as dirname3 } from "node:path";
15642
+ import { readFile as readFile9, writeFile as writeFile4, mkdir as mkdir5, readdir as readdir4, unlink as unlink3 } from "node:fs/promises";
15643
+ import { existsSync as existsSync9 } from "node:fs";
15644
+ import { join as join11, dirname as dirname4 } from "node:path";
15218
15645
  init_durable_store();
15219
15646
  async function operationsDir(repoRoot) {
15220
15647
  const common = await resolveGitCommonDir(repoRoot);
15221
- return join8(opencodeShipStateDir(common), "github", "operations");
15648
+ return join11(opencodeShipStateDir(common), "github", "operations");
15222
15649
  }
15223
- var SAFE_ID_RE2 = /^[A-Za-z0-9._-]{1,128}$/;
15650
+ var SAFE_ID_RE3 = /^[A-Za-z0-9._-]{1,128}$/;
15224
15651
  function operationPath(dir, operationId2) {
15225
- if (!SAFE_ID_RE2.test(operationId2)) {
15652
+ if (!SAFE_ID_RE3.test(operationId2)) {
15226
15653
  throw new Error(`invalid operationId: ${JSON.stringify(operationId2)}`);
15227
15654
  }
15228
- return join8(dir, `${operationId2}.json`);
15655
+ return join11(dir, `${operationId2}.json`);
15229
15656
  }
15230
15657
  async function hasOperation(repoRoot, operationId2) {
15231
15658
  const dir = await operationsDir(repoRoot);
15232
- return existsSync7(operationPath(dir, operationId2));
15659
+ return existsSync9(operationPath(dir, operationId2));
15233
15660
  }
15234
15661
  async function recordOperation(repoRoot, operationId2, record2) {
15235
15662
  if (typeof operationId2 !== "string" || operationId2.length === 0) {
@@ -15241,7 +15668,7 @@ async function recordOperation(repoRoot, operationId2, record2) {
15241
15668
  const dir = await operationsDir(repoRoot);
15242
15669
  await mkdir5(dir, { recursive: true });
15243
15670
  const path = operationPath(dir, operationId2);
15244
- if (existsSync7(path)) {
15671
+ if (existsSync9(path)) {
15245
15672
  return { recorded: false, path };
15246
15673
  }
15247
15674
  const fullRecord = {
@@ -15511,76 +15938,33 @@ function createPublishTool(deps) {
15511
15938
  };
15512
15939
  }
15513
15940
 
15514
- // src/profile.js
15515
- var PROFILES = Object.freeze(["engineering"]);
15516
- var LEGACY_PROFILES = Object.freeze(["core"]);
15517
-
15518
- // src/installer/engineering-config.js
15519
- var MODEL_ID_RE = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
15520
- var DEFAULTS = Object.freeze({
15521
- planner: "openai/gpt-5.6-sol",
15522
- builder: "minimax/MiniMax-M3",
15523
- finalReviewer: "openai/gpt-5.6-sol"
15524
- });
15525
- function resolveModelRoles(cfg, { strict = false, allowDeferred = false } = {}) {
15526
- const REQUIRED = ["planner", "builder", "finalReviewer"];
15527
- if (strict) {
15528
- const issues = [];
15529
- for (const role of REQUIRED) {
15530
- const id = cfg?.models?.[role];
15531
- if (typeof id !== "string" || id.length === 0 || !MODEL_ID_RE.test(id)) {
15532
- issues.push(role);
15533
- }
15534
- }
15535
- if (issues.length > 0) {
15536
- throw new Error(`resolveModelRoles: required role(s) missing or invalid: ${issues.join(", ")}`);
15537
- }
15538
- return { planner: cfg.models.planner, builder: cfg.models.builder, finalReviewer: cfg.models.finalReviewer };
15539
- }
15540
- const out = { ...DEFAULTS };
15541
- if (cfg && cfg.models) {
15542
- for (const [role, id] of Object.entries(cfg.models)) {
15543
- if (id && typeof id === "string" && id.length > 0) {
15544
- out[role] = id;
15545
- } else if (strict && Object.prototype.hasOwnProperty.call(cfg.models, role)) {
15546
- throw new Error(`resolveModelRoles: user provided empty model id for '${role}'`);
15547
- }
15548
- }
15549
- }
15550
- if (allowDeferred) {
15551
- return { planner: out.planner ?? null, builder: out.builder ?? null, finalReviewer: out.finalReviewer ?? null };
15552
- }
15553
- for (const role of REQUIRED) {
15554
- if (!out[role]) {
15555
- throw new Error(`resolveModelRoles: required role '${role}' missing and no default available`);
15556
- }
15557
- }
15558
- return out;
15559
- }
15560
-
15561
15941
  // src/installer/lock.js
15562
- import { readFile as readFile8, writeFile as writeFile5, rename as rename3, mkdir as mkdir6 } from "node:fs/promises";
15563
- import { existsSync as existsSync8 } from "node:fs";
15564
- import { dirname as dirname4, resolve as resolve9, posix } from "node:path";
15942
+ import { readFile as readFile10, writeFile as writeFile5, rename as rename3, mkdir as mkdir6 } from "node:fs/promises";
15943
+ import { existsSync as existsSync10 } from "node:fs";
15944
+ import { dirname as dirname5, resolve as resolve10, posix } from "node:path";
15565
15945
 
15566
15946
  // src/installer/hash.js
15567
- import { createHash as createHash7 } from "node:crypto";
15947
+ import { createHash as createHash8 } from "node:crypto";
15568
15948
  function bytesHash(buffer) {
15569
- return createHash7("sha256").update(buffer).digest("hex");
15949
+ return createHash8("sha256").update(buffer).digest("hex");
15570
15950
  }
15571
15951
  function bytesHashString(text) {
15572
15952
  return bytesHash(Buffer.from(text, "utf8"));
15573
15953
  }
15574
15954
 
15955
+ // src/profile.js
15956
+ var PROFILES = Object.freeze(["engineering"]);
15957
+ var LEGACY_PROFILES = Object.freeze(["core"]);
15958
+
15575
15959
  // src/installer/lock.js
15576
15960
  function lockPath(repoRoot) {
15577
- return resolve9(repoRoot, ".opencode", "ship.lock.json");
15961
+ return resolve10(repoRoot, ".opencode", "ship.lock.json");
15578
15962
  }
15579
15963
  async function readLock2(repoRoot) {
15580
15964
  const path = lockPath(repoRoot);
15581
- if (!existsSync8(path)) return null;
15965
+ if (!existsSync10(path)) return null;
15582
15966
  try {
15583
- const raw = await readFile8(path, "utf8");
15967
+ const raw = await readFile10(path, "utf8");
15584
15968
  return JSON.parse(raw);
15585
15969
  } catch {
15586
15970
  return null;
@@ -15593,15 +15977,11 @@ function isSetupComplete(lock) {
15593
15977
  return manager.setupComplete === true;
15594
15978
  }
15595
15979
 
15596
- // src/tools/ship-plan-start.js
15597
- import { join as join10 } from "node:path";
15598
- import { mkdir as mkdir8, writeFile as writeFile6 } from "node:fs/promises";
15599
-
15600
15980
  // src/runtime/opencode-dispatcher.js
15601
15981
  import { mkdir as mkdir7 } from "node:fs/promises";
15602
- import { join as join9 } from "node:path";
15982
+ import { join as join12 } from "node:path";
15603
15983
  init_durable_store();
15604
- import { createHash as createHash8 } from "node:crypto";
15984
+ import { createHash as createHash9 } from "node:crypto";
15605
15985
  import { dirname as dirnameOf } from "node:path";
15606
15986
  var ROLE_PLANNER = "planner";
15607
15987
  var ROLE_BUILDER = "builder";
@@ -15626,20 +16006,16 @@ function dispatchKeyFor(role, input) {
15626
16006
  }
15627
16007
  }
15628
16008
  function dispatchDir(commonDir, workflowId, dispatchKey) {
15629
- return join9(opencodeShipStateDir(commonDir), "runs", workflowId, "dispatch", dispatchKey);
16009
+ return join12(opencodeShipStateDir(commonDir), "runs", workflowId, "dispatch", dispatchKey);
15630
16010
  }
15631
16011
  async function dispatchPath(commonDir, workflowId, dispatchKey) {
15632
- return join9(dispatchDir(commonDir, workflowId, dispatchKey), "dispatch.json");
16012
+ return join12(dispatchDir(commonDir, workflowId, dispatchKey), "dispatch.json");
15633
16013
  }
15634
16014
  function hashPayload(value) {
15635
- return createHash8("sha256").update(JSON.stringify(value), "utf8").digest("hex");
16015
+ return createHash9("sha256").update(JSON.stringify(value), "utf8").digest("hex");
15636
16016
  }
15637
- async function prepareDispatch(repoRoot, workflowId, role, keyInput, payload) {
15638
- if (!ROLE_KEYS.has(role)) {
15639
- throw new Error(`prepareDispatch: unknown role ${role}`);
15640
- }
16017
+ async function prepareDispatchRecord(repoRoot, workflowId, dispatchKey, role, keyInput, payload) {
15641
16018
  const common = await resolveGitCommonDir(repoRoot);
15642
- const dispatchKey = dispatchKeyFor(role, keyInput);
15643
16019
  const commonDir = opencodeShipStateDir(common);
15644
16020
  const dir = dispatchDir(common, workflowId, dispatchKey);
15645
16021
  await mkdir7(dir, { recursive: true });
@@ -15658,10 +16034,10 @@ async function prepareDispatch(repoRoot, workflowId, role, keyInput, payload) {
15658
16034
  }
15659
16035
  async function transitionDispatch(repoRoot, workflowId, dispatchKey, nextState, fields = {}) {
15660
16036
  const common = await resolveGitCommonDir(repoRoot);
15661
- const baseDir = join9(opencodeShipStateDir(common), "runs", workflowId, "dispatch", dispatchKey);
16037
+ const baseDir = join12(opencodeShipStateDir(common), "runs", workflowId, "dispatch", dispatchKey);
15662
16038
  await mkdir7(baseDir, { recursive: true });
15663
16039
  const next = Number(fields.sequence ?? 0);
15664
- const path = join9(baseDir, `seq-${String(next).padStart(6, "0")}.json`);
16040
+ const path = join12(baseDir, `seq-${String(next).padStart(6, "0")}.json`);
15665
16041
  const record2 = {
15666
16042
  workflowId,
15667
16043
  dispatchKey,
@@ -15673,27 +16049,27 @@ async function transitionDispatch(repoRoot, workflowId, dispatchKey, nextState,
15673
16049
  return record2;
15674
16050
  }
15675
16051
  async function readLatestDispatch(repoRoot, workflowId, dispatchKey) {
15676
- const { readdir: readdir10, readFile: readFile25 } = await import("node:fs/promises");
16052
+ const { readdir: readdir10, readFile: readFile27 } = await import("node:fs/promises");
15677
16053
  const common = await resolveGitCommonDir(repoRoot);
15678
- const baseDir = join9(opencodeShipStateDir(common), "runs", workflowId, "dispatch", dispatchKey);
16054
+ const baseDir = join12(opencodeShipStateDir(common), "runs", workflowId, "dispatch", dispatchKey);
15679
16055
  const { readdirSync: readdirSync2, statSync: statSync2 } = await import("node:fs");
15680
16056
  if (!statSync2(baseDir, { throwIfNoEntry: false })) return null;
15681
16057
  const files = readdirSync2(baseDir).filter((f) => f.startsWith("seq-")).sort();
15682
16058
  if (files.length === 0) {
15683
- const initial = join9(baseDir, "dispatch.json");
16059
+ const initial = join12(baseDir, "dispatch.json");
15684
16060
  if (!statSync2(initial, { throwIfNoEntry: false })) return null;
15685
- return JSON.parse(await readFile25(initial, "utf8"));
16061
+ return JSON.parse(await readFile27(initial, "utf8"));
15686
16062
  }
15687
16063
  const last = files[files.length - 1];
15688
- const raw = await readFile25(join9(baseDir, last), "utf8");
16064
+ const raw = await readFile27(join12(baseDir, last), "utf8");
15689
16065
  return JSON.parse(raw);
15690
16066
  }
15691
16067
  async function readPreparedDispatch(repoRoot, workflowId, dispatchKey) {
15692
- const { readFile: readFile25 } = await import("node:fs/promises");
16068
+ const { readFile: readFile27 } = await import("node:fs/promises");
15693
16069
  const common = await resolveGitCommonDir(repoRoot);
15694
- const path = join9(opencodeShipStateDir(common), "runs", workflowId, "dispatch", dispatchKey, "dispatch.json");
16070
+ const path = join12(opencodeShipStateDir(common), "runs", workflowId, "dispatch", dispatchKey, "dispatch.json");
15695
16071
  try {
15696
- return JSON.parse(await readFile25(path, "utf8"));
16072
+ return JSON.parse(await readFile27(path, "utf8"));
15697
16073
  } catch (err) {
15698
16074
  if (err?.code === "ENOENT") return null;
15699
16075
  throw err;
@@ -15704,33 +16080,91 @@ async function dispatchWorker(input) {
15704
16080
  if (!ROLE_KEYS.has(role)) {
15705
16081
  throw new Error(`dispatchWorker: unknown role ${role}`);
15706
16082
  }
16083
+ const dispatchKey = dispatchKeyFor(role, keyInput);
16084
+ return dispatchSession({
16085
+ repoRoot,
16086
+ workflowId,
16087
+ dispatchKey,
16088
+ role,
16089
+ keyInput,
16090
+ payload,
16091
+ client,
16092
+ parentSessionID,
16093
+ titleMarker,
16094
+ agent,
16095
+ model,
16096
+ promptText: payload?.promptText,
16097
+ requireControllerLease: true,
16098
+ caller: "dispatchWorker"
16099
+ });
16100
+ }
16101
+ async function dispatchController({ repoRoot, issueNumber, client, parentSessionID }) {
16102
+ if (!Number.isInteger(issueNumber) || issueNumber <= 0) {
16103
+ throw new Error("dispatchController: positive issueNumber required");
16104
+ }
16105
+ return dispatchSession({
16106
+ repoRoot,
16107
+ workflowId: `wf-${issueNumber}`,
16108
+ dispatchKey: `controller:issue-${issueNumber}`,
16109
+ role: "controller",
16110
+ keyInput: { issueNumber },
16111
+ payload: { issueNumber, agent: "ship-controller" },
16112
+ client,
16113
+ parentSessionID,
16114
+ titleMarker: `ship-controller-wf-${issueNumber}`,
16115
+ agent: "ship-controller",
16116
+ promptText: `Start or resume durable delivery for issue #${issueNumber}. Call ship_plan_start before implementation mutation.`,
16117
+ requireControllerLease: false,
16118
+ caller: "dispatchController"
16119
+ });
16120
+ }
16121
+ async function dispatchSession(input) {
16122
+ const {
16123
+ repoRoot,
16124
+ workflowId,
16125
+ dispatchKey,
16126
+ role,
16127
+ keyInput,
16128
+ payload,
16129
+ client,
16130
+ parentSessionID,
16131
+ titleMarker,
16132
+ agent,
16133
+ model,
16134
+ promptText,
16135
+ requireControllerLease,
16136
+ caller
16137
+ } = input;
15707
16138
  if (!client || typeof client.session?.create !== "function" || typeof client.session?.promptAsync !== "function") {
15708
- throw new Error("dispatchWorker: client.session.create and client.session.promptAsync are required");
16139
+ throw new Error(`${caller}: client.session.create and client.session.promptAsync are required`);
15709
16140
  }
15710
16141
  if (!parentSessionID || typeof parentSessionID !== "string") {
15711
- throw new Error("dispatchWorker: parentSessionID required");
16142
+ throw new Error(`${caller}: parentSessionID required`);
16143
+ }
16144
+ if (requireControllerLease) {
16145
+ await assertControllerLease(repoRoot, workflowId, parentSessionID);
15712
16146
  }
15713
- await assertControllerLease(repoRoot, workflowId, parentSessionID);
15714
- const dispatchKey = dispatchKeyFor(role, keyInput);
15715
16147
  const common = await resolveGitCommonDir(repoRoot);
15716
16148
  const stateDir = opencodeShipStateDir(common);
15717
16149
  const preparedPayload = { ...payload, agent: agent ?? null, model: model ?? null };
16150
+ const parentAudit = requireControllerLease ? { controllerSessionID: parentSessionID } : { parentSessionID };
15718
16151
  return withResourceLock(stateDir, `dispatch:${workflowId}:${dispatchKey}`, async () => {
15719
16152
  let latest = await readLatestDispatch(repoRoot, workflowId, dispatchKey);
15720
16153
  const preparedRecord = await readPreparedDispatch(repoRoot, workflowId, dispatchKey);
15721
16154
  if (preparedRecord && preparedRecord.payloadHash !== hashPayload(preparedPayload)) {
15722
- throw new Error(`dispatchWorker: payload changed for existing dispatch ${dispatchKey}`);
16155
+ throw new Error(`${caller}: payload changed for existing dispatch ${dispatchKey}`);
15723
16156
  }
15724
16157
  if (latest?.state === "prompted" || latest?.state === "completed") {
15725
16158
  return { sessionID: latest.sessionID, dispatchKey };
15726
16159
  }
15727
16160
  if (!preparedRecord) {
15728
- await prepareDispatch(repoRoot, workflowId, role, keyInput, preparedPayload);
16161
+ await prepareDispatchRecord(repoRoot, workflowId, dispatchKey, role, keyInput, preparedPayload);
15729
16162
  latest = { state: "prepared", sequence: 0 };
15730
16163
  }
15731
16164
  let sequence = Number(latest?.sequence ?? 0);
15732
16165
  const title = titleMarker ?? `ship-${role}-${dispatchKey}`;
15733
- let sessionID = latest?.state === "created" ? latest.sessionID : null;
16166
+ const canReuseSession = latest?.state === "created" || latest?.state === "failed" && latest?.lastError?.startsWith("promptAsync:");
16167
+ let sessionID = canReuseSession ? latest?.sessionID ?? null : null;
15734
16168
  if (!sessionID) {
15735
16169
  try {
15736
16170
  const created = await client.session.create({
@@ -15738,12 +16172,12 @@ async function dispatchWorker(input) {
15738
16172
  query: { directory: repoRoot }
15739
16173
  });
15740
16174
  if (created?.error) {
15741
- throw new Error(`dispatchWorker: session.create failed: ${formatSdkError(created.error)}`);
16175
+ throw new Error(`${caller}: session.create failed: ${formatSdkError(created.error)}`);
15742
16176
  }
15743
16177
  const createdData = created?.data ?? created;
15744
16178
  sessionID = createdData?.id ?? createdData?.sessionID;
15745
16179
  if (!sessionID) {
15746
- throw new Error(`dispatchWorker: client.session.create did not return a session id`);
16180
+ throw new Error(`${caller}: client.session.create did not return a session id`);
15747
16181
  }
15748
16182
  } catch (err) {
15749
16183
  sequence += 1;
@@ -15757,12 +16191,12 @@ async function dispatchWorker(input) {
15757
16191
  await transitionDispatch(repoRoot, workflowId, dispatchKey, "created", {
15758
16192
  sequence,
15759
16193
  sessionID,
15760
- controllerSessionID: parentSessionID
16194
+ ...parentAudit
15761
16195
  });
15762
16196
  }
15763
16197
  try {
15764
16198
  const body = {
15765
- parts: [{ type: "text", text: String(payload?.promptText ?? "") }]
16199
+ parts: [{ type: "text", text: String(promptText ?? "") }]
15766
16200
  };
15767
16201
  if (agent) body.agent = agent;
15768
16202
  if (model) body.model = parseModelId(model);
@@ -15772,14 +16206,14 @@ async function dispatchWorker(input) {
15772
16206
  query: { directory: repoRoot }
15773
16207
  });
15774
16208
  if (prompted?.error) {
15775
- throw new Error(`dispatchWorker: session.promptAsync failed: ${formatSdkError(prompted.error)}`);
16209
+ throw new Error(`${caller}: session.promptAsync failed: ${formatSdkError(prompted.error)}`);
15776
16210
  }
15777
16211
  } catch (err) {
15778
16212
  sequence += 1;
15779
16213
  await transitionDispatch(repoRoot, workflowId, dispatchKey, "failed", {
15780
16214
  sequence,
15781
16215
  sessionID,
15782
- controllerSessionID: parentSessionID,
16216
+ ...parentAudit,
15783
16217
  lastError: `promptAsync: ${err?.message ?? err}`
15784
16218
  });
15785
16219
  throw err;
@@ -15788,7 +16222,7 @@ async function dispatchWorker(input) {
15788
16222
  await transitionDispatch(repoRoot, workflowId, dispatchKey, "prompted", {
15789
16223
  sequence,
15790
16224
  sessionID,
15791
- controllerSessionID: parentSessionID
16225
+ ...parentAudit
15792
16226
  });
15793
16227
  return { sessionID, dispatchKey };
15794
16228
  });
@@ -15812,7 +16246,7 @@ function formatSdkError(error45) {
15812
16246
  async function issueControllerLease(repoRoot, workflowId, controllerSessionID) {
15813
16247
  const { atomicReplaceJson: atomicReplaceJson2 } = await Promise.resolve().then(() => (init_durable_store(), durable_store_exports));
15814
16248
  const common = await resolveGitCommonDir(repoRoot);
15815
- const path = join9(opencodeShipStateDir(common), "runs", workflowId, "controller.json");
16249
+ const path = join12(opencodeShipStateDir(common), "runs", workflowId, "controller.json");
15816
16250
  await mkdir7(dirnameOf(path), { recursive: true });
15817
16251
  await atomicReplaceJson2(path, {
15818
16252
  workflowId,
@@ -15821,13 +16255,13 @@ async function issueControllerLease(repoRoot, workflowId, controllerSessionID) {
15821
16255
  });
15822
16256
  }
15823
16257
  async function readControllerLease(repoRoot, workflowId) {
15824
- const { readFile: readFile25 } = await import("node:fs/promises");
15825
- const { existsSync: existsSync28 } = await import("node:fs");
16258
+ const { readFile: readFile27 } = await import("node:fs/promises");
16259
+ const { existsSync: existsSync29 } = await import("node:fs");
15826
16260
  const common = await resolveGitCommonDir(repoRoot);
15827
- const path = join9(opencodeShipStateDir(common), "runs", workflowId, "controller.json");
15828
- if (!existsSync28(path)) return null;
16261
+ const path = join12(opencodeShipStateDir(common), "runs", workflowId, "controller.json");
16262
+ if (!existsSync29(path)) return null;
15829
16263
  try {
15830
- return JSON.parse(await readFile25(path, "utf8"));
16264
+ return JSON.parse(await readFile27(path, "utf8"));
15831
16265
  } catch {
15832
16266
  return null;
15833
16267
  }
@@ -15896,7 +16330,118 @@ var ROLES = Object.freeze({
15896
16330
  FINAL_SPEC: ROLE_FINAL_SPEC
15897
16331
  });
15898
16332
 
16333
+ // src/tools/ship-deliver.js
16334
+ function createDeliverTool(deps) {
16335
+ return async function deliver(input) {
16336
+ const opId = input.operationId ?? `deliver-${Date.now().toString(36)}`;
16337
+ const issueNumber = input.issueNumber;
16338
+ if (!Number.isInteger(issueNumber) || issueNumber <= 0) {
16339
+ return failure("deliver", "positive issueNumber required", {
16340
+ operationId: opId,
16341
+ retryable: false,
16342
+ details: { workflowId: null, controllerSessionID: null }
16343
+ });
16344
+ }
16345
+ const workflowId = `wf-${issueNumber}`;
16346
+ const ctx = deps.ctx ?? null;
16347
+ if (!ctx || typeof ctx.sessionID !== "string" || ctx.sessionID.length === 0) {
16348
+ return failure("deliver", "ToolContext.sessionID required (Build session)", {
16349
+ operationId: opId,
16350
+ retryable: false,
16351
+ details: { workflowId, controllerSessionID: null }
16352
+ });
16353
+ }
16354
+ if (ctx.agent !== "build") {
16355
+ return failure("deliver", "ToolContext.agent must be Build", {
16356
+ operationId: opId,
16357
+ retryable: false,
16358
+ details: { workflowId, controllerSessionID: null }
16359
+ });
16360
+ }
16361
+ const client = deps.opencodeClient;
16362
+ if (!client || typeof client.session?.create !== "function" || typeof client.session?.promptAsync !== "function") {
16363
+ return failure("deliver", "OpenCode client is unavailable", {
16364
+ operationId: opId,
16365
+ retryable: false,
16366
+ details: { workflowId, controllerSessionID: null }
16367
+ });
16368
+ }
16369
+ const lock = await readLock2(deps.repoRoot);
16370
+ if (!isSetupComplete(lock)) {
16371
+ return failure("deliver", "setup is not complete; run /setup-ship-workflow first", {
16372
+ operationId: opId,
16373
+ retryable: false,
16374
+ details: { workflowId, controllerSessionID: null }
16375
+ });
16376
+ }
16377
+ try {
16378
+ const dispatched = await dispatchController({
16379
+ repoRoot: deps.repoRoot,
16380
+ issueNumber,
16381
+ client,
16382
+ parentSessionID: ctx.sessionID
16383
+ });
16384
+ return success2("deliver", {
16385
+ workflowId,
16386
+ controllerSessionID: dispatched.sessionID,
16387
+ dispatchKey: dispatched.dispatchKey
16388
+ }, { operationId: opId });
16389
+ } catch (err) {
16390
+ return failure("deliver", String(err?.message ?? err), {
16391
+ operationId: opId,
16392
+ retryable: true,
16393
+ details: { workflowId, controllerSessionID: null }
16394
+ });
16395
+ }
16396
+ };
16397
+ }
16398
+
16399
+ // src/installer/engineering-config.js
16400
+ var MODEL_ID_RE = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
16401
+ var DEFAULTS = Object.freeze({
16402
+ planner: "openai/gpt-5.6-sol",
16403
+ builder: "minimax/MiniMax-M3",
16404
+ finalReviewer: "openai/gpt-5.6-sol"
16405
+ });
16406
+ function resolveModelRoles(cfg, { strict = false, allowDeferred = false } = {}) {
16407
+ const REQUIRED = ["planner", "builder", "finalReviewer"];
16408
+ if (strict) {
16409
+ const issues = [];
16410
+ for (const role of REQUIRED) {
16411
+ const id = cfg?.models?.[role];
16412
+ if (typeof id !== "string" || id.length === 0 || !MODEL_ID_RE.test(id)) {
16413
+ issues.push(role);
16414
+ }
16415
+ }
16416
+ if (issues.length > 0) {
16417
+ throw new Error(`resolveModelRoles: required role(s) missing or invalid: ${issues.join(", ")}`);
16418
+ }
16419
+ return { planner: cfg.models.planner, builder: cfg.models.builder, finalReviewer: cfg.models.finalReviewer };
16420
+ }
16421
+ const out = { ...DEFAULTS };
16422
+ if (cfg && cfg.models) {
16423
+ for (const [role, id] of Object.entries(cfg.models)) {
16424
+ if (id && typeof id === "string" && id.length > 0) {
16425
+ out[role] = id;
16426
+ } else if (strict && Object.prototype.hasOwnProperty.call(cfg.models, role)) {
16427
+ throw new Error(`resolveModelRoles: user provided empty model id for '${role}'`);
16428
+ }
16429
+ }
16430
+ }
16431
+ if (allowDeferred) {
16432
+ return { planner: out.planner ?? null, builder: out.builder ?? null, finalReviewer: out.finalReviewer ?? null };
16433
+ }
16434
+ for (const role of REQUIRED) {
16435
+ if (!out[role]) {
16436
+ throw new Error(`resolveModelRoles: required role '${role}' missing and no default available`);
16437
+ }
16438
+ }
16439
+ return out;
16440
+ }
16441
+
15899
16442
  // src/tools/ship-plan-start.js
16443
+ import { join as join13 } from "node:path";
16444
+ import { mkdir as mkdir8, writeFile as writeFile6 } from "node:fs/promises";
15900
16445
  function normalizeWorkflowId(issueNumber) {
15901
16446
  return `wf-${issueNumber}`;
15902
16447
  }
@@ -15925,7 +16470,7 @@ function createPlanStartTool(deps) {
15925
16470
  const repoRoot = deps.repoRoot;
15926
16471
  try {
15927
16472
  const commonDir = await resolveGitCommonDir(repoRoot);
15928
- const wfDir = join10(opencodeShipStateDir(commonDir), "plans", workflowId);
16473
+ const wfDir = join13(opencodeShipStateDir(commonDir), "plans", workflowId);
15929
16474
  await mkdir8(wfDir, { recursive: true });
15930
16475
  await issueControllerLease(repoRoot, workflowId, ctx.sessionID);
15931
16476
  const matchingManifests = (await listManifests(repoRoot)).filter((manifest) => manifest.issueNumber === issueNumber);
@@ -15968,7 +16513,7 @@ function createPlanStartTool(deps) {
15968
16513
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
15969
16514
  state: "drafting"
15970
16515
  };
15971
- await writeFile6(join10(wfDir, "index.json"), JSON.stringify(indexRecord, null, 2), "utf8");
16516
+ await writeFile6(join13(wfDir, "index.json"), JSON.stringify(indexRecord, null, 2), "utf8");
15972
16517
  return success2("plan-start", {
15973
16518
  workflowId,
15974
16519
  issueNumber,
@@ -15988,7 +16533,7 @@ function createPlanStartTool(deps) {
15988
16533
  }
15989
16534
 
15990
16535
  // src/workflow/plan.js
15991
- import { createHash as createHash9 } from "node:crypto";
16536
+ import { createHash as createHash10 } from "node:crypto";
15992
16537
  function isPlainObject2(v) {
15993
16538
  return v !== null && typeof v === "object" && !Array.isArray(v);
15994
16539
  }
@@ -16245,21 +16790,21 @@ function computePlanHash(plan) {
16245
16790
  return sha2563(json2);
16246
16791
  }
16247
16792
  function sha2563(text) {
16248
- return createHash9("sha256").update(text, "utf8").digest("hex");
16793
+ return createHash10("sha256").update(text, "utf8").digest("hex");
16249
16794
  }
16250
16795
 
16251
16796
  // src/workflow/plan-store.js
16252
- import { readFile as readFile9, writeFile as writeFile7, mkdir as mkdir9, readdir as readdir5, unlink as unlink4 } from "node:fs/promises";
16253
- import { existsSync as existsSync9 } from "node:fs";
16254
- import { join as join11, dirname as dirname5 } from "node:path";
16797
+ import { readFile as readFile11, writeFile as writeFile7, mkdir as mkdir9, readdir as readdir5, unlink as unlink4 } from "node:fs/promises";
16798
+ import { existsSync as existsSync11 } from "node:fs";
16799
+ import { join as join14, dirname as dirname6 } from "node:path";
16255
16800
  init_durable_store();
16256
- import { createHash as createHash10 } from "node:crypto";
16801
+ import { createHash as createHash11 } from "node:crypto";
16257
16802
  function revisionsDir(commonDir, workflowId) {
16258
- return join11(opencodeShipStateDir(commonDir), "plans", workflowId, "revisions");
16803
+ return join14(opencodeShipStateDir(commonDir), "plans", workflowId, "revisions");
16259
16804
  }
16260
16805
  function revisionDir(commonDir, workflowId, revision) {
16261
16806
  const n = String(revision).padStart(6, "0");
16262
- return join11(revisionsDir(commonDir, workflowId), n);
16807
+ return join14(revisionsDir(commonDir, workflowId), n);
16263
16808
  }
16264
16809
  async function resolveCommon(repoRoot) {
16265
16810
  return resolveGitCommonDir(repoRoot);
@@ -16273,9 +16818,9 @@ async function publishPlanRevision(repoRoot, plan) {
16273
16818
  const common = await resolveCommon(repoRoot);
16274
16819
  const dir = revisionDir(common, plan.workflowId, plan.revision);
16275
16820
  await mkdir9(dir, { recursive: true });
16276
- const planPath = join11(dir, "plan.json");
16277
- if (existsSync9(planPath)) {
16278
- const existing = JSON.parse(await readFile9(planPath, "utf8"));
16821
+ const planPath = join14(dir, "plan.json");
16822
+ if (existsSync11(planPath)) {
16823
+ const existing = JSON.parse(await readFile11(planPath, "utf8"));
16279
16824
  if (existing?.plan?.workflowId !== plan.workflowId) {
16280
16825
  throw new Error(`publishPlanRevision: workflowId mismatch on existing record at ${planPath}`);
16281
16826
  }
@@ -16305,15 +16850,15 @@ async function publishApproval(repoRoot, approval) {
16305
16850
  const common = await resolveCommon(repoRoot);
16306
16851
  const dir = revisionDir(common, approval.workflowId, approval.revision);
16307
16852
  await mkdir9(dir, { recursive: true });
16308
- const path = join11(dir, "approval.json");
16309
- const planPath = join11(dir, "plan.json");
16310
- if (existsSync9(planPath)) {
16311
- const planRecord = JSON.parse(await readFile9(planPath, "utf8"));
16853
+ const path = join14(dir, "approval.json");
16854
+ const planPath = join14(dir, "plan.json");
16855
+ if (existsSync11(planPath)) {
16856
+ const planRecord = JSON.parse(await readFile11(planPath, "utf8"));
16312
16857
  if (planRecord.hash !== approval.sha256) {
16313
16858
  throw new Error(`publishApproval: sha256 mismatch with plan record (plan ${planRecord.hash?.slice(0, 8)}, approval ${approval.sha256.slice(0, 8)})`);
16314
16859
  }
16315
16860
  }
16316
- if (existsSync9(path)) {
16861
+ if (existsSync11(path)) {
16317
16862
  return { recorded: false, path };
16318
16863
  }
16319
16864
  await publishImmutableJson(path, { ...approval, publishedAt: (/* @__PURE__ */ new Date()).toISOString() });
@@ -16321,10 +16866,10 @@ async function publishApproval(repoRoot, approval) {
16321
16866
  }
16322
16867
  async function readPlanRevision(repoRoot, workflowId, revision) {
16323
16868
  const common = await resolveCommon(repoRoot);
16324
- const path = join11(revisionDir(common, workflowId, revision), "plan.json");
16325
- if (!existsSync9(path)) return null;
16869
+ const path = join14(revisionDir(common, workflowId, revision), "plan.json");
16870
+ if (!existsSync11(path)) return null;
16326
16871
  try {
16327
- const raw = await readFile9(path, "utf8");
16872
+ const raw = await readFile11(path, "utf8");
16328
16873
  return JSON.parse(raw);
16329
16874
  } catch {
16330
16875
  return null;
@@ -16332,14 +16877,14 @@ async function readPlanRevision(repoRoot, workflowId, revision) {
16332
16877
  }
16333
16878
 
16334
16879
  // src/tools/ship-plan-submit.js
16335
- var SAFE_ID_RE3 = /^[A-Za-z0-9._-]{1,128}$/;
16880
+ var SAFE_ID_RE4 = /^[A-Za-z0-9._-]{1,128}$/;
16336
16881
  function createPlanSubmitTool(deps) {
16337
16882
  return async function planSubmit(input) {
16338
16883
  const opId = input.operationId ?? `plan-submit-${Date.now().toString(36)}`;
16339
16884
  const workflowId = String(input.workflowId ?? "");
16340
16885
  const revision = Number(input.revision);
16341
16886
  const plan = input.plan;
16342
- if (!workflowId || !SAFE_ID_RE3.test(workflowId)) {
16887
+ if (!workflowId || !SAFE_ID_RE4.test(workflowId)) {
16343
16888
  return failure("plan-submit", "workflowId required (safe id)", { operationId: opId, retryable: false });
16344
16889
  }
16345
16890
  if (!Number.isInteger(revision) || revision <= 0) {
@@ -16390,7 +16935,7 @@ function createPlanSubmitTool(deps) {
16390
16935
  }
16391
16936
 
16392
16937
  // src/tools/ship-plan-approve.js
16393
- var SAFE_ID_RE4 = /^[A-Za-z0-9._-]{1,128}$/;
16938
+ var SAFE_ID_RE5 = /^[A-Za-z0-9._-]{1,128}$/;
16394
16939
  function createPlanApproveTool(deps) {
16395
16940
  return async function planApprove(input) {
16396
16941
  const opId = input.operationId ?? `plan-approve-${Date.now().toString(36)}`;
@@ -16398,7 +16943,7 @@ function createPlanApproveTool(deps) {
16398
16943
  const revision = Number(input.revision);
16399
16944
  const sha2565 = String(input.sha256 ?? "");
16400
16945
  const subject = String(input.subject ?? "");
16401
- if (!workflowId || !SAFE_ID_RE4.test(workflowId)) {
16946
+ if (!workflowId || !SAFE_ID_RE5.test(workflowId)) {
16402
16947
  return failure("plan-approve", "workflowId required (safe id)", { operationId: opId, retryable: false });
16403
16948
  }
16404
16949
  if (!Number.isInteger(revision) || revision <= 0) {
@@ -16446,20 +16991,20 @@ function createPlanApproveTool(deps) {
16446
16991
  }
16447
16992
 
16448
16993
  // src/tools/ship-run-start.js
16449
- import { readFile as readFile10 } from "node:fs/promises";
16450
- import { join as join12 } from "node:path";
16451
- import { execFile } from "node:child_process";
16452
- var SAFE_ID_RE5 = /^[A-Za-z0-9._-]{1,128}$/;
16994
+ import { readFile as readFile12 } from "node:fs/promises";
16995
+ import { join as join15 } from "node:path";
16996
+ import { execFile as execFile2 } from "node:child_process";
16997
+ var SAFE_ID_RE6 = /^[A-Za-z0-9._-]{1,128}$/;
16453
16998
  async function readRevisionRecord(repoRoot, workflowId, revision) {
16454
16999
  const commonDir = await resolveGitCommonDir(repoRoot);
16455
17000
  const rev = String(revision).padStart(6, "0");
16456
- const dir = join12(opencodeShipStateDir(commonDir), "plans", workflowId, "revisions", rev);
16457
- const planRaw = await readFile10(join12(dir, "plan.json"), "utf8");
17001
+ const dir = join15(opencodeShipStateDir(commonDir), "plans", workflowId, "revisions", rev);
17002
+ const planRaw = await readFile12(join15(dir, "plan.json"), "utf8");
16458
17003
  const plan = JSON.parse(planRaw);
16459
- const approvalPath = join12(dir, "approval.json");
17004
+ const approvalPath = join15(dir, "approval.json");
16460
17005
  let approval = null;
16461
17006
  try {
16462
- approval = JSON.parse(await readFile10(approvalPath, "utf8"));
17007
+ approval = JSON.parse(await readFile12(approvalPath, "utf8"));
16463
17008
  } catch {
16464
17009
  approval = null;
16465
17010
  }
@@ -16469,7 +17014,7 @@ function createRunStartTool(deps) {
16469
17014
  return async function runStart(input) {
16470
17015
  const opId = input.operationId ?? `run-start-${Date.now().toString(36)}`;
16471
17016
  const workflowId = String(input.workflowId ?? "");
16472
- if (!workflowId || !SAFE_ID_RE5.test(workflowId)) {
17017
+ if (!workflowId || !SAFE_ID_RE6.test(workflowId)) {
16473
17018
  return failure("run-start", "workflowId required (safe id)", { operationId: opId, retryable: false });
16474
17019
  }
16475
17020
  const ctx = input.ctx ?? deps.ctx ?? null;
@@ -16527,7 +17072,7 @@ function createRunStartTool(deps) {
16527
17072
  }
16528
17073
  function gitHead(cwd) {
16529
17074
  return new Promise((resolveP, rejectP) => {
16530
- execFile("git", ["-C", cwd, "rev-parse", "HEAD"], { cwd, shell: false }, (err, stdout, stderr) => {
17075
+ execFile2("git", ["-C", cwd, "rev-parse", "HEAD"], { cwd, shell: false }, (err, stdout, stderr) => {
16531
17076
  if (err) return rejectP(new Error(stderr || err.message));
16532
17077
  resolveP(String(stdout).trim());
16533
17078
  });
@@ -16535,20 +17080,94 @@ function gitHead(cwd) {
16535
17080
  }
16536
17081
 
16537
17082
  // src/tools/ship-task-start.js
16538
- import { mkdir as mkdir10 } from "node:fs/promises";
16539
- import { join as join13 } from "node:path";
16540
- init_durable_store();
16541
- import { createHash as createHash11 } from "node:crypto";
16542
- var SAFE_ID_RE6 = /^[A-Za-z0-9._-]{1,128}$/;
17083
+ import { mkdir as mkdir10 } from "node:fs/promises";
17084
+ import { join as join16 } from "node:path";
17085
+ init_durable_store();
17086
+ import { createHash as createHash12 } from "node:crypto";
17087
+
17088
+ // src/workflow/worktree-resolver.js
17089
+ function failureRecord(kind, details = {}) {
17090
+ return { ok: false, kind, ...details };
17091
+ }
17092
+ async function resolveWorkflowWorktree(repoRoot, workflowId) {
17093
+ const runState = await readRunState(repoRoot, workflowId);
17094
+ if (!runState) {
17095
+ return failureRecord("missing-workflow-run", { workflowId });
17096
+ }
17097
+ const planRecord = await readPlanRevision(repoRoot, workflowId, runState.revision);
17098
+ if (!planRecord) {
17099
+ return failureRecord("missing-workflow-plan", {
17100
+ workflowId,
17101
+ revision: runState.revision
17102
+ });
17103
+ }
17104
+ if (planRecord.hash !== runState.sha256) {
17105
+ return failureRecord("workflow-plan-mismatch", {
17106
+ workflowId,
17107
+ revision: runState.revision,
17108
+ expected: runState.sha256,
17109
+ received: planRecord.hash
17110
+ });
17111
+ }
17112
+ const issueNumber = planRecord.plan?.source?.issueNumber;
17113
+ if (!Number.isInteger(issueNumber) || issueNumber <= 0) {
17114
+ return failureRecord("missing-workflow-issue", {
17115
+ workflowId,
17116
+ revision: runState.revision
17117
+ });
17118
+ }
17119
+ const matches = (await listManifests(repoRoot)).filter((manifest2) => manifest2.issueNumber === issueNumber);
17120
+ if (matches.length !== 1) {
17121
+ return failureRecord("ambiguous-workflow-manifest", {
17122
+ issueNumber,
17123
+ count: matches.length
17124
+ });
17125
+ }
17126
+ const manifest = matches[0];
17127
+ if (manifest.schemaVersion !== 2) {
17128
+ return failureRecord("workflow-mismatch", {
17129
+ expectedSchema: 2,
17130
+ receivedSchema: manifest.schemaVersion
17131
+ });
17132
+ }
17133
+ if (manifest.workflowId !== workflowId) {
17134
+ return failureRecord("workflow-mismatch", {
17135
+ expected: workflowId,
17136
+ received: manifest.workflowId
17137
+ });
17138
+ }
17139
+ if (!manifest.worktreePath) {
17140
+ return failureRecord("missing-worktree-path", { taskId: manifest.taskId });
17141
+ }
17142
+ const linked = await validateLinkedWorktree(repoRoot, manifest.worktreePath, {
17143
+ allowCurrentLinked: true
17144
+ });
17145
+ if (!linked.ok) {
17146
+ return failureRecord("invalid-worktree", {
17147
+ reason: linked.kind,
17148
+ message: linked.message
17149
+ });
17150
+ }
17151
+ return {
17152
+ ok: true,
17153
+ workflowId,
17154
+ issueNumber,
17155
+ manifest,
17156
+ worktreePath: linked.path
17157
+ };
17158
+ }
17159
+
17160
+ // src/tools/ship-task-start.js
17161
+ var SAFE_ID_RE7 = /^[A-Za-z0-9._-]{1,128}$/;
16543
17162
  function createTaskStartTool(deps) {
16544
17163
  return async function taskStart(input) {
16545
17164
  const opId = input.operationId ?? `task-start-${Date.now().toString(36)}`;
16546
17165
  const workflowId = String(input.workflowId ?? "");
16547
17166
  const taskId = String(input.taskId ?? "");
16548
- if (!workflowId || !SAFE_ID_RE6.test(workflowId)) {
17167
+ if (!workflowId || !SAFE_ID_RE7.test(workflowId)) {
16549
17168
  return failure("task-start", "workflowId required (safe id)", { operationId: opId, retryable: false });
16550
17169
  }
16551
- if (!taskId || !SAFE_ID_RE6.test(taskId)) {
17170
+ if (!taskId || !SAFE_ID_RE7.test(taskId)) {
16552
17171
  return failure("task-start", "taskId required (safe id)", { operationId: opId, retryable: false });
16553
17172
  }
16554
17173
  const ctx = input.ctx ?? deps.ctx ?? null;
@@ -16588,13 +17207,21 @@ function createTaskStartTool(deps) {
16588
17207
  if (unsatisfied.length > 0) {
16589
17208
  return failure("task-start", `task dependencies are incomplete: ${unsatisfied.join(", ")}`, { operationId: opId, retryable: false });
16590
17209
  }
16591
- const briefHash = createHash11("sha256").update(canonicalJson(task), "utf8").digest("hex");
17210
+ const briefHash = createHash12("sha256").update(canonicalJson(task), "utf8").digest("hex");
16592
17211
  const round = runState.round > 0 ? runState.round : 1;
16593
17212
  try {
17213
+ const resolved = await resolveWorkflowWorktree(deps.repoRoot, workflowId);
17214
+ if (!resolved.ok) {
17215
+ return failure("task-start", `workflow worktree resolution failed: ${resolved.kind}`, {
17216
+ operationId: opId,
17217
+ retryable: false,
17218
+ details: resolved
17219
+ });
17220
+ }
16594
17221
  let dispatchResult = null;
16595
17222
  if (deps.opencodeClient) {
16596
17223
  dispatchResult = await dispatchWorker({
16597
- repoRoot: deps.repoRoot,
17224
+ repoRoot: resolved.worktreePath,
16598
17225
  workflowId,
16599
17226
  role: ROLES.BUILDER,
16600
17227
  keyInput: { taskId, round },
@@ -16614,7 +17241,7 @@ ${JSON.stringify(task, null, 2)}`
16614
17241
  });
16615
17242
  }
16616
17243
  const commonDir = await resolveGitCommonDir(deps.repoRoot);
16617
- const dispatchDir2 = join13(
17244
+ const dispatchDir2 = join16(
16618
17245
  opencodeShipStateDir(commonDir),
16619
17246
  "runs",
16620
17247
  workflowId,
@@ -16637,7 +17264,7 @@ ${JSON.stringify(task, null, 2)}`
16637
17264
  task,
16638
17265
  dispatchedAt: (/* @__PURE__ */ new Date()).toISOString()
16639
17266
  };
16640
- await publishImmutableJson(join13(dispatchDir2, "dispatch.json"), record2);
17267
+ await publishImmutableJson(join16(dispatchDir2, "dispatch.json"), record2);
16641
17268
  const { state, event } = await appendRunEvent(
16642
17269
  deps.repoRoot,
16643
17270
  workflowId,
@@ -16663,15 +17290,15 @@ ${JSON.stringify(task, null, 2)}`
16663
17290
  }
16664
17291
 
16665
17292
  // src/tools/ship-task-commit.js
16666
- import { execFile as execFile2 } from "node:child_process";
16667
- import { mkdir as mkdir11, readFile as readFile11 } from "node:fs/promises";
16668
- import { existsSync as existsSync10 } from "node:fs";
16669
- import { join as join14 } from "node:path";
17293
+ import { execFile as execFile3 } from "node:child_process";
17294
+ import { mkdir as mkdir11, readFile as readFile13 } from "node:fs/promises";
17295
+ import { existsSync as existsSync12 } from "node:fs";
17296
+ import { join as join17 } from "node:path";
16670
17297
  init_durable_store();
16671
- var SAFE_ID_RE7 = /^[A-Za-z0-9._-]{1,128}$/;
17298
+ var SAFE_ID_RE8 = /^[A-Za-z0-9._-]{1,128}$/;
16672
17299
  function spawn5(cmd, args, cwd) {
16673
17300
  return new Promise((resolveP, rejectP) => {
16674
- execFile2(cmd, args, { cwd, shell: false }, (err, stdout, stderr) => {
17301
+ execFile3(cmd, args, { cwd, shell: false }, (err, stdout, stderr) => {
16675
17302
  if (err) {
16676
17303
  const msg = typeof stderr === "string" ? stderr : stderr ? String(stderr) : err.message;
16677
17304
  return rejectP(new Error(`${cmd} failed: ${msg}`));
@@ -16690,10 +17317,10 @@ function createTaskCommitTool(deps) {
16690
17317
  const planHash = String(input.planHash ?? "");
16691
17318
  const reviewHash = String(input.reviewHash ?? "");
16692
17319
  const round = Number(input.round ?? 1);
16693
- if (!workflowId || !SAFE_ID_RE7.test(workflowId)) {
17320
+ if (!workflowId || !SAFE_ID_RE8.test(workflowId)) {
16694
17321
  return failure("task-commit", "workflowId required (safe id)", { operationId: opId, retryable: false });
16695
17322
  }
16696
- if (!taskId || !SAFE_ID_RE7.test(taskId)) {
17323
+ if (!taskId || !SAFE_ID_RE8.test(taskId)) {
16697
17324
  return failure("task-commit", "taskId required (safe id)", { operationId: opId, retryable: false });
16698
17325
  }
16699
17326
  if (!/^[0-9a-f]{40}$/.test(expectedHead)) {
@@ -16755,19 +17382,27 @@ function createTaskCommitTool(deps) {
16755
17382
  return failure("task-commit", `round does not match the active run (${runState.round})`, { operationId: opId, retryable: false });
16756
17383
  }
16757
17384
  try {
16758
- const actualHead = (await spawn5("git", ["-C", deps.repoRoot, "rev-parse", "HEAD"], deps.repoRoot)).trim();
17385
+ const resolved = await resolveWorkflowWorktree(deps.repoRoot, workflowId);
17386
+ if (!resolved.ok) {
17387
+ return failure("task-commit", `workflow worktree resolution failed: ${resolved.kind}`, {
17388
+ operationId: opId,
17389
+ retryable: false,
17390
+ details: resolved
17391
+ });
17392
+ }
17393
+ const actualHead = (await spawn5("git", ["-C", resolved.worktreePath, "rev-parse", "HEAD"], resolved.worktreePath)).trim();
16759
17394
  if (actualHead !== expectedHead) {
16760
17395
  return failure("task-commit", `HEAD drift (expected ${expectedHead.slice(0, 8)}, got ${actualHead.slice(0, 8)})`, { operationId: opId, retryable: false });
16761
17396
  }
16762
17397
  const trailers = buildCommitTrailers({ workflowId, planHash, taskId, round, reviewHash });
16763
- const message = await spawn5("git", ["-C", deps.repoRoot, "log", "-1", "--format=%B", expectedHead], deps.repoRoot);
17398
+ const message = await spawn5("git", ["-C", resolved.worktreePath, "log", "-1", "--format=%B", expectedHead], resolved.worktreePath);
16764
17399
  const trailerLines = trailers.map((t) => ` ${t}`).join("\n");
16765
17400
  const missingTrailer = trailers.find((trailer) => !message.includes(trailer));
16766
17401
  if (missingTrailer) {
16767
17402
  return failure("task-commit", `commit ${expectedHead.slice(0, 8)} missing trailer: ${missingTrailer}`, { operationId: opId, retryable: false });
16768
17403
  }
16769
17404
  const commonDir = await resolveGitCommonDir(deps.repoRoot);
16770
- const commitDir = join14(opencodeShipStateDir(commonDir), "runs", workflowId, "tasks", taskId, "commit");
17405
+ const commitDir = join17(opencodeShipStateDir(commonDir), "runs", workflowId, "tasks", taskId, "commit");
16771
17406
  await mkdir11(commitDir, { recursive: true });
16772
17407
  const record2 = {
16773
17408
  workflowId,
@@ -16780,9 +17415,9 @@ function createTaskCommitTool(deps) {
16780
17415
  trailerBlock: trailerLines,
16781
17416
  committedAt: (/* @__PURE__ */ new Date()).toISOString()
16782
17417
  };
16783
- const commitPath = join14(commitDir, "commit.json");
16784
- if (existsSync10(commitPath)) {
16785
- const existing = JSON.parse(await readFile11(commitPath, "utf8"));
17418
+ const commitPath = join17(commitDir, "commit.json");
17419
+ if (existsSync12(commitPath)) {
17420
+ const existing = JSON.parse(await readFile13(commitPath, "utf8"));
16786
17421
  if (existing.workflowId !== workflowId || existing.taskId !== taskId || existing.round !== round || existing.commitSha !== commitSha || existing.planHash !== planHash || existing.reviewHash !== reviewHash) {
16787
17422
  return failure("task-commit", "immutable task commit conflicts with retry", { operationId: opId, retryable: false });
16788
17423
  }
@@ -16803,13 +17438,13 @@ function createTaskCommitTool(deps) {
16803
17438
  }
16804
17439
 
16805
17440
  // src/tools/ship-task-complete.js
16806
- import { mkdir as mkdir12, readFile as readFile12 } from "node:fs/promises";
16807
- import { existsSync as existsSync11 } from "node:fs";
16808
- import { join as join15 } from "node:path";
16809
- import { execFile as execFile3 } from "node:child_process";
16810
- import { createHash as createHash12 } from "node:crypto";
17441
+ import { mkdir as mkdir12, readFile as readFile14 } from "node:fs/promises";
17442
+ import { existsSync as existsSync13 } from "node:fs";
17443
+ import { join as join18 } from "node:path";
17444
+ import { execFile as execFile4 } from "node:child_process";
17445
+ import { createHash as createHash13 } from "node:crypto";
16811
17446
  init_durable_store();
16812
- var SAFE_ID_RE8 = /^[A-Za-z0-9._-]{1,128}$/;
17447
+ var SAFE_ID_RE9 = /^[A-Za-z0-9._-]{1,128}$/;
16813
17448
  function createTaskCompleteTool(deps) {
16814
17449
  return async function taskComplete(input) {
16815
17450
  const opId = input.operationId ?? `task-complete-${Date.now().toString(36)}`;
@@ -16818,16 +17453,16 @@ function createTaskCompleteTool(deps) {
16818
17453
  const moreTasks = input.moreTasks === false ? false : input.moreTasks === true ? true : null;
16819
17454
  const nextTaskId = input.nextTaskId ? String(input.nextTaskId) : null;
16820
17455
  const expectedHead = String(input.expectedHead ?? "");
16821
- if (!workflowId || !SAFE_ID_RE8.test(workflowId)) {
17456
+ if (!workflowId || !SAFE_ID_RE9.test(workflowId)) {
16822
17457
  return failure("task-complete", "workflowId required (safe id)", { operationId: opId, retryable: false });
16823
17458
  }
16824
- if (!taskId || !SAFE_ID_RE8.test(taskId)) {
17459
+ if (!taskId || !SAFE_ID_RE9.test(taskId)) {
16825
17460
  return failure("task-complete", "taskId required (safe id)", { operationId: opId, retryable: false });
16826
17461
  }
16827
17462
  if (moreTasks === null) {
16828
17463
  return failure("task-complete", "moreTasks must be explicitly true or false", { operationId: opId, retryable: false });
16829
17464
  }
16830
- if (moreTasks && (!nextTaskId || !SAFE_ID_RE8.test(nextTaskId))) {
17465
+ if (moreTasks && (!nextTaskId || !SAFE_ID_RE9.test(nextTaskId))) {
16831
17466
  return failure("task-complete", "nextTaskId required when moreTasks=true", { operationId: opId, retryable: false });
16832
17467
  }
16833
17468
  if (!moreTasks && !/^[0-9a-f]{40}$/.test(expectedHead)) {
@@ -16870,6 +17505,14 @@ function createTaskCompleteTool(deps) {
16870
17505
  return failure("task-complete", `plan still has incomplete tasks: ${remainingTasks.map((task) => task.id).join(", ")}`, { operationId: opId, retryable: false });
16871
17506
  }
16872
17507
  try {
17508
+ const resolved = await resolveWorkflowWorktree(deps.repoRoot, workflowId);
17509
+ if (!resolved.ok) {
17510
+ return failure("task-complete", `workflow worktree resolution failed: ${resolved.kind}`, {
17511
+ operationId: opId,
17512
+ retryable: false,
17513
+ details: resolved
17514
+ });
17515
+ }
16873
17516
  const gateEvidence = moreTasks ? null : await loadTrustedGateEvidence({
16874
17517
  repoRoot: deps.repoRoot,
16875
17518
  repoSlug: deps.repoSlug,
@@ -16881,7 +17524,7 @@ function createTaskCompleteTool(deps) {
16881
17524
  });
16882
17525
  const commonDir = await resolveGitCommonDir(deps.repoRoot);
16883
17526
  const finalPackage = moreTasks ? null : await loadOrBuildFinalPackage({
16884
- repoRoot: deps.repoRoot,
17527
+ repoRoot: resolved.worktreePath,
16885
17528
  commonDir,
16886
17529
  workflowId,
16887
17530
  runState,
@@ -16891,7 +17534,7 @@ function createTaskCompleteTool(deps) {
16891
17534
  ciHash: gateEvidence?.ciHash ?? "",
16892
17535
  gateTaskId: gateEvidence?.taskId ?? ""
16893
17536
  });
16894
- const completeDir = join15(opencodeShipStateDir(commonDir), "runs", workflowId, "tasks", taskId, "complete");
17537
+ const completeDir = join18(opencodeShipStateDir(commonDir), "runs", workflowId, "tasks", taskId, "complete");
16895
17538
  await mkdir12(completeDir, { recursive: true });
16896
17539
  const record2 = {
16897
17540
  workflowId,
@@ -16901,9 +17544,9 @@ function createTaskCompleteTool(deps) {
16901
17544
  finalReview: finalPackage,
16902
17545
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
16903
17546
  };
16904
- const completePath = join15(completeDir, "complete.json");
16905
- if (existsSync11(completePath)) {
16906
- const existing = JSON.parse(await readFile12(completePath, "utf8"));
17547
+ const completePath = join18(completeDir, "complete.json");
17548
+ if (existsSync13(completePath)) {
17549
+ const existing = JSON.parse(await readFile14(completePath, "utf8"));
16907
17550
  if (existing.workflowId !== workflowId || existing.taskId !== taskId || existing.moreTasks !== moreTasks || (existing.nextTaskId ?? null) !== (nextTaskId ?? null) || (existing.finalReview?.packageHash ?? null) !== (finalPackage?.packageHash ?? null)) {
16908
17551
  return failure("task-complete", "immutable task completion conflicts with retry", { operationId: opId, retryable: false });
16909
17552
  }
@@ -16930,7 +17573,7 @@ function createTaskCompleteTool(deps) {
16930
17573
  if (!moreTasks && deps.opencodeClient) {
16931
17574
  const models = resolveModelRoles(deps.config?.workflow, { strict: true });
16932
17575
  const packageHash = finalPackage.packageHash;
16933
- const packagePath = join15(opencodeShipStateDir(commonDir), "runs", workflowId, "final-review", "package.json");
17576
+ const packagePath = join18(opencodeShipStateDir(commonDir), "runs", workflowId, "final-review", "package.json");
16934
17577
  const promptText = [
16935
17578
  `Review workflow ${workflowId} package ${packageHash} at HEAD ${finalPackage.headSha} against merge base ${finalPackage.mergeBaseSha}.`,
16936
17579
  `Canonical package path: ${packagePath}`,
@@ -16939,7 +17582,7 @@ ${JSON.stringify(finalPackage, null, 2)}`
16939
17582
  ].join("\n\n");
16940
17583
  const [standards, spec] = await Promise.all([
16941
17584
  dispatchWorker({
16942
- repoRoot: deps.repoRoot,
17585
+ repoRoot: resolved.worktreePath,
16943
17586
  workflowId,
16944
17587
  role: ROLES.FINAL_STANDARDS,
16945
17588
  keyInput: { packageHash },
@@ -16951,7 +17594,7 @@ ${JSON.stringify(finalPackage, null, 2)}`
16951
17594
  model: models.finalReviewer
16952
17595
  }),
16953
17596
  dispatchWorker({
16954
- repoRoot: deps.repoRoot,
17597
+ repoRoot: resolved.worktreePath,
16955
17598
  workflowId,
16956
17599
  role: ROLES.FINAL_SPEC,
16957
17600
  keyInput: { packageHash },
@@ -16989,7 +17632,7 @@ async function loadTrustedGateEvidence({ repoRoot, repoSlug, driver, adapter, wo
16989
17632
  const manifests = (await listManifests(repoRoot)).filter((manifest2) => manifest2.issueNumber === issueNumber);
16990
17633
  if (manifests.length !== 1) throw new Error(`expected one delivery manifest for issue #${issueNumber}, found ${manifests.length}`);
16991
17634
  const manifest = manifests[0];
16992
- if (manifest.schemaVersion < 2 || manifest.workflowId !== workflowId) {
17635
+ if (manifest.schemaVersion !== 2 || manifest.workflowId !== workflowId) {
16993
17636
  throw new Error("delivery manifest is not linked to the current workflow");
16994
17637
  }
16995
17638
  if (manifest.lastVerifierSha !== expectedHead || !/^[0-9a-f]{64}$/.test(manifest.lastVerificationHash ?? "")) {
@@ -17025,10 +17668,10 @@ async function loadTrustedGateEvidence({ repoRoot, repoSlug, driver, adapter, wo
17025
17668
  return { verificationHash: verification.receiptHash, ciHash: ci.receiptHash, taskId: manifest.taskId };
17026
17669
  }
17027
17670
  async function loadOrBuildFinalPackage({ repoRoot, commonDir, workflowId, runState, planRecord, expectedHead, verificationHash, ciHash, gateTaskId }) {
17028
- const reviewDir = join15(opencodeShipStateDir(commonDir), "runs", workflowId, "final-review");
17029
- const packagePath = join15(reviewDir, "package.json");
17030
- if (existsSync11(packagePath)) {
17031
- const existing = JSON.parse(await readFile12(packagePath, "utf8"));
17671
+ const reviewDir = join18(opencodeShipStateDir(commonDir), "runs", workflowId, "final-review");
17672
+ const packagePath = join18(reviewDir, "package.json");
17673
+ if (existsSync13(packagePath)) {
17674
+ const existing = JSON.parse(await readFile14(packagePath, "utf8"));
17032
17675
  if (existing.headSha !== expectedHead || existing.verificationHash !== verificationHash || existing.ciHash !== ciHash || existing.gateTaskId !== gateTaskId) {
17033
17676
  throw new Error("final review package already exists with different gate evidence");
17034
17677
  }
@@ -17045,10 +17688,10 @@ async function loadOrBuildFinalPackage({ repoRoot, commonDir, workflowId, runSta
17045
17688
  throw new Error(`approved base ${mergeBaseSha.slice(0, 8)} is not the merge base of final HEAD`);
17046
17689
  }
17047
17690
  const revision = String(runState.revision).padStart(6, "0");
17048
- const approvalRaw = await readFile12(join15(opencodeShipStateDir(commonDir), "plans", workflowId, "revisions", revision, "approval.json"), "utf8");
17691
+ const approvalRaw = await readFile14(join18(opencodeShipStateDir(commonDir), "plans", workflowId, "revisions", revision, "approval.json"), "utf8");
17049
17692
  const tasks = [];
17050
17693
  for (const completedTaskId of runState.completedTasks ?? []) {
17051
- const commitRaw = await readFile12(join15(opencodeShipStateDir(commonDir), "runs", workflowId, "tasks", completedTaskId, "commit", "commit.json"), "utf8");
17694
+ const commitRaw = await readFile14(join18(opencodeShipStateDir(commonDir), "runs", workflowId, "tasks", completedTaskId, "commit", "commit.json"), "utf8");
17052
17695
  const commit = JSON.parse(commitRaw);
17053
17696
  tasks.push({
17054
17697
  taskId: completedTaskId,
@@ -17075,23 +17718,23 @@ async function loadOrBuildFinalPackage({ repoRoot, commonDir, workflowId, runSta
17075
17718
  }
17076
17719
  function git(cwd, args) {
17077
17720
  return new Promise((resolveP, rejectP) => {
17078
- execFile3("git", ["-C", cwd, ...args], { cwd, shell: false }, (err, stdout, stderr) => {
17721
+ execFile4("git", ["-C", cwd, ...args], { cwd, shell: false }, (err, stdout, stderr) => {
17079
17722
  if (err) return rejectP(new Error(`git ${args[0]} failed: ${stderr || err.message}`));
17080
17723
  resolveP(String(stdout));
17081
17724
  });
17082
17725
  });
17083
17726
  }
17084
17727
  function sha2564(value) {
17085
- return createHash12("sha256").update(value, "utf8").digest("hex");
17728
+ return createHash13("sha256").update(value, "utf8").digest("hex");
17086
17729
  }
17087
17730
 
17088
17731
  // src/tools/ship-task-report.js
17089
- import { mkdir as mkdir13, readFile as readFile13 } from "node:fs/promises";
17090
- import { existsSync as existsSync12 } from "node:fs";
17091
- import { createHash as createHash13 } from "node:crypto";
17092
- import { join as join16 } from "node:path";
17732
+ import { mkdir as mkdir13, readFile as readFile15 } from "node:fs/promises";
17733
+ import { existsSync as existsSync14 } from "node:fs";
17734
+ import { createHash as createHash14 } from "node:crypto";
17735
+ import { join as join19 } from "node:path";
17093
17736
  init_durable_store();
17094
- var SAFE_ID_RE9 = /^[A-Za-z0-9._-]{1,128}$/;
17737
+ var SAFE_ID_RE10 = /^[A-Za-z0-9._-]{1,128}$/;
17095
17738
  function createTaskReportTool(deps) {
17096
17739
  return async function taskReport(input) {
17097
17740
  const opId = input.operationId ?? `task-report-${Date.now().toString(36)}`;
@@ -17099,10 +17742,10 @@ function createTaskReportTool(deps) {
17099
17742
  const taskId = String(input.taskId ?? "");
17100
17743
  const round = Number(input.round ?? 1);
17101
17744
  const summary = String(input.summary ?? "");
17102
- if (!workflowId || !SAFE_ID_RE9.test(workflowId)) {
17745
+ if (!workflowId || !SAFE_ID_RE10.test(workflowId)) {
17103
17746
  return failure("task-report", "workflowId required (safe id)", { operationId: opId, retryable: false });
17104
17747
  }
17105
- if (!taskId || !SAFE_ID_RE9.test(taskId)) {
17748
+ if (!taskId || !SAFE_ID_RE10.test(taskId)) {
17106
17749
  return failure("task-report", "taskId required (safe id)", { operationId: opId, retryable: false });
17107
17750
  }
17108
17751
  if (!Number.isInteger(round) || round <= 0) {
@@ -17137,8 +17780,16 @@ function createTaskReportTool(deps) {
17137
17780
  return failure("task-report", `another task is already active (${runState.activeTask})`, { operationId: opId, retryable: false });
17138
17781
  }
17139
17782
  try {
17783
+ const resolved = await resolveWorkflowWorktree(deps.repoRoot, workflowId);
17784
+ if (!resolved.ok) {
17785
+ return failure("task-report", `workflow worktree resolution failed: ${resolved.kind}`, {
17786
+ operationId: opId,
17787
+ retryable: false,
17788
+ details: resolved
17789
+ });
17790
+ }
17140
17791
  const commonDir = await resolveGitCommonDir(deps.repoRoot);
17141
- const reportDir = join16(opencodeShipStateDir(commonDir), "runs", workflowId, "tasks", taskId, "rounds", `${String(round).padStart(4, "0")}`);
17792
+ const reportDir = join19(opencodeShipStateDir(commonDir), "runs", workflowId, "tasks", taskId, "rounds", `${String(round).padStart(4, "0")}`);
17142
17793
  await mkdir13(reportDir, { recursive: true });
17143
17794
  const record2 = {
17144
17795
  workflowId,
@@ -17150,12 +17801,12 @@ function createTaskReportTool(deps) {
17150
17801
  tests: Array.isArray(input.tests) ? input.tests : [],
17151
17802
  submittedAt: (/* @__PURE__ */ new Date()).toISOString()
17152
17803
  };
17153
- const reportPath = join16(reportDir, "implementer-report.json");
17804
+ const reportPath = join19(reportDir, "implementer-report.json");
17154
17805
  let state = runState;
17155
17806
  let event = runState.events.at(-1) ?? { sequence: 0 };
17156
17807
  let persistedRecord = record2;
17157
- if (existsSync12(reportPath)) {
17158
- persistedRecord = JSON.parse(await readFile13(reportPath, "utf8"));
17808
+ if (existsSync14(reportPath)) {
17809
+ persistedRecord = JSON.parse(await readFile15(reportPath, "utf8"));
17159
17810
  if (!sameReport(persistedRecord, record2)) {
17160
17811
  return failure("task-report", "immutable report already exists with different content", { operationId: opId, retryable: false });
17161
17812
  }
@@ -17186,7 +17837,7 @@ function createTaskReportTool(deps) {
17186
17837
  if (deps.opencodeClient) {
17187
17838
  try {
17188
17839
  reviewerDispatch = await dispatchWorker({
17189
- repoRoot: deps.repoRoot,
17840
+ repoRoot: resolved.worktreePath,
17190
17841
  workflowId,
17191
17842
  role: ROLES.TASK_REVIEWER,
17192
17843
  keyInput: { taskId, round },
@@ -17230,7 +17881,7 @@ function reportHash(record2) {
17230
17881
  const sorted = Object.keys(record2).sort();
17231
17882
  const ordered = {};
17232
17883
  for (const k of sorted) ordered[k] = record2[k];
17233
- return createHash13("sha256").update(JSON.stringify(ordered), "utf8").digest("hex");
17884
+ return createHash14("sha256").update(JSON.stringify(ordered), "utf8").digest("hex");
17234
17885
  }
17235
17886
  function sameReport(left, right) {
17236
17887
  for (const field of ["workflowId", "taskId", "round", "builderSessionID", "summary"]) {
@@ -17240,12 +17891,12 @@ function sameReport(left, right) {
17240
17891
  }
17241
17892
 
17242
17893
  // src/tools/ship-task-review.js
17243
- import { createHash as createHash14 } from "node:crypto";
17244
- import { mkdir as mkdir14, readFile as readFile14 } from "node:fs/promises";
17245
- import { existsSync as existsSync13 } from "node:fs";
17246
- import { join as join17 } from "node:path";
17894
+ import { createHash as createHash15 } from "node:crypto";
17895
+ import { mkdir as mkdir14, readFile as readFile16 } from "node:fs/promises";
17896
+ import { existsSync as existsSync15 } from "node:fs";
17897
+ import { join as join20 } from "node:path";
17247
17898
  init_durable_store();
17248
- var SAFE_ID_RE10 = /^[A-Za-z0-9._-]{1,128}$/;
17899
+ var SAFE_ID_RE11 = /^[A-Za-z0-9._-]{1,128}$/;
17249
17900
  var VERDICT_VALUES = /* @__PURE__ */ new Set(["pass", "fail", "none"]);
17250
17901
  function createTaskReviewTool(deps) {
17251
17902
  return async function taskReview(input) {
@@ -17255,10 +17906,10 @@ function createTaskReviewTool(deps) {
17255
17906
  const round = Number(input.round ?? 1);
17256
17907
  const spec = input.spec;
17257
17908
  const quality = input.quality;
17258
- if (!workflowId || !SAFE_ID_RE10.test(workflowId)) {
17909
+ if (!workflowId || !SAFE_ID_RE11.test(workflowId)) {
17259
17910
  return failure("task-review", "workflowId required (safe id)", { operationId: opId, retryable: false });
17260
17911
  }
17261
- if (!taskId || !SAFE_ID_RE10.test(taskId)) {
17912
+ if (!taskId || !SAFE_ID_RE11.test(taskId)) {
17262
17913
  return failure("task-review", "taskId required (safe id)", { operationId: opId, retryable: false });
17263
17914
  }
17264
17915
  if (!Number.isInteger(round) || round <= 0) {
@@ -17314,7 +17965,7 @@ function createTaskReviewTool(deps) {
17314
17965
  const specPass = String(spec.verdict) === "pass";
17315
17966
  const qualityPass = String(quality.verdict) === "pass";
17316
17967
  const commonDir = await resolveGitCommonDir(deps.repoRoot);
17317
- const reviewDir = join17(opencodeShipStateDir(commonDir), "runs", workflowId, "tasks", taskId, "rounds", `${String(round).padStart(4, "0")}`);
17968
+ const reviewDir = join20(opencodeShipStateDir(commonDir), "runs", workflowId, "tasks", taskId, "rounds", `${String(round).padStart(4, "0")}`);
17318
17969
  await mkdir14(reviewDir, { recursive: true });
17319
17970
  const record2 = {
17320
17971
  workflowId,
@@ -17326,9 +17977,9 @@ function createTaskReviewTool(deps) {
17326
17977
  state: specPass && qualityPass ? "commit-pending" : "fix-pending",
17327
17978
  reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
17328
17979
  };
17329
- const reviewPath = join17(reviewDir, "review.json");
17330
- if (existsSync13(reviewPath)) {
17331
- const existing = JSON.parse(await readFile14(reviewPath, "utf8"));
17980
+ const reviewPath = join20(reviewDir, "review.json");
17981
+ if (existsSync15(reviewPath)) {
17982
+ const existing = JSON.parse(await readFile16(reviewPath, "utf8"));
17332
17983
  if (existing.workflowId !== workflowId || existing.taskId !== taskId || existing.round !== round || existing.reviewerSessionID !== auth.sessionID || JSON.stringify(existing.spec) !== JSON.stringify(spec) || JSON.stringify(existing.quality) !== JSON.stringify(quality)) {
17333
17984
  return failure("task-review", "immutable task review conflicts with retry", { operationId: opId, retryable: false });
17334
17985
  }
@@ -17363,15 +18014,15 @@ function verdictHash(record2) {
17363
18014
  const sorted = Object.keys(record2).sort();
17364
18015
  const ordered = {};
17365
18016
  for (const k of sorted) ordered[k] = record2[k];
17366
- return createHash14("sha256").update(JSON.stringify(ordered), "utf8").digest("hex");
18017
+ return createHash15("sha256").update(JSON.stringify(ordered), "utf8").digest("hex");
17367
18018
  }
17368
18019
 
17369
18020
  // src/tools/ship-final-review.js
17370
- import { mkdir as mkdir15, readFile as readFile15 } from "node:fs/promises";
17371
- import { existsSync as existsSync14 } from "node:fs";
17372
- import { join as join18 } from "node:path";
18021
+ import { mkdir as mkdir15, readFile as readFile17 } from "node:fs/promises";
18022
+ import { existsSync as existsSync16 } from "node:fs";
18023
+ import { join as join21 } from "node:path";
17373
18024
  init_durable_store();
17374
- var SAFE_ID_RE11 = /^[A-Za-z0-9._-]{1,128}$/;
18025
+ var SAFE_ID_RE12 = /^[A-Za-z0-9._-]{1,128}$/;
17375
18026
  var AXES = /* @__PURE__ */ new Set(["standards", "spec"]);
17376
18027
  var VERDICTS = /* @__PURE__ */ new Set(["pass", "fail", "blocked"]);
17377
18028
  var ROLE_FOR_AXIS = {
@@ -17388,7 +18039,7 @@ function createFinalReviewTool(deps) {
17388
18039
  const mergeBaseSha = String(input.mergeBaseSha ?? "");
17389
18040
  const packageHash = String(input.packageHash ?? "");
17390
18041
  const findings = Array.isArray(input.findings) ? input.findings : [];
17391
- if (!workflowId || !SAFE_ID_RE11.test(workflowId)) {
18042
+ if (!workflowId || !SAFE_ID_RE12.test(workflowId)) {
17392
18043
  return failure("final-review", "workflowId required (safe id)", { operationId: opId, retryable: false });
17393
18044
  }
17394
18045
  if (!AXES.has(axis)) {
@@ -17435,18 +18086,18 @@ function createFinalReviewTool(deps) {
17435
18086
  }
17436
18087
  try {
17437
18088
  const commonDir = await resolveGitCommonDir(deps.repoRoot);
17438
- const packagePath = join18(opencodeShipStateDir(commonDir), "runs", workflowId, "final-review", "package.json");
17439
- if (!existsSync14(packagePath)) {
18089
+ const packagePath = join21(opencodeShipStateDir(commonDir), "runs", workflowId, "final-review", "package.json");
18090
+ if (!existsSync16(packagePath)) {
17440
18091
  return failure("final-review", "canonical final review package is missing", { operationId: opId, retryable: false });
17441
18092
  }
17442
- const finalPackage = JSON.parse(await readFile15(packagePath, "utf8"));
18093
+ const finalPackage = JSON.parse(await readFile17(packagePath, "utf8"));
17443
18094
  if (hashFinalReviewPackage(finalPackage) !== finalPackage.packageHash) {
17444
18095
  return failure("final-review", "canonical final review package hash is invalid", { operationId: opId, retryable: false });
17445
18096
  }
17446
18097
  if (finalPackage.packageHash !== packageHash || finalPackage.headSha !== headSha || finalPackage.mergeBaseSha !== mergeBaseSha) {
17447
18098
  return failure("final-review", "review input does not match the canonical final review package", { operationId: opId, retryable: false });
17448
18099
  }
17449
- const reviewDir = join18(opencodeShipStateDir(commonDir), "runs", workflowId, "final-review", axis);
18100
+ const reviewDir = join21(opencodeShipStateDir(commonDir), "runs", workflowId, "final-review", axis);
17450
18101
  await mkdir15(reviewDir, { recursive: true });
17451
18102
  let record2 = {
17452
18103
  workflowId,
@@ -17460,9 +18111,9 @@ function createFinalReviewTool(deps) {
17460
18111
  findings,
17461
18112
  reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
17462
18113
  };
17463
- const reviewPath = join18(reviewDir, "review.json");
17464
- if (existsSync14(reviewPath)) {
17465
- record2 = JSON.parse(await readFile15(reviewPath, "utf8"));
18114
+ const reviewPath = join21(reviewDir, "review.json");
18115
+ if (existsSync16(reviewPath)) {
18116
+ record2 = JSON.parse(await readFile17(reviewPath, "utf8"));
17466
18117
  if (record2.axis !== axis || record2.verdict !== verdict || record2.headSha !== headSha || record2.mergeBaseSha !== mergeBaseSha || record2.packageHash !== packageHash || record2.reviewerSessionID !== auth.sessionID || hashAxisRecord(
17467
18118
  /** @type {any} */
17468
18119
  record2
@@ -17523,8 +18174,8 @@ function createFinalReviewTool(deps) {
17523
18174
 
17524
18175
  // src/workflow/resume.js
17525
18176
  import { readdir as readdir6, mkdir as mkdir16 } from "node:fs/promises";
17526
- import { existsSync as existsSync15 } from "node:fs";
17527
- import { join as join19 } from "node:path";
18177
+ import { existsSync as existsSync17 } from "node:fs";
18178
+ import { join as join22 } from "node:path";
17528
18179
  init_durable_store();
17529
18180
  function parseTrailer(text, key) {
17530
18181
  if (typeof text !== "string") return null;
@@ -17599,12 +18250,12 @@ async function resumeRun(repoRoot, workflowId) {
17599
18250
  }
17600
18251
 
17601
18252
  // src/tools/ship-resume.js
17602
- var SAFE_ID_RE12 = /^[A-Za-z0-9._-]{1,128}$/;
18253
+ var SAFE_ID_RE13 = /^[A-Za-z0-9._-]{1,128}$/;
17603
18254
  function createResumeTool(deps) {
17604
18255
  return async function resume(input) {
17605
18256
  const opId = input.operationId ?? `resume-${Date.now().toString(36)}`;
17606
18257
  const workflowId = String(input.workflowId ?? "");
17607
- if (!workflowId || !SAFE_ID_RE12.test(workflowId)) {
18258
+ if (!workflowId || !SAFE_ID_RE13.test(workflowId)) {
17608
18259
  return failure("resume", "workflowId required (safe id)", { operationId: opId, retryable: false });
17609
18260
  }
17610
18261
  const ctx = input.ctx ?? deps.ctx ?? null;
@@ -17631,9 +18282,9 @@ function createResumeTool(deps) {
17631
18282
  }
17632
18283
 
17633
18284
  // src/tools/ship-status.js
17634
- import { readFile as readFile16, readdir as readdir7 } from "node:fs/promises";
17635
- import { existsSync as existsSync16 } from "node:fs";
17636
- import { join as join20 } from "node:path";
18285
+ import { readFile as readFile18, readdir as readdir7 } from "node:fs/promises";
18286
+ import { existsSync as existsSync18 } from "node:fs";
18287
+ import { join as join23 } from "node:path";
17637
18288
  function createStatusTool(deps) {
17638
18289
  return async function status(input) {
17639
18290
  const opId = input.operationId ?? `status-${Date.now().toString(36)}`;
@@ -17641,21 +18292,21 @@ function createStatusTool(deps) {
17641
18292
  if (!workflowId) return failure("status", "workflowId required", { operationId: opId, retryable: false });
17642
18293
  try {
17643
18294
  const commonDir = await resolveGitCommonDir(deps.repoRoot);
17644
- const planRoot = join20(opencodeShipStateDir(commonDir), "plans", workflowId);
17645
- const runRoot = join20(opencodeShipStateDir(commonDir), "runs", workflowId);
17646
- const indexPath = join20(planRoot, "index.json");
17647
- if (!existsSync16(indexPath)) return failure("status", "no workflow record", { operationId: opId, retryable: false });
17648
- const index = JSON.parse(await readFile16(indexPath, "utf8"));
18295
+ const planRoot = join23(opencodeShipStateDir(commonDir), "plans", workflowId);
18296
+ const runRoot = join23(opencodeShipStateDir(commonDir), "runs", workflowId);
18297
+ const indexPath = join23(planRoot, "index.json");
18298
+ if (!existsSync18(indexPath)) return failure("status", "no workflow record", { operationId: opId, retryable: false });
18299
+ const index = JSON.parse(await readFile18(indexPath, "utf8"));
17649
18300
  let run = null;
17650
- const runPath = join20(runRoot, "run.json");
17651
- if (existsSync16(runPath)) run = JSON.parse(await readFile16(runPath, "utf8"));
18301
+ const runPath = join23(runRoot, "run.json");
18302
+ if (existsSync18(runPath)) run = JSON.parse(await readFile18(runPath, "utf8"));
17652
18303
  let lastEvent = null;
17653
- const eventsDir = join20(runRoot, "events");
17654
- if (existsSync16(eventsDir)) {
18304
+ const eventsDir = join23(runRoot, "events");
18305
+ if (existsSync18(eventsDir)) {
17655
18306
  const events = await readdir7(eventsDir);
17656
18307
  const sorted = events.filter((n) => n.endsWith(".json")).sort();
17657
18308
  if (sorted.length > 0) {
17658
- lastEvent = JSON.parse(await readFile16(join20(eventsDir, sorted[sorted.length - 1]), "utf8"));
18309
+ lastEvent = JSON.parse(await readFile18(join23(eventsDir, sorted[sorted.length - 1]), "utf8"));
17659
18310
  }
17660
18311
  }
17661
18312
  return success2("status", { workflowId, index, run, lastEvent }, { operationId: opId });
@@ -17667,14 +18318,14 @@ function createStatusTool(deps) {
17667
18318
 
17668
18319
  // src/skills/registry.js
17669
18320
  import { spawn as spawn7 } from "node:child_process";
17670
- import { readFile as readFile17, writeFile as writeFile8, mkdir as mkdir17 } from "node:fs/promises";
17671
- import { resolve as resolve11, dirname as dirname7 } from "node:path";
17672
- import { createHash as createHash15 } from "node:crypto";
18321
+ import { readFile as readFile19, writeFile as writeFile8, mkdir as mkdir17 } from "node:fs/promises";
18322
+ import { resolve as resolve12, dirname as dirname8 } from "node:path";
18323
+ import { createHash as createHash16 } from "node:crypto";
17673
18324
 
17674
18325
  // src/tools/skill-discovery.js
17675
18326
  import { spawn as spawn6 } from "node:child_process";
17676
- import { existsSync as existsSync17, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from "node:fs";
17677
- import { dirname as dirname6, join as join21, normalize, resolve as resolve10, sep } from "node:path";
18327
+ import { existsSync as existsSync19, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from "node:fs";
18328
+ import { dirname as dirname7, join as join24, normalize, resolve as resolve11, sep as sep2 } from "node:path";
17678
18329
  var DEFAULT_TRUSTED_OWNERS = Object.freeze([
17679
18330
  "vercel-labs",
17680
18331
  "anthropics",
@@ -17742,9 +18393,9 @@ async function listSkills({ repoRoot, query, npmBin = "npx" }) {
17742
18393
  }
17743
18394
 
17744
18395
  // src/skills/policy.js
17745
- import { readFile as readFile18, writeFile as writeFile9 } from "node:fs/promises";
17746
- import { existsSync as existsSync18 } from "node:fs";
17747
- import { resolve as resolve12 } from "node:path";
18396
+ import { readFile as readFile20, writeFile as writeFile9 } from "node:fs/promises";
18397
+ import { existsSync as existsSync20 } from "node:fs";
18398
+ import { resolve as resolve13 } from "node:path";
17748
18399
  var DEFAULT_TRUSTED_OWNERS2 = Object.freeze([
17749
18400
  "vercel-labs",
17750
18401
  "anthropics",
@@ -17756,7 +18407,7 @@ var DEFAULT_MIN_INSTALLS = 1e3;
17756
18407
  var MAX_TRUSTED_PER_RUN = 5;
17757
18408
  var POLICY_PATH = ".opencode/ship.skills.policy.json";
17758
18409
  function policyPath(repoRoot) {
17759
- return resolve12(repoRoot, POLICY_PATH);
18410
+ return resolve13(repoRoot, POLICY_PATH);
17760
18411
  }
17761
18412
  function defaultPolicy() {
17762
18413
  return {
@@ -17768,9 +18419,9 @@ function defaultPolicy() {
17768
18419
  }
17769
18420
  async function readPolicy(repoRoot) {
17770
18421
  const path = policyPath(repoRoot);
17771
- if (!existsSync18(path)) return defaultPolicy();
18422
+ if (!existsSync20(path)) return defaultPolicy();
17772
18423
  try {
17773
- const raw = await readFile18(path, "utf8");
18424
+ const raw = await readFile20(path, "utf8");
17774
18425
  const parsed = JSON.parse(raw);
17775
18426
  return mergePolicy(defaultPolicy(), parsed);
17776
18427
  } catch {
@@ -17857,33 +18508,33 @@ function createSkillDiscoverTool(deps) {
17857
18508
  }
17858
18509
 
17859
18510
  // src/tools/ship-skill-install.js
17860
- import { readFile as readFile20, writeFile as writeFile11, mkdir as mkdir19, rm, rename as rename5, stat as stat2 } from "node:fs/promises";
17861
- import { existsSync as existsSync21 } from "node:fs";
17862
- import { resolve as resolve15, join as join23, dirname as dirname10, sep as sep3, isAbsolute as isAbsolute3 } from "node:path";
17863
- import { createHash as createHash17 } from "node:crypto";
18511
+ import { readFile as readFile22, writeFile as writeFile11, mkdir as mkdir19, rm, rename as rename5, stat as stat2 } from "node:fs/promises";
18512
+ import { existsSync as existsSync22 } from "node:fs";
18513
+ import { resolve as resolve15, join as join25, dirname as dirname10, sep as sep3, isAbsolute as isAbsolute3 } from "node:path";
18514
+ import { createHash as createHash18 } from "node:crypto";
17864
18515
  import { execFile as execFile5 } from "node:child_process";
17865
18516
  import { mkdtemp } from "node:fs/promises";
17866
18517
  import { tmpdir } from "node:os";
17867
18518
  import { randomBytes as randomBytes3 } from "node:crypto";
17868
18519
 
17869
18520
  // src/skills/inventory.js
17870
- import { readFile as readFile19, writeFile as writeFile10, mkdir as mkdir18, rename as rename4 } from "node:fs/promises";
17871
- import { existsSync as existsSync19 } from "node:fs";
17872
- import { resolve as resolve13, dirname as dirname8, isAbsolute, posix as posix2 } from "node:path";
17873
- import { createHash as createHash16 } from "node:crypto";
18521
+ import { readFile as readFile21, writeFile as writeFile10, mkdir as mkdir18, rename as rename4 } from "node:fs/promises";
18522
+ import { existsSync as existsSync21 } from "node:fs";
18523
+ import { resolve as resolve14, dirname as dirname9, isAbsolute as isAbsolute2, posix as posix2 } from "node:path";
18524
+ import { createHash as createHash17 } from "node:crypto";
17874
18525
  var INVENTORY_PATH = ".opencode/ship.skills.lock.json";
17875
18526
  var INVENTORY_SCHEMA = 2;
17876
18527
  function inventoryPath(repoRoot) {
17877
- return resolve13(repoRoot, INVENTORY_PATH);
18528
+ return resolve14(repoRoot, INVENTORY_PATH);
17878
18529
  }
17879
18530
  async function readInventory(repoRoot) {
17880
18531
  const path = inventoryPath(repoRoot);
17881
- if (!existsSync19(path)) {
18532
+ if (!existsSync21(path)) {
17882
18533
  return { schemaVersion: INVENTORY_SCHEMA, events: [] };
17883
18534
  }
17884
18535
  let raw;
17885
18536
  try {
17886
- raw = await readFile19(path, "utf8");
18537
+ raw = await readFile21(path, "utf8");
17887
18538
  } catch (err) {
17888
18539
  return { schemaVersion: INVENTORY_SCHEMA, events: [], parseError: `read failed: ${err?.message ?? err}` };
17889
18540
  }
@@ -17910,7 +18561,7 @@ async function readInventory(repoRoot) {
17910
18561
  }
17911
18562
  async function writeInventory(repoRoot, inventory) {
17912
18563
  const path = inventoryPath(repoRoot);
17913
- await mkdir18(dirname8(path), { recursive: true });
18564
+ await mkdir18(dirname9(path), { recursive: true });
17914
18565
  const tmp = `${path}.${Date.now().toString(36)}.tmp`;
17915
18566
  await writeFile10(tmp, JSON.stringify({ schemaVersion: INVENTORY_SCHEMA, events: inventory.events }, null, 2) + "\n", "utf8");
17916
18567
  await rename4(tmp, path);
@@ -17930,7 +18581,7 @@ function canonicalize3(value) {
17930
18581
  return JSON.stringify(sort(value));
17931
18582
  }
17932
18583
  function hashEvent(event) {
17933
- return createHash16("sha256").update(canonicalize3(event), "utf8").digest("hex");
18584
+ return createHash17("sha256").update(canonicalize3(event), "utf8").digest("hex");
17934
18585
  }
17935
18586
  async function appendEvent2(repoRoot, eventInput) {
17936
18587
  const inventory = await readInventory(repoRoot);
@@ -17943,7 +18594,7 @@ async function appendEvent2(repoRoot, eventInput) {
17943
18594
  }
17944
18595
  const previousHash = inventory.events.length > 0 ? inventory.events[inventory.events.length - 1].hash : "0".repeat(64);
17945
18596
  const sequence = inventory.events.length + 1;
17946
- if (eventInput.destination && isAbsolute(eventInput.destination)) {
18597
+ if (eventInput.destination && isAbsolute2(eventInput.destination)) {
17947
18598
  throw new Error(`inventory refuses absolute destination: ${eventInput.destination}`);
17948
18599
  }
17949
18600
  const base = {
@@ -18043,146 +18694,8 @@ async function findActiveInstall(repoRoot, skillName) {
18043
18694
  return { ok: true, install: null, uninstallHash: null };
18044
18695
  }
18045
18696
 
18046
- // src/skills/worktree.js
18047
- import { execFile as execFile4 } from "node:child_process";
18048
- import { promises as fs, existsSync as existsSync20 } from "node:fs";
18049
- import { resolve as resolve14, dirname as dirname9, sep as sep2, isAbsolute as isAbsolute2, join as join22 } from "node:path";
18050
- function listRegisteredWorktrees(mainRepo) {
18051
- return new Promise((resolveP, rejectP) => {
18052
- execFile4(
18053
- "git",
18054
- ["-C", mainRepo, "worktree", "list", "--porcelain", "-z"],
18055
- { shell: false, maxBuffer: 1024 * 1024 },
18056
- (err, stdout) => {
18057
- if (err) return rejectP(err);
18058
- const records = parsePorcelain(stdout);
18059
- const mainRecord = records.shift();
18060
- const mainPath = mainRecord?.worktree ? resolve14(mainRecord.worktree) : null;
18061
- const linked = [];
18062
- for (const r of records) {
18063
- if (!r.worktree) continue;
18064
- const p = resolve14(r.worktree);
18065
- if (mainPath && p === mainPath) continue;
18066
- linked.push({ path: p, branch: r.HEAD ?? null });
18067
- }
18068
- resolveP(linked);
18069
- }
18070
- );
18071
- });
18072
- }
18073
- function parsePorcelain(text) {
18074
- const tokens = text.split("\0");
18075
- const out = [];
18076
- let current = {};
18077
- for (const tok of tokens) {
18078
- if (tok.length === 0) {
18079
- if (Object.keys(current).length > 0) {
18080
- out.push(current);
18081
- current = {};
18082
- }
18083
- continue;
18084
- }
18085
- const idx = tok.indexOf(" ");
18086
- const key = idx === -1 ? tok : tok.slice(0, idx);
18087
- const value = idx === -1 ? "" : tok.slice(idx + 1);
18088
- if (key === "branch") {
18089
- current.HEAD = value.startsWith("refs/heads/") ? value : `refs/heads/${value}`;
18090
- } else {
18091
- current[key] = value;
18092
- }
18093
- }
18094
- if (Object.keys(current).length > 0) out.push(current);
18095
- return out;
18096
- }
18097
- async function validateLinkedWorktree(mainRepo, worktreePath) {
18098
- const main = resolve14(mainRepo);
18099
- if (!existsSync20(main)) {
18100
- return { ok: false, kind: "missing", message: `main repository ${main} does not exist` };
18101
- }
18102
- if (!worktreePath) {
18103
- return { ok: false, kind: "unlinked", message: "worktreePath is required" };
18104
- }
18105
- const wt = resolve14(worktreePath);
18106
- if (!existsSync20(wt)) {
18107
- return { ok: false, kind: "missing", message: `worktree ${wt} does not exist` };
18108
- }
18109
- if (wt === main) {
18110
- return { ok: false, kind: "main", message: "installs into the main worktree are forbidden" };
18111
- }
18112
- let cursor = wt;
18113
- while (cursor !== dirname9(cursor)) {
18114
- const stat3 = await fs.lstat(cursor).catch(() => null);
18115
- if (stat3?.isSymbolicLink()) {
18116
- return {
18117
- ok: false,
18118
- kind: "ancestor-symlink",
18119
- message: `worktree path contains a symlink at ${cursor}`
18120
- };
18121
- }
18122
- cursor = dirname9(cursor);
18123
- }
18124
- const real = await fs.realpath(wt).catch(() => null);
18125
- if (real && real !== wt) {
18126
- return {
18127
- ok: false,
18128
- kind: "symlink",
18129
- message: `worktree ${wt} resolves through a symlink to ${real}`
18130
- };
18131
- }
18132
- const linked = await listRegisteredWorktrees(main);
18133
- const matched = linked.find((entry) => entry.path === wt);
18134
- if (!matched) {
18135
- return {
18136
- ok: false,
18137
- kind: "unlinked",
18138
- message: `worktree ${wt} is not registered (git worktree list)`
18139
- };
18140
- }
18141
- return { ok: true, path: wt, registered: !!matched };
18142
- }
18143
- function validateRelativeInstallPath(destRel) {
18144
- if (typeof destRel !== "string" || destRel.length === 0) {
18145
- return { ok: false, kind: "absolute", message: "destination path required" };
18146
- }
18147
- if (isAbsolute2(destRel)) {
18148
- return { ok: false, kind: "absolute", message: `destination must be relative: ${destRel}` };
18149
- }
18150
- if (destRel.includes("\\")) {
18151
- return { ok: false, kind: "parent-relative", message: `destination must use POSIX separators: ${destRel}` };
18152
- }
18153
- const parts = destRel.split("/");
18154
- for (const p of parts) {
18155
- if (p === "" || p === "." || p === "..") {
18156
- return { ok: false, kind: "parent-relative", message: `destination escapes worktree: ${destRel}` };
18157
- }
18158
- }
18159
- return { ok: true };
18160
- }
18161
- async function validateInstallDestination(worktreeRoot, destRel) {
18162
- const relativeCheck = validateRelativeInstallPath(destRel);
18163
- if (!relativeCheck.ok) return relativeCheck;
18164
- const root = resolve14(worktreeRoot);
18165
- const destination = resolve14(root, ...destRel.split("/"));
18166
- if (destination !== root && !destination.startsWith(root + sep2)) {
18167
- return { ok: false, kind: "escape", message: `destination escapes worktree: ${destRel}` };
18168
- }
18169
- let cursor = root;
18170
- for (const part of destRel.split("/")) {
18171
- cursor = join22(cursor, part);
18172
- const entry = await fs.lstat(cursor).catch(() => null);
18173
- if (!entry) continue;
18174
- if (entry.isSymbolicLink()) {
18175
- return { ok: false, kind: "symlink", message: `destination path contains a symlink at ${cursor}` };
18176
- }
18177
- if (cursor !== destination && !entry.isDirectory()) {
18178
- return { ok: false, kind: "not-directory", message: `destination ancestor is not a directory: ${cursor}` };
18179
- }
18180
- }
18181
- return { ok: true, path: destination };
18182
- }
18183
-
18184
18697
  // src/tools/ship-skill-install.js
18185
- var SAFE_ID_RE13 = /^[A-Za-z0-9._-]{1,128}$/;
18698
+ var SAFE_ID_RE14 = /^[A-Za-z0-9._-]{1,128}$/;
18186
18699
  var SAFE_NAME_RE = /^[A-Za-z0-9._/-]{1,160}$/;
18187
18700
  var SAFE_VERSION_RE = /^[A-Za-z0-9._+-]{1,64}$/;
18188
18701
  var SKILLS_CLI_VERSION = "1.0.4";
@@ -18199,7 +18712,7 @@ function createSkillInstallTool(deps) {
18199
18712
  if (!worktreePath) {
18200
18713
  return failure("skill-install", "worktreePath required (must be the active issue worktree)", { operationId: opId, retryable: false });
18201
18714
  }
18202
- if (!skillName || !SAFE_ID_RE13.test(skillName)) {
18715
+ if (!skillName || !SAFE_ID_RE14.test(skillName)) {
18203
18716
  return failure("skill-install", "skillName required (safe id)", { operationId: opId, retryable: false });
18204
18717
  }
18205
18718
  if (version2 && !SAFE_VERSION_RE.test(version2)) {
@@ -18229,7 +18742,7 @@ function createSkillInstallTool(deps) {
18229
18742
  return failure("skill-install", `destination rejected: ${destinationCheck.message}`, { operationId: opId, retryable: false });
18230
18743
  }
18231
18744
  const destAbs = destinationCheck.path;
18232
- if (existsSync21(destAbs)) {
18745
+ if (existsSync22(destAbs)) {
18233
18746
  return failure("skill-install", "destination already exists; use ship_skill_audit to detect drift", { operationId: opId, retryable: false });
18234
18747
  }
18235
18748
  const managedCatalog = (deps.config?.value?.skills ?? []).map((s) => s?.name).filter(Boolean);
@@ -18249,7 +18762,7 @@ function createSkillInstallTool(deps) {
18249
18762
  if (!decision.ok) {
18250
18763
  return failure("skill-install", `policy forbids install: ${decision.reason}`, { operationId: opId, retryable: false });
18251
18764
  }
18252
- const stage = await mkdtemp(join23(tmpdir(), `ship-skill-stage-${randomBytes3(4).toString("hex")}-`));
18765
+ const stage = await mkdtemp(join25(tmpdir(), `ship-skill-stage-${randomBytes3(4).toString("hex")}-`));
18253
18766
  let installedFiles;
18254
18767
  try {
18255
18768
  const materialise = deps.materialiseFromSkillsCli ?? materialiseFromSkillsCli;
@@ -18300,7 +18813,7 @@ function createSkillInstallTool(deps) {
18300
18813
  sequence: recorded.sequence
18301
18814
  }, { operationId: opId });
18302
18815
  } catch (err) {
18303
- if (existsSync21(destAbs)) {
18816
+ if (existsSync22(destAbs)) {
18304
18817
  await rm(destAbs, { recursive: true, force: true }).catch(() => null);
18305
18818
  }
18306
18819
  return failure("skill-install", String(err?.message ?? err), { operationId: opId, retryable: true });
@@ -18346,16 +18859,16 @@ async function materialiseFromSkillsCli({ packageSpec, skillName, version: versi
18346
18859
  );
18347
18860
  });
18348
18861
  if (!result.ok) return result;
18349
- const stagedDir = join23(stageDir, ".opencode", "skills", skillName);
18350
- if (!existsSync21(stagedDir)) {
18862
+ const stagedDir = join25(stageDir, ".opencode", "skills", skillName);
18863
+ if (!existsSync22(stagedDir)) {
18351
18864
  return {
18352
18865
  ok: false,
18353
18866
  retryable: false,
18354
18867
  message: `skills CLI did not produce ${stagedDir}`
18355
18868
  };
18356
18869
  }
18357
- const skillMd = join23(stagedDir, "SKILL.md");
18358
- if (!existsSync21(skillMd)) {
18870
+ const skillMd = join25(stagedDir, "SKILL.md");
18871
+ if (!existsSync22(skillMd)) {
18359
18872
  return {
18360
18873
  ok: false,
18361
18874
  retryable: false,
@@ -18387,25 +18900,25 @@ async function walk(rootDir, currentDir, out) {
18387
18900
  const { readdir: readdir10 } = await import("node:fs/promises");
18388
18901
  const entries = await readdir10(currentDir, { withFileTypes: true });
18389
18902
  for (const e of entries) {
18390
- const abs = join23(currentDir, e.name);
18903
+ const abs = join25(currentDir, e.name);
18391
18904
  if (e.isDirectory()) {
18392
18905
  if (e.name === ".git") continue;
18393
18906
  await walk(rootDir, abs, out);
18394
18907
  continue;
18395
18908
  }
18396
18909
  if (!e.isFile()) continue;
18397
- const raw = await readFile20(abs);
18910
+ const raw = await readFile22(abs);
18398
18911
  const fileStat = await stat2(abs);
18399
18912
  out.push({
18400
18913
  path: abs.slice(rootDir.length + 1).split(sep3).join("/"),
18401
- sha256: createHash17("sha256").update(raw).digest("hex"),
18914
+ sha256: createHash18("sha256").update(raw).digest("hex"),
18402
18915
  mode: fileStat.mode & 511,
18403
18916
  size: fileStat.size
18404
18917
  });
18405
18918
  }
18406
18919
  }
18407
18920
  function hashBytes(bytes) {
18408
- return createHash17("sha256").update(bytes).digest("hex");
18921
+ return createHash18("sha256").update(bytes).digest("hex");
18409
18922
  }
18410
18923
  function hashesEqual(a, b) {
18411
18924
  if (a.length !== b.length) return false;
@@ -18420,23 +18933,23 @@ async function copyDir(srcDir, destDir) {
18420
18933
  const { readdir: readdir10 } = await import("node:fs/promises");
18421
18934
  const entries = await readdir10(srcDir, { withFileTypes: true });
18422
18935
  for (const e of entries) {
18423
- const src = join23(srcDir, e.name);
18424
- const dest = join23(destDir, e.name);
18936
+ const src = join25(srcDir, e.name);
18937
+ const dest = join25(destDir, e.name);
18425
18938
  if (e.isDirectory()) {
18426
18939
  if (e.name === ".git") continue;
18427
18940
  await copyDir(src, dest);
18428
18941
  } else if (e.isFile()) {
18429
- const raw = await readFile20(src);
18942
+ const raw = await readFile22(src);
18430
18943
  await writeFile11(dest, raw, { mode: 420 });
18431
18944
  }
18432
18945
  }
18433
18946
  }
18434
18947
 
18435
18948
  // src/tools/ship-skill-audit.js
18436
- import { readdir as readdir8, readFile as readFile21 } from "node:fs/promises";
18437
- import { existsSync as existsSync22 } from "node:fs";
18438
- import { resolve as resolve16, join as join24, isAbsolute as isAbsolute4 } from "node:path";
18439
- import { createHash as createHash18 } from "node:crypto";
18949
+ import { readdir as readdir8, readFile as readFile23 } from "node:fs/promises";
18950
+ import { existsSync as existsSync23 } from "node:fs";
18951
+ import { resolve as resolve16, join as join26, isAbsolute as isAbsolute4 } from "node:path";
18952
+ import { createHash as createHash19 } from "node:crypto";
18440
18953
  function createSkillAuditTool(deps) {
18441
18954
  return async function skillAudit(input) {
18442
18955
  const opId = input.operationId ?? `skill-audit-${Date.now().toString(36)}`;
@@ -18477,21 +18990,21 @@ function createSkillAuditTool(deps) {
18477
18990
  continue;
18478
18991
  }
18479
18992
  for (const f of ev.files ?? []) {
18480
- const filePath = join24(installRoot, ev.destination, f.path);
18481
- if (!existsSync22(filePath)) {
18993
+ const filePath = join26(installRoot, ev.destination, f.path);
18994
+ if (!existsSync23(filePath)) {
18482
18995
  missing.push({ skill: ev.skill, path: f.path, sequence: ev.sequence });
18483
18996
  continue;
18484
18997
  }
18485
- const raw = await readFile21(filePath);
18486
- const sha = createHash18("sha256").update(raw).digest("hex");
18998
+ const raw = await readFile23(filePath);
18999
+ const sha = createHash19("sha256").update(raw).digest("hex");
18487
19000
  if (sha !== f.sha256) {
18488
19001
  drifted.push({ skill: ev.skill, path: f.path, expected: f.sha256, actual: sha, sequence: ev.sequence });
18489
19002
  }
18490
19003
  }
18491
19004
  }
18492
19005
  const untracked = [];
18493
- const opencodeDir = join24(inventoryRoot, ".opencode", "skills");
18494
- if (existsSync22(opencodeDir)) {
19006
+ const opencodeDir = join26(inventoryRoot, ".opencode", "skills");
19007
+ if (existsSync23(opencodeDir)) {
18495
19008
  const entries = await readdir8(opencodeDir, { withFileTypes: true }).catch(() => []);
18496
19009
  for (const e of entries) {
18497
19010
  if (!e.isDirectory()) continue;
@@ -18513,16 +19026,16 @@ function createSkillAuditTool(deps) {
18513
19026
  }
18514
19027
 
18515
19028
  // src/tools/ship-skill-uninstall.js
18516
- import { readFile as readFile22, unlink as unlink5, rm as rm2, readdir as readdir9, lstat } from "node:fs/promises";
18517
- import { existsSync as existsSync23 } from "node:fs";
18518
- import { resolve as resolve17, join as join25 } from "node:path";
18519
- import { createHash as createHash19 } from "node:crypto";
18520
- var SAFE_ID_RE14 = /^[A-Za-z0-9._-]{1,128}$/;
19029
+ import { readFile as readFile24, unlink as unlink5, rm as rm2, readdir as readdir9, lstat } from "node:fs/promises";
19030
+ import { existsSync as existsSync24 } from "node:fs";
19031
+ import { resolve as resolve17, join as join27 } from "node:path";
19032
+ import { createHash as createHash20 } from "node:crypto";
19033
+ var SAFE_ID_RE15 = /^[A-Za-z0-9._-]{1,128}$/;
18521
19034
  function createSkillUninstallTool(deps) {
18522
19035
  return async function skillUninstall(input) {
18523
19036
  const opId = input.operationId ?? `skill-uninstall-${Date.now().toString(36)}`;
18524
19037
  const skillName = String(input.skill ?? "");
18525
- if (!skillName || !SAFE_ID_RE14.test(skillName)) {
19038
+ if (!skillName || !SAFE_ID_RE15.test(skillName)) {
18526
19039
  return failure("skill-uninstall", "skill required (safe id)", { operationId: opId, retryable: false });
18527
19040
  }
18528
19041
  const repoRoot = resolve17(deps.repoRoot);
@@ -18563,20 +19076,20 @@ function createSkillUninstallTool(deps) {
18563
19076
  return failure("skill-uninstall", `recorded file rejected: ${fileCheck.message}`, { operationId: opId, retryable: false });
18564
19077
  }
18565
19078
  const filePath = fileCheck.path;
18566
- if (!existsSync23(filePath)) {
19079
+ if (!existsSync24(filePath)) {
18567
19080
  return failure("skill-uninstall", `recorded file missing: ${f.path}`, { operationId: opId, retryable: false });
18568
19081
  }
18569
- const raw = await readFile22(filePath);
18570
- const sha = createHash19("sha256").update(raw).digest("hex");
19082
+ const raw = await readFile24(filePath);
19083
+ const sha = createHash20("sha256").update(raw).digest("hex");
18571
19084
  if (sha !== f.sha256) {
18572
19085
  return failure("skill-uninstall", `recorded file drifted: ${f.path}`, { operationId: opId, retryable: false });
18573
19086
  }
18574
19087
  }
18575
19088
  for (const f of found.install.files ?? []) {
18576
- const filePath = join25(skillDir, ...f.path.split("/"));
19089
+ const filePath = join27(skillDir, ...f.path.split("/"));
18577
19090
  await unlink5(filePath).catch(() => null);
18578
19091
  }
18579
- if (existsSync23(skillDir)) {
19092
+ if (existsSync24(skillDir)) {
18580
19093
  await rm2(skillDir, { recursive: true, force: true });
18581
19094
  }
18582
19095
  const recorded = await appendEvent2(inventoryRoot, {
@@ -18595,12 +19108,12 @@ function createSkillUninstallTool(deps) {
18595
19108
  };
18596
19109
  }
18597
19110
  async function listInstalledFiles(root) {
18598
- if (!existsSync23(root)) return { ok: true, paths: [] };
19111
+ if (!existsSync24(root)) return { ok: true, paths: [] };
18599
19112
  const paths = [];
18600
19113
  const walk2 = async (dir, prefix = "") => {
18601
19114
  for (const entry of await readdir9(dir, { withFileTypes: true })) {
18602
19115
  const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
18603
- const absolute = join25(dir, entry.name);
19116
+ const absolute = join27(dir, entry.name);
18604
19117
  const info = await lstat(absolute);
18605
19118
  if (info.isSymbolicLink()) return { ok: false, message: `symlinked content present: ${relative}` };
18606
19119
  if (info.isDirectory()) {
@@ -18624,9 +19137,9 @@ function recoverManifestAfterCrash(manifest) {
18624
19137
  }
18625
19138
 
18626
19139
  // src/installer/plugin-owner.js
18627
- import { spawnSync as spawnSync4 } from "node:child_process";
19140
+ import { spawnSync as spawnSync5 } from "node:child_process";
18628
19141
  async function reconcileOwner(repoRoot, adapter) {
18629
- const r = spawnSync4("git", ["-C", repoRoot, "config", "--get", "user.name"], {
19142
+ const r = spawnSync5("git", ["-C", repoRoot, "config", "--get", "user.name"], {
18630
19143
  encoding: "utf8"
18631
19144
  });
18632
19145
  if (r.status === 0 && r.stdout.trim()) return r.stdout.trim();
@@ -18634,8 +19147,8 @@ async function reconcileOwner(repoRoot, adapter) {
18634
19147
  }
18635
19148
 
18636
19149
  // src/installer/config.js
18637
- import { readFile as readFile23, writeFile as writeFile12, rename as rename6, mkdir as mkdir20 } from "node:fs/promises";
18638
- import { existsSync as existsSync24 } from "node:fs";
19150
+ import { readFile as readFile25, writeFile as writeFile12, rename as rename6, mkdir as mkdir20 } from "node:fs/promises";
19151
+ import { existsSync as existsSync25 } from "node:fs";
18639
19152
  import { dirname as dirname11, resolve as resolve18 } from "node:path";
18640
19153
 
18641
19154
  // schema/ship-config.schema.json
@@ -18947,8 +19460,8 @@ function configPath(repoRoot) {
18947
19460
  }
18948
19461
  async function loadConfig(repoRoot) {
18949
19462
  const path = configPath(repoRoot);
18950
- if (!existsSync24(path)) return null;
18951
- const raw = await readFile23(path, "utf8");
19463
+ if (!existsSync25(path)) return null;
19464
+ const raw = await readFile25(path, "utf8");
18952
19465
  let parsed;
18953
19466
  try {
18954
19467
  parsed = JSON.parse(raw);
@@ -19013,26 +19526,26 @@ function renderDefaultConfig(detection, overrides = {}) {
19013
19526
  }
19014
19527
 
19015
19528
  // src/installer/detection/project.js
19016
- import { spawnSync as spawnSync5 } from "node:child_process";
19017
- import { existsSync as existsSync25, readFileSync as readFileSync2 } from "node:fs";
19018
- import { resolve as resolve19, join as join26 } from "node:path";
19019
- function runGit2(cwd, args) {
19020
- const r = spawnSync5("git", ["-C", cwd, ...args], {
19529
+ import { spawnSync as spawnSync6 } from "node:child_process";
19530
+ import { existsSync as existsSync26, readFileSync as readFileSync2 } from "node:fs";
19531
+ import { resolve as resolve19, join as join28 } from "node:path";
19532
+ function runGit3(cwd, args) {
19533
+ const r = spawnSync6("git", ["-C", cwd, ...args], {
19021
19534
  stdio: ["ignore", "pipe", "pipe"],
19022
19535
  encoding: "utf8"
19023
19536
  });
19024
19537
  return { status: r.status ?? -1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
19025
19538
  }
19026
19539
  function detectPackageManager(repoRoot) {
19027
- if (existsSync25(join26(repoRoot, "pnpm-lock.yaml"))) return "pnpm";
19028
- if (existsSync25(join26(repoRoot, "yarn.lock"))) return "yarn";
19029
- if (existsSync25(join26(repoRoot, "bun.lockb"))) return "bun";
19030
- if (existsSync25(join26(repoRoot, "package-lock.json"))) return "npm";
19540
+ if (existsSync26(join28(repoRoot, "pnpm-lock.yaml"))) return "pnpm";
19541
+ if (existsSync26(join28(repoRoot, "yarn.lock"))) return "yarn";
19542
+ if (existsSync26(join28(repoRoot, "bun.lockb"))) return "bun";
19543
+ if (existsSync26(join28(repoRoot, "package-lock.json"))) return "npm";
19031
19544
  return null;
19032
19545
  }
19033
19546
  function readPackageJson(repoRoot) {
19034
- const path = join26(repoRoot, "package.json");
19035
- if (!existsSync25(path)) return null;
19547
+ const path = join28(repoRoot, "package.json");
19548
+ if (!existsSync26(path)) return null;
19036
19549
  try {
19037
19550
  return JSON.parse(readFileSync2(path, "utf8"));
19038
19551
  } catch {
@@ -19070,7 +19583,7 @@ function parseRepoSlugFromRemote(url2) {
19070
19583
  return null;
19071
19584
  }
19072
19585
  function detectRemote(repoRoot) {
19073
- const remotes = runGit2(repoRoot, ["remote", "-v"]);
19586
+ const remotes = runGit3(repoRoot, ["remote", "-v"]);
19074
19587
  if (remotes.status !== 0) return { candidates: [], primary: null };
19075
19588
  const lines = remotes.stdout.split("\n").filter(Boolean);
19076
19589
  const map2 = /* @__PURE__ */ new Map();
@@ -19086,23 +19599,23 @@ function detectRemote(repoRoot) {
19086
19599
  return { candidates: list, primary: origin };
19087
19600
  }
19088
19601
  function detectDefaultBranch(repoRoot, remoteName) {
19089
- const head = runGit2(repoRoot, ["symbolic-ref", `refs/remotes/${remoteName}/HEAD`]);
19602
+ const head = runGit3(repoRoot, ["symbolic-ref", `refs/remotes/${remoteName}/HEAD`]);
19090
19603
  if (head.status === 0) {
19091
19604
  const ref = head.stdout.trim();
19092
19605
  const match = ref.match(/^refs\/remotes\/[^/]+\/(.+)$/);
19093
19606
  if (match) return match[1];
19094
19607
  }
19095
- const local = runGit2(repoRoot, ["remote", "show", remoteName]);
19608
+ const local = runGit3(repoRoot, ["remote", "show", remoteName]);
19096
19609
  if (local.status === 0) {
19097
19610
  const match = local.stdout.match(/HEAD branch:\s*(\S+)/);
19098
19611
  if (match) return match[1];
19099
19612
  }
19100
- const branch = runGit2(repoRoot, ["branch", "--list"]);
19613
+ const branch = runGit3(repoRoot, ["branch", "--list"]);
19101
19614
  if (branch.status === 0 && /\*\s*main\b/.test(branch.stdout)) return "main";
19102
19615
  return null;
19103
19616
  }
19104
19617
  function detectOwner(repoRoot) {
19105
- const user = runGit2(repoRoot, ["config", "--get", "user.name"]);
19618
+ const user = runGit3(repoRoot, ["config", "--get", "user.name"]);
19106
19619
  if (user.status === 0 && user.stdout.trim()) return user.stdout.trim();
19107
19620
  const fallback = process.env.USER ?? process.env.USERNAME ?? "opencode-ship";
19108
19621
  return fallback;
@@ -19110,13 +19623,13 @@ function detectOwner(repoRoot) {
19110
19623
  function detectProject(repoRoot = process.cwd()) {
19111
19624
  const errors = [];
19112
19625
  const cwd = resolve19(repoRoot);
19113
- const inside = runGit2(cwd, ["rev-parse", "--show-toplevel"]);
19626
+ const inside = runGit3(cwd, ["rev-parse", "--show-toplevel"]);
19114
19627
  if (inside.status !== 0) {
19115
19628
  errors.push({ kind: "not-a-git-repo", path: cwd, detail: inside.stderr.trim() });
19116
19629
  return { repoRoot: cwd, errors };
19117
19630
  }
19118
19631
  const repoRootActual = inside.stdout.trim();
19119
- const headBranch = runGit2(repoRootActual, ["symbolic-ref", "--short", "HEAD"]);
19632
+ const headBranch = runGit3(repoRootActual, ["symbolic-ref", "--short", "HEAD"]);
19120
19633
  if (headBranch.status !== 0 || headBranch.stdout.trim().length === 0) {
19121
19634
  errors.push({ kind: "detached-head", path: repoRootActual, detail: headBranch.stderr.trim() });
19122
19635
  }
@@ -19156,11 +19669,11 @@ function detectProject(repoRoot = process.cwd()) {
19156
19669
  }
19157
19670
 
19158
19671
  // src/installer/cleanup.js
19159
- import { spawnSync as spawnSync6 } from "node:child_process";
19672
+ import { spawnSync as spawnSync7 } from "node:child_process";
19160
19673
  import { resolve as pathResolve } from "node:path";
19161
- import { existsSync as existsSync26, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "node:fs";
19674
+ import { existsSync as existsSync27, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "node:fs";
19162
19675
  function spawn8(repoRoot, args) {
19163
- const r = spawnSync6("git", ["-C", repoRoot, ...args], { encoding: "utf8" });
19676
+ const r = spawnSync7("git", ["-C", repoRoot, ...args], { encoding: "utf8" });
19164
19677
  return { status: r.status ?? -1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
19165
19678
  }
19166
19679
  function casDeleteBranch2(repoRoot, branch, expectedSha) {
@@ -19183,7 +19696,7 @@ async function cleanupPendingPath(repoRoot) {
19183
19696
  }
19184
19697
  async function loadCleanupPending(repoRoot) {
19185
19698
  const path = await cleanupPendingPath(repoRoot);
19186
- if (!existsSync26(path)) return [];
19699
+ if (!existsSync27(path)) return [];
19187
19700
  try {
19188
19701
  const raw = await readFileSync3(path, "utf8");
19189
19702
  const parsed = JSON.parse(raw);
@@ -19195,7 +19708,7 @@ async function loadCleanupPending(repoRoot) {
19195
19708
  async function saveCleanupPending(repoRoot, entries) {
19196
19709
  const path = await cleanupPendingPath(repoRoot);
19197
19710
  const dir = pathResolve(path, "..");
19198
- if (!existsSync26(dir)) mkdirSync2(dir, { recursive: true });
19711
+ if (!existsSync27(dir)) mkdirSync2(dir, { recursive: true });
19199
19712
  writeFileSync2(path, JSON.stringify(dedupePending(entries), null, 2) + "\n", "utf8");
19200
19713
  }
19201
19714
  function dedupePending(entries) {
@@ -19380,10 +19893,10 @@ function selectRuntimeAdapter({ config: config2, shipAdapter, legacyAdapter }) {
19380
19893
  }
19381
19894
 
19382
19895
  // src/version.js
19383
- import { readFileSync as readFileSync4, existsSync as existsSync27 } from "node:fs";
19896
+ import { readFileSync as readFileSync4, existsSync as existsSync28 } from "node:fs";
19384
19897
  import { dirname as dirname12, resolve as resolve21 } from "node:path";
19385
19898
  import { fileURLToPath } from "node:url";
19386
- var PACKAGE_VERSION = "1.1.7";
19899
+ var PACKAGE_VERSION = "1.1.8";
19387
19900
  var TEMPLATE_SET = `v${PACKAGE_VERSION}`;
19388
19901
 
19389
19902
  // src/plugin.js
@@ -19397,6 +19910,7 @@ var toolDefs = [
19397
19910
  ["delivery_ready", "Mark the PR ready after every required gate has passed.", "ready"],
19398
19911
  ["delivery_merge", "Squash merge the PR after an explicit user request.", "merge"],
19399
19912
  ["delivery_cleanup", "Remove the agent-owned worktree and branch after merge.", "cleanup"],
19913
+ ["delivery_abandon", "Abandon a closed unmerged delivery attempt after explicit approval.", "abandon"],
19400
19914
  ["delivery_github_read", "Typed read of issue, PR, or check data.", "githubRead"],
19401
19915
  ["delivery_issue_comment", "Idempotent typed comment on an issue.", "issueComment"],
19402
19916
  ["delivery_issue_labels", "Idempotent label add/remove on an issue.", "issueLabels"],
@@ -19404,6 +19918,7 @@ var toolDefs = [
19404
19918
  ["delivery_issue_close", "Close an issue with a recorded user permission subject.", "issueClose"],
19405
19919
  ["delivery_sync", "Fetch and merge base into the feature branch.", "sync"],
19406
19920
  ["delivery_publish", "Push the manifest branch to origin with HEAD verification.", "publish"],
19921
+ ["ship_deliver", "Dispatch durable delivery for an issue to the controller.", "deliver"],
19407
19922
  ["ship_plan_start", "Create a workflow and dispatch the configured planner.", "planStart"],
19408
19923
  ["ship_plan_submit", "Planner-only immutable PlanV2 submission.", "planSubmit"],
19409
19924
  ["ship_plan_approve", "Interactive approval + immutable local seal.", "planApprove"],
@@ -19472,7 +19987,7 @@ async function resolveRepoSlug(repoRoot, detection, config2) {
19472
19987
  const fromConfig = config2?.value?.project?.repository;
19473
19988
  if (typeof fromConfig === "string" && fromConfig.includes("/")) return fromConfig;
19474
19989
  if (detection?.repository) return detection.repository;
19475
- const gitConfig = await readFile24(resolve22(repoRoot, ".git/config"), "utf8").catch(() => null);
19990
+ const gitConfig = await readFile26(resolve22(repoRoot, ".git/config"), "utf8").catch(() => null);
19476
19991
  if (gitConfig) {
19477
19992
  const m = gitConfig.match(/url\s*=\s*.*?github\.com[:/]([^/]+)\/([^/\s]+?)(?:\.git)?\b/);
19478
19993
  if (m) return `${m[1]}/${m[2]}`;
@@ -19657,6 +20172,21 @@ var factories = {
19657
20172
  remote: "origin"
19658
20173
  })
19659
20174
  },
20175
+ abandon: {
20176
+ args: {
20177
+ taskId: tool.schema.string(),
20178
+ subject: tool.schema.string(),
20179
+ operationId: tool.schema.string().optional()
20180
+ },
20181
+ build: (rt) => createAbandonTool({
20182
+ driver: rt.driver,
20183
+ repoRoot: rt.repoRoot,
20184
+ repoSlug: rt.repoSlug,
20185
+ owner: rt.owner,
20186
+ adapter: rt.adapter,
20187
+ remote: "origin"
20188
+ })
20189
+ },
19660
20190
  githubRead: {
19661
20191
  args: {
19662
20192
  resource: tool.schema.enum(["issue", "pr", "checks"]),
@@ -19752,6 +20282,17 @@ var factories = {
19752
20282
  owner: rt.owner
19753
20283
  })
19754
20284
  },
20285
+ deliver: {
20286
+ args: {
20287
+ issueNumber: tool.schema.number(),
20288
+ operationId: tool.schema.string().optional()
20289
+ },
20290
+ build: (rt, ctx) => createDeliverTool({
20291
+ repoRoot: rt.repoRoot,
20292
+ opencodeClient: rt.opencodeClient,
20293
+ ctx
20294
+ })
20295
+ },
19755
20296
  planStart: {
19756
20297
  args: {
19757
20298
  issueNumber: tool.schema.number(),