opencode-ship 1.1.1 → 1.1.2-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/plugin.js CHANGED
@@ -1,4 +1,4 @@
1
- // opencode-ship v1.1.1
1
+ // opencode-ship v1.1.2-rc.1
2
2
  var __defProp = Object.defineProperty;
3
3
  var __export = (target, all) => {
4
4
  for (var name in all)
@@ -12428,8 +12428,8 @@ function tool(input) {
12428
12428
  tool.schema = external_exports;
12429
12429
 
12430
12430
  // src/plugin.js
12431
- import { resolve as resolve14 } from "node:path";
12432
- import { readFile as readFile12 } from "node:fs/promises";
12431
+ import { resolve as resolve21 } from "node:path";
12432
+ import { readFile as readFile19 } from "node:fs/promises";
12433
12433
 
12434
12434
  // src/adapter.js
12435
12435
  import { readFile, writeFile, mkdir, rename } from "node:fs/promises";
@@ -12706,7 +12706,7 @@ function validateGhArgv(argv) {
12706
12706
 
12707
12707
  // src/drivers/gh-cli.js
12708
12708
  function defaultRunner(cwd, env) {
12709
- return (args) => new Promise((resolve15, reject2) => {
12709
+ return (args) => new Promise((resolve22, reject2) => {
12710
12710
  const proc = spawn("gh", args, {
12711
12711
  cwd,
12712
12712
  env,
@@ -12718,7 +12718,7 @@ function defaultRunner(cwd, env) {
12718
12718
  proc.stdout.on("data", (d) => stdout += d.toString());
12719
12719
  proc.stderr.on("data", (d) => stderr += d.toString());
12720
12720
  proc.on("error", reject2);
12721
- proc.on("close", (status) => resolve15({ status: status ?? -1, stdout, stderr }));
12721
+ proc.on("close", (status) => resolve22({ status: status ?? -1, stdout, stderr }));
12722
12722
  });
12723
12723
  }
12724
12724
  function viewFields() {
@@ -15392,9 +15392,9 @@ function createRunStartTool(deps) {
15392
15392
  };
15393
15393
  }
15394
15394
 
15395
- // src/tools/ship-task-report.js
15396
- import { mkdir as mkdir8 } from "node:fs/promises";
15397
- import { createHash as createHash6 } from "node:crypto";
15395
+ // src/tools/ship-task-start.js
15396
+ import { readFile as readFile9, mkdir as mkdir9 } from "node:fs/promises";
15397
+ import { existsSync as existsSync8 } from "node:fs";
15398
15398
  import { join as join10 } from "node:path";
15399
15399
 
15400
15400
  // src/workflow/run-controller.js
@@ -15699,9 +15699,302 @@ async function readRunState(repoRoot, workflowId) {
15699
15699
  events
15700
15700
  };
15701
15701
  }
15702
+ function buildCommitTrailers({ workflowId, planHash, taskId, round, reviewHash }) {
15703
+ return [
15704
+ `Opencode-Ship-Workflow: ${workflowId}`,
15705
+ `Opencode-Ship-Plan: ${planHash}`,
15706
+ `Opencode-Ship-Task: ${taskId}`,
15707
+ `Opencode-Ship-Review: ${reviewHash ?? "n/a"}`,
15708
+ `Opencode-Ship-Round: ${round}`
15709
+ ];
15710
+ }
15702
15711
 
15703
- // src/tools/ship-task-report.js
15712
+ // src/installer/lock.js
15713
+ import { readFile as readFile8, writeFile as writeFile8, rename as rename3, mkdir as mkdir8 } from "node:fs/promises";
15714
+ import { existsSync as existsSync7 } from "node:fs";
15715
+ import { dirname as dirname5, resolve as resolve9 } from "node:path";
15716
+
15717
+ // src/installer/hash.js
15718
+ import { createHash as createHash6 } from "node:crypto";
15719
+ function bytesHash(buffer) {
15720
+ return createHash6("sha256").update(buffer).digest("hex");
15721
+ }
15722
+ function bytesHashString(text) {
15723
+ return bytesHash(Buffer.from(text, "utf8"));
15724
+ }
15725
+
15726
+ // src/installer/lock.js
15727
+ function lockPath(repoRoot) {
15728
+ return resolve9(repoRoot, ".opencode", "ship.lock.json");
15729
+ }
15730
+ async function readLock2(repoRoot) {
15731
+ const path = lockPath(repoRoot);
15732
+ if (!existsSync7(path)) return null;
15733
+ try {
15734
+ const raw = await readFile8(path, "utf8");
15735
+ return JSON.parse(raw);
15736
+ } catch {
15737
+ return null;
15738
+ }
15739
+ }
15740
+ function isSetupComplete(lock) {
15741
+ if (!lock || typeof lock !== "object") return false;
15742
+ const manager = lock.manager;
15743
+ if (!manager || typeof manager !== "object") return false;
15744
+ return manager.setupComplete === true;
15745
+ }
15746
+
15747
+ // src/tools/ship-task-start.js
15704
15748
  var SAFE_ID_RE5 = /^[A-Za-z0-9._-]{1,128}$/;
15749
+ function createTaskStartTool(deps) {
15750
+ return async function taskStart(input) {
15751
+ const opId = input.operationId ?? `task-start-${Date.now().toString(36)}`;
15752
+ const workflowId = String(input.workflowId ?? "");
15753
+ const taskId = String(input.taskId ?? "");
15754
+ const briefHash = String(input.briefHash ?? "");
15755
+ const sessionID = String(input.sessionID ?? "");
15756
+ const submittedBy = String(input.submittedBy ?? "");
15757
+ if (!workflowId || !SAFE_ID_RE5.test(workflowId)) {
15758
+ return failure("task-start", "workflowId required (safe id)", { operationId: opId, retryable: false });
15759
+ }
15760
+ if (!taskId || !SAFE_ID_RE5.test(taskId)) {
15761
+ return failure("task-start", "taskId required (safe id)", { operationId: opId, retryable: false });
15762
+ }
15763
+ if (!briefHash || !/^[0-9a-f]{64}$/.test(briefHash)) {
15764
+ return failure("task-start", "briefHash required (sha256)", { operationId: opId, retryable: false });
15765
+ }
15766
+ if (!sessionID) {
15767
+ return failure("task-start", "sessionID required (must identify builder session)", { operationId: opId, retryable: false });
15768
+ }
15769
+ if (!submittedBy) {
15770
+ return failure("task-start", "submittedBy required (must identify builder model)", { operationId: opId, retryable: false });
15771
+ }
15772
+ const lock = await readLock2(deps.repoRoot);
15773
+ if (!isSetupComplete(lock)) {
15774
+ return failure("task-start", "setup is not complete; run /setup-ship-workflow first", { operationId: opId, retryable: false });
15775
+ }
15776
+ let models;
15777
+ try {
15778
+ models = resolveModelRoles(deps.config?.workflow, { strict: true });
15779
+ } catch (err) {
15780
+ return failure("task-start", `builder model unresolved: ${err?.message ?? err}`, { operationId: opId, retryable: false });
15781
+ }
15782
+ if (!submittedBy.startsWith(models.builder)) {
15783
+ return failure("task-start", `submittedBy must be the configured builder model ${models.builder}`, { operationId: opId, retryable: false });
15784
+ }
15785
+ let runState;
15786
+ try {
15787
+ runState = await readRunState(deps.repoRoot, workflowId);
15788
+ } catch (err) {
15789
+ return failure("task-start", `run state unreadable: ${err?.message ?? err}`, { operationId: opId, retryable: false });
15790
+ }
15791
+ if (!runState) {
15792
+ return failure("task-start", "run not started", { operationId: opId, retryable: false });
15793
+ }
15794
+ try {
15795
+ const commonDir = await resolveGitCommonDir(deps.repoRoot);
15796
+ const dispatchDir = join10(opencodeShipStateDir(commonDir), "runs", workflowId, "tasks", taskId, "dispatch");
15797
+ await mkdir9(dispatchDir, { recursive: true });
15798
+ const record2 = {
15799
+ workflowId,
15800
+ taskId,
15801
+ sessionID,
15802
+ builder: models.builder,
15803
+ briefHash,
15804
+ dispatchedAt: (/* @__PURE__ */ new Date()).toISOString()
15805
+ };
15806
+ await publishImmutableJson(join10(dispatchDir, "dispatch.json"), record2);
15807
+ const { state, event } = await appendRunEvent(
15808
+ deps.repoRoot,
15809
+ workflowId,
15810
+ runState,
15811
+ { kind: RUN_EVENT_KINDS.TASK_DISPATCH, data: { taskId, briefHash, sessionID } }
15812
+ );
15813
+ return success2("task-start", { workflowId, taskId, state: state.state, sequence: event.sequence, round: state.round }, { operationId: opId });
15814
+ } catch (err) {
15815
+ return failure("task-start", String(err?.message ?? err), { operationId: opId, retryable: true });
15816
+ }
15817
+ };
15818
+ }
15819
+
15820
+ // src/tools/ship-task-commit.js
15821
+ import { execFile } from "node:child_process";
15822
+ import { mkdir as mkdir10 } from "node:fs/promises";
15823
+ import { join as join11 } from "node:path";
15824
+ var SAFE_ID_RE6 = /^[A-Za-z0-9._-]{1,128}$/;
15825
+ function spawn5(cmd, args, cwd) {
15826
+ return new Promise((resolveP, rejectP) => {
15827
+ execFile(cmd, args, { cwd, shell: false }, (err, stdout, stderr) => {
15828
+ if (err) {
15829
+ const msg = typeof stderr === "string" ? stderr : stderr ? String(stderr) : err.message;
15830
+ return rejectP(new Error(`${cmd} failed: ${msg}`));
15831
+ }
15832
+ resolveP(typeof stdout === "string" ? stdout : String(stdout));
15833
+ });
15834
+ });
15835
+ }
15836
+ function createTaskCommitTool(deps) {
15837
+ return async function taskCommit(input) {
15838
+ const opId = input.operationId ?? `task-commit-${Date.now().toString(36)}`;
15839
+ const workflowId = String(input.workflowId ?? "");
15840
+ const taskId = String(input.taskId ?? "");
15841
+ const expectedHead = String(input.expectedHead ?? "");
15842
+ const commitSha = String(input.commitSha ?? "");
15843
+ const planHash = String(input.planHash ?? "");
15844
+ const reviewHash = String(input.reviewHash ?? "");
15845
+ const round = Number(input.round ?? 1);
15846
+ if (!workflowId || !SAFE_ID_RE6.test(workflowId)) {
15847
+ return failure("task-commit", "workflowId required (safe id)", { operationId: opId, retryable: false });
15848
+ }
15849
+ if (!taskId || !SAFE_ID_RE6.test(taskId)) {
15850
+ return failure("task-commit", "taskId required (safe id)", { operationId: opId, retryable: false });
15851
+ }
15852
+ if (!/^[0-9a-f]{40}$/.test(expectedHead)) {
15853
+ return failure("task-commit", "expectedHead required (40-char commit SHA)", { operationId: opId, retryable: false });
15854
+ }
15855
+ if (!/^[0-9a-f]{40}$/.test(commitSha)) {
15856
+ return failure("task-commit", "commitSha required (40-char commit SHA)", { operationId: opId, retryable: false });
15857
+ }
15858
+ if (!/^[0-9a-f]{64}$/.test(planHash)) {
15859
+ return failure("task-commit", "planHash required (sha256)", { operationId: opId, retryable: false });
15860
+ }
15861
+ if (!reviewHash) {
15862
+ return failure("task-commit", "reviewHash required (from ship_task_review)", { operationId: opId, retryable: false });
15863
+ }
15864
+ const lock = await readLock2(deps.repoRoot);
15865
+ if (!isSetupComplete(lock)) {
15866
+ return failure("task-commit", "setup is not complete; run /setup-ship-workflow first", { operationId: opId, retryable: false });
15867
+ }
15868
+ let runState;
15869
+ try {
15870
+ runState = await readRunState(deps.repoRoot, workflowId);
15871
+ } catch (err) {
15872
+ return failure("task-commit", `run state unreadable: ${err?.message ?? err}`, { operationId: opId, retryable: false });
15873
+ }
15874
+ if (!runState) {
15875
+ return failure("task-commit", "run not started", { operationId: opId, retryable: false });
15876
+ }
15877
+ if (runState.activeTask !== taskId) {
15878
+ return failure("task-commit", `no active task ${taskId} (active=${runState.activeTask})`, { operationId: opId, retryable: false });
15879
+ }
15880
+ if (runState.state !== "commit-pending") {
15881
+ return failure("task-commit", `task-review must pass before commit; run state=${runState.state}`, { operationId: opId, retryable: false });
15882
+ }
15883
+ try {
15884
+ const actualHead = (await spawn5("git", ["-C", deps.repoRoot, "rev-parse", "HEAD"], deps.repoRoot)).trim();
15885
+ if (actualHead !== expectedHead) {
15886
+ return failure("task-commit", `HEAD drift (expected ${expectedHead.slice(0, 8)}, got ${actualHead.slice(0, 8)})`, { operationId: opId, retryable: false });
15887
+ }
15888
+ const trailers = buildCommitTrailers({ workflowId, planHash, taskId, round, reviewHash });
15889
+ const message = await spawn5("git", ["-C", deps.repoRoot, "log", "-1", "--format=%B", expectedHead], deps.repoRoot);
15890
+ const trailerLines = trailers.map((t) => ` ${t}`).join("\n");
15891
+ if (!message.includes(trailers[0])) {
15892
+ return failure("task-commit", `commit ${expectedHead.slice(0, 8)} missing Opencode-Ship-Workflow trailer`, { operationId: opId, retryable: false });
15893
+ }
15894
+ const commonDir = await resolveGitCommonDir(deps.repoRoot);
15895
+ const commitDir = join11(opencodeShipStateDir(commonDir), "runs", workflowId, "tasks", taskId, "commit");
15896
+ await mkdir10(commitDir, { recursive: true });
15897
+ const record2 = {
15898
+ workflowId,
15899
+ taskId,
15900
+ round,
15901
+ commitSha,
15902
+ planHash,
15903
+ reviewHash,
15904
+ trailers,
15905
+ trailerBlock: trailerLines,
15906
+ committedAt: (/* @__PURE__ */ new Date()).toISOString()
15907
+ };
15908
+ await publishImmutableJson(join11(commitDir, "commit.json"), record2);
15909
+ const { state, event } = await appendRunEvent(
15910
+ deps.repoRoot,
15911
+ workflowId,
15912
+ runState,
15913
+ { kind: RUN_EVENT_KINDS.COMMIT, data: { taskId, commitSha } }
15914
+ );
15915
+ return success2("task-commit", { workflowId, taskId, commitSha, state: state.state, sequence: event.sequence }, { operationId: opId });
15916
+ } catch (err) {
15917
+ return failure("task-commit", String(err?.message ?? err), { operationId: opId, retryable: true });
15918
+ }
15919
+ };
15920
+ }
15921
+
15922
+ // src/tools/ship-task-complete.js
15923
+ import { mkdir as mkdir11 } from "node:fs/promises";
15924
+ import { join as join12 } from "node:path";
15925
+ var SAFE_ID_RE7 = /^[A-Za-z0-9._-]{1,128}$/;
15926
+ function createTaskCompleteTool(deps) {
15927
+ return async function taskComplete(input) {
15928
+ const opId = input.operationId ?? `task-complete-${Date.now().toString(36)}`;
15929
+ const workflowId = String(input.workflowId ?? "");
15930
+ const taskId = String(input.taskId ?? "");
15931
+ const moreTasks = input.moreTasks === false ? false : input.moreTasks === true ? true : null;
15932
+ const nextTaskId = input.nextTaskId ? String(input.nextTaskId) : null;
15933
+ if (!workflowId || !SAFE_ID_RE7.test(workflowId)) {
15934
+ return failure("task-complete", "workflowId required (safe id)", { operationId: opId, retryable: false });
15935
+ }
15936
+ if (!taskId || !SAFE_ID_RE7.test(taskId)) {
15937
+ return failure("task-complete", "taskId required (safe id)", { operationId: opId, retryable: false });
15938
+ }
15939
+ if (moreTasks === null) {
15940
+ return failure("task-complete", "moreTasks must be explicitly true or false", { operationId: opId, retryable: false });
15941
+ }
15942
+ if (moreTasks && (!nextTaskId || !SAFE_ID_RE7.test(nextTaskId))) {
15943
+ return failure("task-complete", "nextTaskId required when moreTasks=true", { operationId: opId, retryable: false });
15944
+ }
15945
+ const lock = await readLock2(deps.repoRoot);
15946
+ if (!isSetupComplete(lock)) {
15947
+ return failure("task-complete", "setup is not complete; run /setup-ship-workflow first", { operationId: opId, retryable: false });
15948
+ }
15949
+ let runState;
15950
+ try {
15951
+ runState = await readRunState(deps.repoRoot, workflowId);
15952
+ } catch (err) {
15953
+ return failure("task-complete", `run state unreadable: ${err?.message ?? err}`, { operationId: opId, retryable: false });
15954
+ }
15955
+ if (!runState) {
15956
+ return failure("task-complete", "run not started", { operationId: opId, retryable: false });
15957
+ }
15958
+ if (runState.state !== "committed") {
15959
+ return failure("task-complete", `task-commit must precede task-complete; run state=${runState.state}`, { operationId: opId, retryable: false });
15960
+ }
15961
+ try {
15962
+ const commonDir = await resolveGitCommonDir(deps.repoRoot);
15963
+ const completeDir = join12(opencodeShipStateDir(commonDir), "runs", workflowId, "tasks", taskId, "complete");
15964
+ await mkdir11(completeDir, { recursive: true });
15965
+ const record2 = {
15966
+ workflowId,
15967
+ taskId,
15968
+ moreTasks,
15969
+ nextTaskId: nextTaskId ?? null,
15970
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
15971
+ };
15972
+ await publishImmutableJson(join12(completeDir, "complete.json"), record2);
15973
+ const { state, event } = await appendRunEvent(
15974
+ deps.repoRoot,
15975
+ workflowId,
15976
+ runState,
15977
+ { kind: RUN_EVENT_KINDS.TASK_COMPLETE, data: { taskId, moreTasks, nextTaskId: nextTaskId ?? null } }
15978
+ );
15979
+ return success2("task-complete", {
15980
+ workflowId,
15981
+ taskId,
15982
+ moreTasks,
15983
+ nextTaskId: nextTaskId ?? null,
15984
+ state: state.state,
15985
+ sequence: event.sequence
15986
+ }, { operationId: opId });
15987
+ } catch (err) {
15988
+ return failure("task-complete", String(err?.message ?? err), { operationId: opId, retryable: true });
15989
+ }
15990
+ };
15991
+ }
15992
+
15993
+ // src/tools/ship-task-report.js
15994
+ import { mkdir as mkdir12 } from "node:fs/promises";
15995
+ import { createHash as createHash7 } from "node:crypto";
15996
+ import { join as join13 } from "node:path";
15997
+ var SAFE_ID_RE8 = /^[A-Za-z0-9._-]{1,128}$/;
15705
15998
  function createTaskReportTool(deps) {
15706
15999
  return async function taskReport(input) {
15707
16000
  const opId = input.operationId ?? `task-report-${Date.now().toString(36)}`;
@@ -15710,10 +16003,10 @@ function createTaskReportTool(deps) {
15710
16003
  const round = Number(input.round ?? 1);
15711
16004
  const submittedBy = String(input.submittedBy ?? "");
15712
16005
  const summary = String(input.summary ?? "");
15713
- if (!workflowId || !SAFE_ID_RE5.test(workflowId)) {
16006
+ if (!workflowId || !SAFE_ID_RE8.test(workflowId)) {
15714
16007
  return failure("task-report", "workflowId required (safe id)", { operationId: opId, retryable: false });
15715
16008
  }
15716
- if (!taskId || !SAFE_ID_RE5.test(taskId)) {
16009
+ if (!taskId || !SAFE_ID_RE8.test(taskId)) {
15717
16010
  return failure("task-report", "taskId required (safe id)", { operationId: opId, retryable: false });
15718
16011
  }
15719
16012
  if (!Number.isInteger(round) || round <= 0) {
@@ -15746,8 +16039,8 @@ function createTaskReportTool(deps) {
15746
16039
  }
15747
16040
  try {
15748
16041
  const commonDir = await resolveGitCommonDir(deps.repoRoot);
15749
- const reportDir = join10(opencodeShipStateDir(commonDir), "runs", workflowId, "tasks", taskId, "rounds", `${String(round).padStart(4, "0")}`);
15750
- await mkdir8(reportDir, { recursive: true });
16042
+ const reportDir = join13(opencodeShipStateDir(commonDir), "runs", workflowId, "tasks", taskId, "rounds", `${String(round).padStart(4, "0")}`);
16043
+ await mkdir12(reportDir, { recursive: true });
15751
16044
  const record2 = {
15752
16045
  workflowId,
15753
16046
  taskId,
@@ -15759,7 +16052,7 @@ function createTaskReportTool(deps) {
15759
16052
  tests: Array.isArray(input.tests) ? input.tests : [],
15760
16053
  submittedAt: (/* @__PURE__ */ new Date()).toISOString()
15761
16054
  };
15762
- await publishImmutableJson(join10(reportDir, "implementer-report.json"), record2);
16055
+ await publishImmutableJson(join13(reportDir, "implementer-report.json"), record2);
15763
16056
  const { state, event } = await appendRunEvent(
15764
16057
  deps.repoRoot,
15765
16058
  workflowId,
@@ -15776,14 +16069,14 @@ function reportHash(record2) {
15776
16069
  const sorted = Object.keys(record2).sort();
15777
16070
  const ordered = {};
15778
16071
  for (const k of sorted) ordered[k] = record2[k];
15779
- return createHash6("sha256").update(JSON.stringify(ordered), "utf8").digest("hex");
16072
+ return createHash7("sha256").update(JSON.stringify(ordered), "utf8").digest("hex");
15780
16073
  }
15781
16074
 
15782
16075
  // src/tools/ship-task-review.js
15783
- import { createHash as createHash7 } from "node:crypto";
15784
- import { mkdir as mkdir9 } from "node:fs/promises";
15785
- import { join as join11 } from "node:path";
15786
- var SAFE_ID_RE6 = /^[A-Za-z0-9._-]{1,128}$/;
16076
+ import { createHash as createHash8 } from "node:crypto";
16077
+ import { mkdir as mkdir13 } from "node:fs/promises";
16078
+ import { join as join14 } from "node:path";
16079
+ var SAFE_ID_RE9 = /^[A-Za-z0-9._-]{1,128}$/;
15787
16080
  var VERDICT_VALUES = /* @__PURE__ */ new Set(["pass", "fail", "none"]);
15788
16081
  function createTaskReviewTool(deps) {
15789
16082
  return async function taskReview(input) {
@@ -15794,10 +16087,10 @@ function createTaskReviewTool(deps) {
15794
16087
  const spec = input.spec;
15795
16088
  const quality = input.quality;
15796
16089
  const submittedBy = String(input.submittedBy ?? "");
15797
- if (!workflowId || !SAFE_ID_RE6.test(workflowId)) {
16090
+ if (!workflowId || !SAFE_ID_RE9.test(workflowId)) {
15798
16091
  return failure("task-review", "workflowId required (safe id)", { operationId: opId, retryable: false });
15799
16092
  }
15800
- if (!taskId || !SAFE_ID_RE6.test(taskId)) {
16093
+ if (!taskId || !SAFE_ID_RE9.test(taskId)) {
15801
16094
  return failure("task-review", "taskId required (safe id)", { operationId: opId, retryable: false });
15802
16095
  }
15803
16096
  if (!Number.isInteger(round) || round <= 0) {
@@ -15818,8 +16111,8 @@ function createTaskReviewTool(deps) {
15818
16111
  } catch (err) {
15819
16112
  return failure("task-review", `reviewer model unresolved: ${err?.message ?? err}`, { operationId: opId, retryable: false });
15820
16113
  }
15821
- if (!submittedBy.startsWith(models.finalReviewer)) {
15822
- return failure("task-review", `submittedBy must be the configured finalReviewer model ${models.finalReviewer}`, { operationId: opId, retryable: false });
16114
+ if (!submittedBy.startsWith(models.builder)) {
16115
+ return failure("task-review", `submittedBy must be the configured builder model ${models.builder}`, { operationId: opId, retryable: false });
15823
16116
  }
15824
16117
  let runState;
15825
16118
  try {
@@ -15835,8 +16128,8 @@ function createTaskReviewTool(deps) {
15835
16128
  }
15836
16129
  try {
15837
16130
  const commonDir = await resolveGitCommonDir(deps.repoRoot);
15838
- const reviewDir = join11(opencodeShipStateDir(commonDir), "runs", workflowId, "tasks", taskId, "rounds", `${String(round).padStart(4, "0")}`);
15839
- await mkdir9(reviewDir, { recursive: true });
16131
+ const reviewDir = join14(opencodeShipStateDir(commonDir), "runs", workflowId, "tasks", taskId, "rounds", `${String(round).padStart(4, "0")}`);
16132
+ await mkdir13(reviewDir, { recursive: true });
15840
16133
  const specPass = String(spec.verdict) === "pass";
15841
16134
  const qualityPass = String(quality.verdict) === "pass";
15842
16135
  const reviewHash = verdictHash({ spec, quality, submittedBy, taskId, round });
@@ -15845,13 +16138,13 @@ function createTaskReviewTool(deps) {
15845
16138
  taskId,
15846
16139
  round,
15847
16140
  submittedBy,
15848
- reviewer: models.finalReviewer,
16141
+ reviewer: models.builder,
15849
16142
  spec,
15850
16143
  quality,
15851
16144
  state: specPass && qualityPass ? "commit-pending" : "fix-pending",
15852
16145
  reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
15853
16146
  };
15854
- await publishImmutableJson(join11(reviewDir, "review.json"), record2);
16147
+ await publishImmutableJson(join14(reviewDir, "review.json"), record2);
15855
16148
  const verdict = specPass && qualityPass ? "pass" : "fail";
15856
16149
  const { state, event } = await appendRunEvent(
15857
16150
  deps.repoRoot,
@@ -15869,13 +16162,124 @@ function verdictHash(record2) {
15869
16162
  const sorted = Object.keys(record2).sort();
15870
16163
  const ordered = {};
15871
16164
  for (const k of sorted) ordered[k] = record2[k];
15872
- return createHash7("sha256").update(JSON.stringify(ordered), "utf8").digest("hex");
16165
+ return createHash8("sha256").update(JSON.stringify(ordered), "utf8").digest("hex");
16166
+ }
16167
+
16168
+ // src/tools/ship-final-review.js
16169
+ import { mkdir as mkdir14 } from "node:fs/promises";
16170
+ import { join as join15 } from "node:path";
16171
+ var SAFE_ID_RE10 = /^[A-Za-z0-9._-]{1,128}$/;
16172
+ var AXES = /* @__PURE__ */ new Set(["standards", "spec"]);
16173
+ var VERDICTS = /* @__PURE__ */ new Set(["pass", "fail", "blocked"]);
16174
+ function createFinalReviewTool(deps) {
16175
+ return async function finalReview(input) {
16176
+ const opId = input.operationId ?? `final-review-${Date.now().toString(36)}`;
16177
+ const workflowId = String(input.workflowId ?? "");
16178
+ const axis = String(input.axis ?? "");
16179
+ const verdict = String(input.verdict ?? "");
16180
+ const headSha = String(input.headSha ?? "");
16181
+ const mergeBaseSha = String(input.mergeBaseSha ?? "");
16182
+ const packageHash = String(input.packageHash ?? "");
16183
+ const submittedBy = String(input.submittedBy ?? "");
16184
+ const findings = Array.isArray(input.findings) ? input.findings : [];
16185
+ if (!workflowId || !SAFE_ID_RE10.test(workflowId)) {
16186
+ return failure("final-review", "workflowId required (safe id)", { operationId: opId, retryable: false });
16187
+ }
16188
+ if (!AXES.has(axis)) {
16189
+ return failure("final-review", "axis must be 'standards' or 'spec'", { operationId: opId, retryable: false });
16190
+ }
16191
+ if (!VERDICTS.has(verdict)) {
16192
+ return failure("final-review", "verdict must be one of pass|fail|blocked", { operationId: opId, retryable: false });
16193
+ }
16194
+ if (!/^[0-9a-f]{40}$/.test(headSha)) {
16195
+ return failure("final-review", "headSha required (40-char commit SHA)", { operationId: opId, retryable: false });
16196
+ }
16197
+ if (!/^[0-9a-f]{40}$/.test(mergeBaseSha)) {
16198
+ return failure("final-review", "mergeBaseSha required (40-char commit SHA)", { operationId: opId, retryable: false });
16199
+ }
16200
+ if (!/^[0-9a-f]{64}$/.test(packageHash)) {
16201
+ return failure("final-review", "packageHash required (sha256)", { operationId: opId, retryable: false });
16202
+ }
16203
+ if (!submittedBy) {
16204
+ return failure("final-review", "submittedBy required (must identify finalReviewer model)", { operationId: opId, retryable: false });
16205
+ }
16206
+ const lock = await readLock2(deps.repoRoot);
16207
+ if (!isSetupComplete(lock)) {
16208
+ return failure("final-review", "setup is not complete; run /setup-ship-workflow first", { operationId: opId, retryable: false });
16209
+ }
16210
+ let models;
16211
+ try {
16212
+ models = resolveModelRoles(deps.config?.workflow, { strict: true });
16213
+ } catch (err) {
16214
+ return failure("final-review", `final reviewer model unresolved: ${err?.message ?? err}`, { operationId: opId, retryable: false });
16215
+ }
16216
+ if (!submittedBy.startsWith(models.finalReviewer)) {
16217
+ return failure("final-review", `submittedBy must be the configured finalReviewer model ${models.finalReviewer}`, { operationId: opId, retryable: false });
16218
+ }
16219
+ let runState;
16220
+ try {
16221
+ runState = await readRunState(deps.repoRoot, workflowId);
16222
+ } catch (err) {
16223
+ return failure("final-review", `run state unreadable: ${err?.message ?? err}`, { operationId: opId, retryable: false });
16224
+ }
16225
+ if (!runState) {
16226
+ return failure("final-review", "run not started", { operationId: opId, retryable: false });
16227
+ }
16228
+ if (runState.state !== "all-tasks-done" && runState.state !== "ready-pending") {
16229
+ return failure("final-review", `final review requires all-tasks-done; run state=${runState.state}`, { operationId: opId, retryable: false });
16230
+ }
16231
+ try {
16232
+ const commonDir = await resolveGitCommonDir(deps.repoRoot);
16233
+ const reviewDir = join15(opencodeShipStateDir(commonDir), "runs", workflowId, "final-review", axis);
16234
+ await mkdir14(reviewDir, { recursive: true });
16235
+ const record2 = {
16236
+ workflowId,
16237
+ axis,
16238
+ verdict,
16239
+ headSha,
16240
+ mergeBaseSha,
16241
+ packageHash,
16242
+ submittedBy,
16243
+ reviewer: models.finalReviewer,
16244
+ findings,
16245
+ reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
16246
+ };
16247
+ await publishImmutableJson(join15(reviewDir, "review.json"), record2);
16248
+ const { state, event } = await appendRunEvent(
16249
+ deps.repoRoot,
16250
+ workflowId,
16251
+ runState,
16252
+ {
16253
+ kind: RUN_EVENT_KINDS.FINAL_REVIEW,
16254
+ data: {
16255
+ axis,
16256
+ verdict,
16257
+ headSha,
16258
+ mergeBaseSha,
16259
+ packageHash,
16260
+ review: { verdict, headSha, mergeBaseSha, packageHash }
16261
+ }
16262
+ }
16263
+ );
16264
+ return success2("final-review", {
16265
+ workflowId,
16266
+ axis,
16267
+ verdict,
16268
+ headSha,
16269
+ state: state.state,
16270
+ sequence: event.sequence,
16271
+ finalReview: state.finalReview ?? null
16272
+ }, { operationId: opId });
16273
+ } catch (err) {
16274
+ return failure("final-review", String(err?.message ?? err), { operationId: opId, retryable: true });
16275
+ }
16276
+ };
15873
16277
  }
15874
16278
 
15875
16279
  // src/workflow/resume.js
15876
- import { readFile as readFile8, writeFile as writeFile8, readdir as readdir6, mkdir as mkdir10 } from "node:fs/promises";
15877
- import { existsSync as existsSync7 } from "node:fs";
15878
- import { join as join12 } from "node:path";
16280
+ import { readFile as readFile10, writeFile as writeFile9, readdir as readdir6, mkdir as mkdir15 } from "node:fs/promises";
16281
+ import { existsSync as existsSync9 } from "node:fs";
16282
+ import { join as join16 } from "node:path";
15879
16283
  function parseTrailer(text, key) {
15880
16284
  if (typeof text !== "string") return null;
15881
16285
  const re = new RegExp(`^${key}:\\s*(.+)$`, "m");
@@ -15949,12 +16353,12 @@ async function resumeRun(repoRoot, workflowId) {
15949
16353
  }
15950
16354
 
15951
16355
  // src/tools/ship-resume.js
15952
- var SAFE_ID_RE7 = /^[A-Za-z0-9._-]{1,128}$/;
16356
+ var SAFE_ID_RE11 = /^[A-Za-z0-9._-]{1,128}$/;
15953
16357
  function createResumeTool(deps) {
15954
16358
  return async function resume(input) {
15955
16359
  const opId = input.operationId ?? `resume-${Date.now().toString(36)}`;
15956
16360
  const workflowId = String(input.workflowId ?? "");
15957
- if (!workflowId || !SAFE_ID_RE7.test(workflowId)) {
16361
+ if (!workflowId || !SAFE_ID_RE11.test(workflowId)) {
15958
16362
  return failure("resume", "workflowId required (safe id)", { operationId: opId, retryable: false });
15959
16363
  }
15960
16364
  try {
@@ -15972,9 +16376,9 @@ function createResumeTool(deps) {
15972
16376
  }
15973
16377
 
15974
16378
  // src/tools/ship-status.js
15975
- import { readFile as readFile9, readdir as readdir7 } from "node:fs/promises";
15976
- import { existsSync as existsSync8 } from "node:fs";
15977
- import { join as join13 } from "node:path";
16379
+ import { readFile as readFile11, readdir as readdir7 } from "node:fs/promises";
16380
+ import { existsSync as existsSync10 } from "node:fs";
16381
+ import { join as join17 } from "node:path";
15978
16382
  function createStatusTool(deps) {
15979
16383
  return async function status(input) {
15980
16384
  const opId = input.operationId ?? `status-${Date.now().toString(36)}`;
@@ -15982,21 +16386,21 @@ function createStatusTool(deps) {
15982
16386
  if (!workflowId) return failure("status", "workflowId required", { operationId: opId, retryable: false });
15983
16387
  try {
15984
16388
  const commonDir = await resolveGitCommonDir(deps.repoRoot);
15985
- const planRoot = join13(opencodeShipStateDir(commonDir), "plans", workflowId);
15986
- const runRoot = join13(opencodeShipStateDir(commonDir), "runs", workflowId);
15987
- const indexPath = join13(planRoot, "index.json");
15988
- if (!existsSync8(indexPath)) return failure("status", "no workflow record", { operationId: opId, retryable: false });
15989
- const index = JSON.parse(await readFile9(indexPath, "utf8"));
16389
+ const planRoot = join17(opencodeShipStateDir(commonDir), "plans", workflowId);
16390
+ const runRoot = join17(opencodeShipStateDir(commonDir), "runs", workflowId);
16391
+ const indexPath = join17(planRoot, "index.json");
16392
+ if (!existsSync10(indexPath)) return failure("status", "no workflow record", { operationId: opId, retryable: false });
16393
+ const index = JSON.parse(await readFile11(indexPath, "utf8"));
15990
16394
  let run = null;
15991
- const runPath = join13(runRoot, "run.json");
15992
- if (existsSync8(runPath)) run = JSON.parse(await readFile9(runPath, "utf8"));
16395
+ const runPath = join17(runRoot, "run.json");
16396
+ if (existsSync10(runPath)) run = JSON.parse(await readFile11(runPath, "utf8"));
15993
16397
  let lastEvent = null;
15994
- const eventsDir = join13(runRoot, "events");
15995
- if (existsSync8(eventsDir)) {
16398
+ const eventsDir = join17(runRoot, "events");
16399
+ if (existsSync10(eventsDir)) {
15996
16400
  const events = await readdir7(eventsDir);
15997
16401
  const sorted = events.filter((n) => n.endsWith(".json")).sort();
15998
16402
  if (sorted.length > 0) {
15999
- lastEvent = JSON.parse(await readFile9(join13(eventsDir, sorted[sorted.length - 1]), "utf8"));
16403
+ lastEvent = JSON.parse(await readFile11(join17(eventsDir, sorted[sorted.length - 1]), "utf8"));
16000
16404
  }
16001
16405
  }
16002
16406
  return success2("status", { workflowId, index, run, lastEvent }, { operationId: opId });
@@ -16006,6 +16410,537 @@ function createStatusTool(deps) {
16006
16410
  };
16007
16411
  }
16008
16412
 
16413
+ // src/skills/registry.js
16414
+ import { spawn as spawn7 } from "node:child_process";
16415
+ import { readFile as readFile12, writeFile as writeFile10, mkdir as mkdir16 } from "node:fs/promises";
16416
+ import { resolve as resolve11, dirname as dirname7 } from "node:path";
16417
+ import { createHash as createHash9 } from "node:crypto";
16418
+
16419
+ // src/tools/skill-discovery.js
16420
+ import { spawn as spawn6 } from "node:child_process";
16421
+ import { existsSync as existsSync11, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from "node:fs";
16422
+ import { dirname as dirname6, join as join18, normalize, resolve as resolve10, sep } from "node:path";
16423
+ var DEFAULT_TRUSTED_OWNERS = Object.freeze([
16424
+ "vercel-labs",
16425
+ "anthropics",
16426
+ "obra",
16427
+ "mattpocock",
16428
+ "ComposioHQ"
16429
+ ]);
16430
+ function runCapture(cmd, args, options) {
16431
+ const cwd = options?.cwd;
16432
+ const timeoutMs = options?.timeoutMs ?? 6e4;
16433
+ return new Promise((resolveP, rejectP) => {
16434
+ const child = spawn6(cmd, args, { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
16435
+ let stdout = "";
16436
+ let stderr = "";
16437
+ const timer = setTimeout(() => {
16438
+ child.kill("SIGKILL");
16439
+ rejectP(new Error(`skill-discovery: timeout running '${cmd} ${args.join(" ")}'`));
16440
+ }, timeoutMs);
16441
+ child.stdout.on("data", (chunk) => {
16442
+ stdout += chunk.toString("utf8");
16443
+ });
16444
+ child.stderr.on("data", (chunk) => {
16445
+ stderr += chunk.toString("utf8");
16446
+ });
16447
+ child.on("error", (err) => {
16448
+ clearTimeout(timer);
16449
+ rejectP(err);
16450
+ });
16451
+ child.on("close", (code) => {
16452
+ clearTimeout(timer);
16453
+ resolveP({ code, stdout, stderr });
16454
+ });
16455
+ });
16456
+ }
16457
+ async function discoverSkills({ repoRoot, query, npmBin = "npx" }) {
16458
+ if (!repoRoot || !query) {
16459
+ return { ok: false, error: { kind: "missing-args" } };
16460
+ }
16461
+ const r = await runCapture(npmBin, ["skills", "find", query], { cwd: repoRoot, timeoutMs: 6e4 });
16462
+ if (r.code !== 0 && !r.stdout.trim()) {
16463
+ return { ok: false, error: { kind: "registry-unavailable", stderr: r.stderr } };
16464
+ }
16465
+ return { ok: true, candidates: parseFindOutput(r.stdout), raw: r.stdout };
16466
+ }
16467
+ function parseFindOutput(text) {
16468
+ const lines = text.split(/\r?\n/);
16469
+ const candidates = [];
16470
+ for (const line of lines) {
16471
+ const match = line.match(/^\s*([a-zA-Z0-9_.\-]+)\s+([a-zA-Z0-9_.\-/]+)\s+([0-9]+)\s*$/);
16472
+ if (!match) continue;
16473
+ candidates.push({
16474
+ skill: match[1],
16475
+ package: match[2],
16476
+ installs: Number.parseInt(match[3], 10)
16477
+ });
16478
+ }
16479
+ return candidates;
16480
+ }
16481
+
16482
+ // src/skills/registry.js
16483
+ var SKILLS_CLI_TIMEOUT_MS = 60 * 1e3;
16484
+ var SKILLS_INSTALL_TIMEOUT_MS = 120 * 1e3;
16485
+ function runCapture2(cmd, args, options = {}) {
16486
+ const cwd = options.cwd;
16487
+ const timeoutMs = options.timeoutMs ?? 6e4;
16488
+ const stdin = options.stdin;
16489
+ return new Promise((resolveP, rejectP) => {
16490
+ const child = spawn7(cmd, args, { cwd, shell: false, stdio: ["pipe", "pipe", "pipe"] });
16491
+ let stdout = "";
16492
+ let stderr = "";
16493
+ const timer = setTimeout(() => {
16494
+ child.kill("SIGKILL");
16495
+ rejectP(new Error(`skill-registry: timeout running '${cmd} ${args.join(" ")}'`));
16496
+ }, timeoutMs);
16497
+ child.stdout.on("data", (chunk) => {
16498
+ stdout += chunk.toString("utf8");
16499
+ });
16500
+ child.stderr.on("data", (chunk) => {
16501
+ stderr += chunk.toString("utf8");
16502
+ });
16503
+ child.on("error", (err) => {
16504
+ clearTimeout(timer);
16505
+ rejectP(err);
16506
+ });
16507
+ child.on("close", (code) => {
16508
+ clearTimeout(timer);
16509
+ resolveP({ code, stdout, stderr });
16510
+ });
16511
+ if (stdin !== void 0) {
16512
+ child.stdin.write(stdin);
16513
+ child.stdin.end();
16514
+ }
16515
+ });
16516
+ }
16517
+ async function listSkills({ repoRoot, query, npmBin = "npx" }) {
16518
+ return discoverSkills({ repoRoot, query, npmBin });
16519
+ }
16520
+ async function fetchSkillBytes({ repoRoot, packageSpec, npmBin = "npx" }) {
16521
+ const r = await runCapture2(npmBin, ["view", packageSpec, "dist.tarball"], {
16522
+ cwd: repoRoot,
16523
+ timeoutMs: SKILLS_CLI_TIMEOUT_MS
16524
+ });
16525
+ if (r.code !== 0) {
16526
+ return { ok: false, error: { kind: "registry-unavailable", stderr: r.stderr } };
16527
+ }
16528
+ const tarball = r.stdout.trim();
16529
+ if (!/^https?:\/\//.test(tarball)) {
16530
+ return { ok: false, error: { kind: "bad-tarball-url", tarball } };
16531
+ }
16532
+ return { ok: true, tarball };
16533
+ }
16534
+
16535
+ // src/skills/policy.js
16536
+ import { readFile as readFile13, writeFile as writeFile11 } from "node:fs/promises";
16537
+ import { existsSync as existsSync12 } from "node:fs";
16538
+ import { resolve as resolve12 } from "node:path";
16539
+ var DEFAULT_TRUSTED_OWNERS2 = Object.freeze([
16540
+ "vercel-labs",
16541
+ "anthropics",
16542
+ "obra",
16543
+ "mattpocock",
16544
+ "ComposioHQ"
16545
+ ]);
16546
+ var DEFAULT_MIN_INSTALLS = 1e3;
16547
+ var MAX_TRUSTED_PER_RUN = 5;
16548
+ var POLICY_PATH = ".opencode/ship.skills.policy.json";
16549
+ function policyPath(repoRoot) {
16550
+ return resolve12(repoRoot, POLICY_PATH);
16551
+ }
16552
+ function defaultPolicy() {
16553
+ return {
16554
+ trustedOwners: [...DEFAULT_TRUSTED_OWNERS2],
16555
+ minInstalls: DEFAULT_MIN_INSTALLS,
16556
+ blocklist: [],
16557
+ maxTrustedPerRun: MAX_TRUSTED_PER_RUN
16558
+ };
16559
+ }
16560
+ async function readPolicy(repoRoot) {
16561
+ const path = policyPath(repoRoot);
16562
+ if (!existsSync12(path)) return defaultPolicy();
16563
+ try {
16564
+ const raw = await readFile13(path, "utf8");
16565
+ const parsed = JSON.parse(raw);
16566
+ return mergePolicy(defaultPolicy(), parsed);
16567
+ } catch {
16568
+ return defaultPolicy();
16569
+ }
16570
+ }
16571
+ function mergePolicy(base, override) {
16572
+ const out = { ...base };
16573
+ if (Array.isArray(override?.trustedOwners)) {
16574
+ out.trustedOwners = [...new Set(override.trustedOwners)];
16575
+ }
16576
+ if (Number.isInteger(override?.minInstalls)) {
16577
+ out.minInstalls = override.minInstalls;
16578
+ }
16579
+ if (Array.isArray(override?.blocklist)) {
16580
+ out.blocklist = [...new Set(override.blocklist)];
16581
+ }
16582
+ if (Number.isInteger(override?.maxTrustedPerRun)) {
16583
+ out.maxTrustedPerRun = override.maxTrustedPerRun;
16584
+ }
16585
+ return out;
16586
+ }
16587
+ function isAutoInstallable(candidate, policy) {
16588
+ if (!candidate || typeof candidate !== "object") {
16589
+ return { ok: false, reason: "missing-candidate" };
16590
+ }
16591
+ if ((policy.blocklist ?? []).includes(candidate.package)) {
16592
+ return { ok: false, reason: "blocked" };
16593
+ }
16594
+ const owner = String(candidate.package).split("/")[0];
16595
+ if (!(policy.trustedOwners ?? []).includes(owner)) {
16596
+ return { ok: false, reason: "untrusted-owner" };
16597
+ }
16598
+ if (candidate.installs < (policy.minInstalls ?? DEFAULT_MIN_INSTALLS)) {
16599
+ return { ok: false, reason: "below-threshold" };
16600
+ }
16601
+ return { ok: true };
16602
+ }
16603
+
16604
+ // src/tools/ship-skill-discover.js
16605
+ function createSkillDiscoverTool(deps) {
16606
+ return async function skillDiscover(input) {
16607
+ const opId = input.operationId ?? `skill-discover-${Date.now().toString(36)}`;
16608
+ const query = String(input.query ?? "");
16609
+ if (!query) {
16610
+ return failure("skill-discover", "query required", { operationId: opId, retryable: false });
16611
+ }
16612
+ let policy;
16613
+ try {
16614
+ policy = await readPolicy(deps.repoRoot);
16615
+ } catch (err) {
16616
+ return failure("skill-discover", `policy unreadable: ${err?.message ?? err}`, { operationId: opId, retryable: false });
16617
+ }
16618
+ let result;
16619
+ try {
16620
+ result = await listSkills({ repoRoot: deps.repoRoot, query });
16621
+ } catch (err) {
16622
+ return failure("skill-discover", `registry unavailable: ${err?.message ?? err}`, { operationId: opId, retryable: true });
16623
+ }
16624
+ if (!result.ok) {
16625
+ return failure("skill-discover", result.error?.kind ?? "registry-unavailable", { operationId: opId, retryable: true });
16626
+ }
16627
+ const auto = [];
16628
+ const needsApproval = [];
16629
+ let autoCount = 0;
16630
+ for (const candidate of result.candidates ?? []) {
16631
+ if (policy.blocklist.includes(candidate.package)) continue;
16632
+ const decision = isAutoInstallable(candidate, policy);
16633
+ if (decision.ok && autoCount < policy.maxTrustedPerRun) {
16634
+ auto.push(candidate);
16635
+ autoCount += 1;
16636
+ } else {
16637
+ needsApproval.push({ ...candidate, reason: decision.reason ?? "needs-approval" });
16638
+ }
16639
+ }
16640
+ return success2("skill-discover", {
16641
+ query,
16642
+ policy,
16643
+ auto,
16644
+ needsApproval,
16645
+ total: (result.candidates ?? []).length
16646
+ }, { operationId: opId });
16647
+ };
16648
+ }
16649
+
16650
+ // src/tools/ship-skill-install.js
16651
+ import { execFile as execFile2 } from "node:child_process";
16652
+ import { readFile as readFile15, mkdir as mkdir18, writeFile as writeFile13 } from "node:fs/promises";
16653
+ import { existsSync as existsSync14 } from "node:fs";
16654
+ import { resolve as resolve14, join as join19, dirname as dirname9, sep as sep2 } from "node:path";
16655
+ import { createHash as createHash11 } from "node:crypto";
16656
+
16657
+ // src/skills/inventory.js
16658
+ import { readFile as readFile14, writeFile as writeFile12, mkdir as mkdir17, rename as rename4 } from "node:fs/promises";
16659
+ import { existsSync as existsSync13 } from "node:fs";
16660
+ import { resolve as resolve13, dirname as dirname8 } from "node:path";
16661
+ import { createHash as createHash10 } from "node:crypto";
16662
+ var INVENTORY_PATH = ".opencode/ship.skills.lock.json";
16663
+ function inventoryPath(repoRoot) {
16664
+ return resolve13(repoRoot, INVENTORY_PATH);
16665
+ }
16666
+ async function readInventory(repoRoot) {
16667
+ const path = inventoryPath(repoRoot);
16668
+ if (!existsSync13(path)) {
16669
+ return { schemaVersion: 1, entries: [] };
16670
+ }
16671
+ try {
16672
+ const raw = await readFile14(path, "utf8");
16673
+ const parsed = JSON.parse(raw);
16674
+ if (!parsed || typeof parsed !== "object") {
16675
+ return { schemaVersion: 1, entries: [] };
16676
+ }
16677
+ if (!Array.isArray(parsed.entries)) parsed.entries = [];
16678
+ if (!Number.isInteger(parsed.schemaVersion)) parsed.schemaVersion = 1;
16679
+ return parsed;
16680
+ } catch {
16681
+ return { schemaVersion: 1, entries: [] };
16682
+ }
16683
+ }
16684
+ async function writeInventory(repoRoot, inventory) {
16685
+ const path = inventoryPath(repoRoot);
16686
+ await mkdir17(dirname8(path), { recursive: true });
16687
+ const tmp = `${path}.${Date.now().toString(36)}.tmp`;
16688
+ await writeFile12(tmp, JSON.stringify(inventory, null, 2) + "\n", "utf8");
16689
+ await rename4(tmp, path);
16690
+ return path;
16691
+ }
16692
+ function canonicalize3(value) {
16693
+ const seen = /* @__PURE__ */ new WeakSet();
16694
+ const sort = (v) => {
16695
+ if (v === null || typeof v !== "object") return v;
16696
+ if (seen.has(v)) return null;
16697
+ seen.add(v);
16698
+ if (Array.isArray(v)) return v.map(sort);
16699
+ const out = {};
16700
+ for (const k of Object.keys(v).sort()) out[k] = sort(v[k]);
16701
+ return out;
16702
+ };
16703
+ return JSON.stringify(sort(value));
16704
+ }
16705
+ function hashEntry(entry) {
16706
+ return createHash10("sha256").update(canonicalize3(entry), "utf8").digest("hex");
16707
+ }
16708
+ async function appendEntry(repoRoot, entry) {
16709
+ const inventory = await readInventory(repoRoot);
16710
+ const previousHash = inventory.entries.length > 0 ? inventory.entries[inventory.entries.length - 1].hash : "0".repeat(64);
16711
+ const stamped = {
16712
+ ...entry,
16713
+ sequence: inventory.entries.length + 1,
16714
+ previousHash,
16715
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString()
16716
+ };
16717
+ stamped.hash = hashEntry({ ...stamped, hash: void 0 });
16718
+ inventory.entries.push(stamped);
16719
+ await writeInventory(repoRoot, inventory);
16720
+ return stamped;
16721
+ }
16722
+ async function verifyInventory(repoRoot) {
16723
+ const inventory = await readInventory(repoRoot);
16724
+ let prev = "0".repeat(64);
16725
+ for (const entry of inventory.entries) {
16726
+ if (entry.previousHash !== prev) {
16727
+ return { ok: false, reason: "chain-break", entry: entry.sequence };
16728
+ }
16729
+ const recomputed = hashEntry({ ...entry, hash: void 0 });
16730
+ if (recomputed !== entry.hash) {
16731
+ return { ok: false, reason: "hash-mismatch", entry: entry.sequence };
16732
+ }
16733
+ prev = entry.hash;
16734
+ }
16735
+ return { ok: true, count: inventory.entries.length };
16736
+ }
16737
+
16738
+ // src/tools/ship-skill-install.js
16739
+ var SAFE_ID_RE12 = /^[A-Za-z0-9._-]{1,128}$/;
16740
+ function isInside(parent, child) {
16741
+ const a = resolve14(parent) + sep2;
16742
+ const b = resolve14(child);
16743
+ return b.startsWith(a) || b === resolve14(parent);
16744
+ }
16745
+ function createSkillInstallTool(deps) {
16746
+ return async function skillInstall(input) {
16747
+ const opId = input.operationId ?? `skill-install-${Date.now().toString(36)}`;
16748
+ const packageSpec = String(input.package ?? "");
16749
+ const worktreePath = String(input.worktreePath ?? "");
16750
+ const skillName = String(input.skillName ?? "");
16751
+ const version2 = String(input.version ?? "latest");
16752
+ if (!packageSpec || !/^[A-Za-z0-9._/@-]+$/.test(packageSpec)) {
16753
+ return failure("skill-install", "package required (safe npm spec)", { operationId: opId, retryable: false });
16754
+ }
16755
+ if (!worktreePath) {
16756
+ return failure("skill-install", "worktreePath required (must be the active issue worktree)", { operationId: opId, retryable: false });
16757
+ }
16758
+ if (!skillName || !SAFE_ID_RE12.test(skillName)) {
16759
+ return failure("skill-install", "skillName required (safe id)", { operationId: opId, retryable: false });
16760
+ }
16761
+ const policy = await readPolicy(deps.repoRoot);
16762
+ const candidate = { package: packageSpec, skill: skillName, installs: policy.minInstalls + 1 };
16763
+ const decision = isAutoInstallable(candidate, policy);
16764
+ if (!decision.ok) {
16765
+ return failure("skill-install", `policy forbids install: ${decision.reason}`, { operationId: opId, retryable: false });
16766
+ }
16767
+ const wt = resolve14(worktreePath);
16768
+ const mainRepo = resolve14(deps.repoRoot);
16769
+ if (wt === mainRepo) {
16770
+ return failure("skill-install", "installs into the main worktree are forbidden", { operationId: opId, retryable: false });
16771
+ }
16772
+ if (!isInside(mainRepo, wt) && !isInside(wt, mainRepo)) {
16773
+ return failure("skill-install", "worktreePath must be inside the active repository", { operationId: opId, retryable: false });
16774
+ }
16775
+ const destDir = resolve14(wt, ".opencode", "skills", skillName);
16776
+ if (existsSync14(destDir)) {
16777
+ return failure("skill-install", "destination already exists; use ship_skill_audit to detect drift", { operationId: opId, retryable: false });
16778
+ }
16779
+ const managedCatalog = (deps.config?.value?.skills ?? []).map((s) => s?.name).filter(Boolean);
16780
+ if (managedCatalog.includes(skillName)) {
16781
+ return failure("skill-install", "candidate shadows a managed skill", { operationId: opId, retryable: false });
16782
+ }
16783
+ let resolved;
16784
+ try {
16785
+ resolved = await fetchSkillBytes({ repoRoot: deps.repoRoot, packageSpec: `${packageSpec}@${version2}` });
16786
+ } catch (err) {
16787
+ return failure("skill-install", `registry fetch failed: ${err?.message ?? err}`, { operationId: opId, retryable: true });
16788
+ }
16789
+ if (!resolved.ok) {
16790
+ return failure("skill-install", resolved.error?.kind ?? "registry-unavailable", { operationId: opId, retryable: true });
16791
+ }
16792
+ let fetched;
16793
+ try {
16794
+ const tarBytes = await fetchTarball(resolved.tarball);
16795
+ fetched = { tarball: resolved.tarball, sha256: createHash11("sha256").update(tarBytes).digest("hex"), bytes: tarBytes };
16796
+ } catch (err) {
16797
+ return failure("skill-install", `tarball download failed: ${err?.message ?? err}`, { operationId: opId, retryable: true });
16798
+ }
16799
+ try {
16800
+ await mkdir18(destDir, { recursive: true });
16801
+ await writeFile13(join19(destDir, "SKILL.md"), `# Trusted-installed skill
16802
+
16803
+ package: ${packageSpec}
16804
+ install: pinned@${version2}
16805
+ tarball: ${fetched.tarball}
16806
+ sha256: ${fetched.sha256}
16807
+ worktree: ${wt}
16808
+ recordedAt: ${(/* @__PURE__ */ new Date()).toISOString()}
16809
+ `, "utf8");
16810
+ const recorded = await appendEntry(deps.repoRoot, {
16811
+ skill: skillName,
16812
+ package: packageSpec,
16813
+ version: version2,
16814
+ worktreePath: wt,
16815
+ tarball: fetched.tarball,
16816
+ sha256: fetched.sha256,
16817
+ destDir,
16818
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString()
16819
+ });
16820
+ return success2("skill-install", {
16821
+ skill: skillName,
16822
+ package: packageSpec,
16823
+ version: version2,
16824
+ destDir,
16825
+ sha256: fetched.sha256,
16826
+ sequence: recorded.sequence
16827
+ }, { operationId: opId });
16828
+ } catch (err) {
16829
+ return failure("skill-install", String(err?.message ?? err), { operationId: opId, retryable: true });
16830
+ }
16831
+ };
16832
+ }
16833
+ async function fetchTarball(url2) {
16834
+ const { request } = await import("node:https");
16835
+ const { request: httpRequest } = await import("node:http");
16836
+ const { URL: URL2 } = await import("node:url");
16837
+ const parsed = new URL2(url2);
16838
+ const lib = parsed.protocol === "https:" ? request : httpRequest;
16839
+ return new Promise((resolveP, rejectP) => {
16840
+ const req = lib(url2, { method: "GET" }, (res) => {
16841
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
16842
+ resolveP(fetchTarball(res.headers.location));
16843
+ return;
16844
+ }
16845
+ if (res.statusCode !== 200) {
16846
+ rejectP(new Error(`tarball fetch returned HTTP ${res.statusCode}`));
16847
+ return;
16848
+ }
16849
+ const chunks = [];
16850
+ res.on("data", (c) => chunks.push(c));
16851
+ res.on("end", () => resolveP(Buffer.concat(chunks)));
16852
+ res.on("error", rejectP);
16853
+ });
16854
+ req.on("error", rejectP);
16855
+ req.end();
16856
+ });
16857
+ }
16858
+
16859
+ // src/tools/ship-skill-audit.js
16860
+ import { readdir as readdir8, readFile as readFile16 } from "node:fs/promises";
16861
+ import { existsSync as existsSync15 } from "node:fs";
16862
+ import { resolve as resolve15, join as join20 } from "node:path";
16863
+ import { createHash as createHash12 } from "node:crypto";
16864
+ function createSkillAuditTool(deps) {
16865
+ return async function skillAudit(input) {
16866
+ const opId = input.operationId ?? `skill-audit-${Date.now().toString(36)}`;
16867
+ const repoRoot = resolve15(deps.repoRoot);
16868
+ const inventory = await readInventory(repoRoot);
16869
+ const chain = await verifyInventory(repoRoot);
16870
+ const missing = [];
16871
+ const drifted = [];
16872
+ const colliding = [];
16873
+ for (const entry of inventory.entries) {
16874
+ const skillPath = join20(repoRoot, entry.destDir ?? "", "SKILL.md");
16875
+ if (!existsSync15(skillPath)) {
16876
+ missing.push({ skill: entry.skill, sequence: entry.sequence });
16877
+ continue;
16878
+ }
16879
+ const raw = await readFile16(skillPath, "utf8");
16880
+ const sha = createHash12("sha256").update(raw, "utf8").digest("hex");
16881
+ const expected = entry.sha256 ?? entry.hash;
16882
+ if (sha !== expected) {
16883
+ drifted.push({ skill: entry.skill, sequence: entry.sequence, expected, actual: sha });
16884
+ }
16885
+ }
16886
+ const untracked = [];
16887
+ const opencodeDir = join20(repoRoot, ".opencode", "skills");
16888
+ if (existsSync15(opencodeDir)) {
16889
+ const entries = await readdir8(opencodeDir, { withFileTypes: true }).catch(() => []);
16890
+ for (const e of entries) {
16891
+ if (!e.isDirectory()) continue;
16892
+ const recorded = inventory.entries.find((en) => en.skill === e.name);
16893
+ if (!recorded) {
16894
+ untracked.push({ skill: e.name });
16895
+ }
16896
+ }
16897
+ }
16898
+ return success2("skill-audit", {
16899
+ chain,
16900
+ missing,
16901
+ drifted,
16902
+ untracked,
16903
+ colliding,
16904
+ total: inventory.entries.length
16905
+ }, { operationId: opId });
16906
+ };
16907
+ }
16908
+
16909
+ // src/tools/ship-skill-uninstall.js
16910
+ import { readFile as readFile17, unlink as unlink5, writeFile as writeFile14 } from "node:fs/promises";
16911
+ import { existsSync as existsSync16 } from "node:fs";
16912
+ import { resolve as resolve16, join as join21 } from "node:path";
16913
+ import { createHash as createHash13 } from "node:crypto";
16914
+ var SAFE_ID_RE13 = /^[A-Za-z0-9._-]{1,128}$/;
16915
+ function createSkillUninstallTool(deps) {
16916
+ return async function skillUninstall(input) {
16917
+ const opId = input.operationId ?? `skill-uninstall-${Date.now().toString(36)}`;
16918
+ const skillName = String(input.skill ?? "");
16919
+ if (!skillName || !SAFE_ID_RE13.test(skillName)) {
16920
+ return failure("skill-uninstall", "skill required (safe id)", { operationId: opId, retryable: false });
16921
+ }
16922
+ const repoRoot = resolve16(deps.repoRoot);
16923
+ const inventory = await readInventory(repoRoot);
16924
+ const idx = inventory.entries.findIndex((e) => e.skill === skillName);
16925
+ if (idx === -1) {
16926
+ return failure("skill-uninstall", "skill not in inventory", { operationId: opId, retryable: false });
16927
+ }
16928
+ const entry = inventory.entries[idx];
16929
+ const target = join21(repoRoot, entry.destDir ?? "", "SKILL.md");
16930
+ if (existsSync16(target)) {
16931
+ const raw = await readFile17(target, "utf8");
16932
+ const sha = createHash13("sha256").update(raw, "utf8").digest("hex");
16933
+ if (sha !== (entry.sha256 ?? entry.hash)) {
16934
+ return failure("skill-uninstall", "skill file drifted; refusing to remove. Resolve manually.", { operationId: opId, retryable: false });
16935
+ }
16936
+ await unlink5(target).catch(() => null);
16937
+ }
16938
+ inventory.entries.splice(idx, 1);
16939
+ await writeInventory(repoRoot, inventory);
16940
+ return success2("skill-uninstall", { skill: skillName, removed: true }, { operationId: opId });
16941
+ };
16942
+ }
16943
+
16009
16944
  // src/recovery.js
16010
16945
  function recoverManifestAfterCrash(manifest) {
16011
16946
  return manifest;
@@ -16022,9 +16957,9 @@ async function reconcileOwner(repoRoot, adapter) {
16022
16957
  }
16023
16958
 
16024
16959
  // src/installer/config.js
16025
- import { readFile as readFile10, writeFile as writeFile9, rename as rename3, mkdir as mkdir11 } from "node:fs/promises";
16026
- import { existsSync as existsSync9 } from "node:fs";
16027
- import { dirname as dirname5, resolve as resolve9 } from "node:path";
16960
+ import { readFile as readFile18, writeFile as writeFile15, rename as rename5, mkdir as mkdir19 } from "node:fs/promises";
16961
+ import { existsSync as existsSync17 } from "node:fs";
16962
+ import { dirname as dirname10, resolve as resolve17 } from "node:path";
16028
16963
 
16029
16964
  // schema/ship-config.schema.json
16030
16965
  var ship_config_schema_default = {
@@ -16329,23 +17264,14 @@ function validateSchema(value, schema) {
16329
17264
  return { ok: issues.length === 0, issues };
16330
17265
  }
16331
17266
 
16332
- // src/installer/hash.js
16333
- import { createHash as createHash8 } from "node:crypto";
16334
- function bytesHash(buffer) {
16335
- return createHash8("sha256").update(buffer).digest("hex");
16336
- }
16337
- function bytesHashString(text) {
16338
- return bytesHash(Buffer.from(text, "utf8"));
16339
- }
16340
-
16341
17267
  // src/installer/config.js
16342
17268
  function configPath(repoRoot) {
16343
- return resolve9(repoRoot, ".opencode", "ship.config.json");
17269
+ return resolve17(repoRoot, ".opencode", "ship.config.json");
16344
17270
  }
16345
17271
  async function loadConfig(repoRoot) {
16346
17272
  const path = configPath(repoRoot);
16347
- if (!existsSync9(path)) return null;
16348
- const raw = await readFile10(path, "utf8");
17273
+ if (!existsSync17(path)) return null;
17274
+ const raw = await readFile18(path, "utf8");
16349
17275
  let parsed;
16350
17276
  try {
16351
17277
  parsed = JSON.parse(raw);
@@ -16409,28 +17335,10 @@ function renderDefaultConfig(detection, overrides = {}) {
16409
17335
  };
16410
17336
  }
16411
17337
 
16412
- // src/installer/lock.js
16413
- import { readFile as readFile11, writeFile as writeFile10, rename as rename4, mkdir as mkdir12 } from "node:fs/promises";
16414
- import { existsSync as existsSync10 } from "node:fs";
16415
- import { dirname as dirname6, resolve as resolve10 } from "node:path";
16416
- function lockPath(repoRoot) {
16417
- return resolve10(repoRoot, ".opencode", "ship.lock.json");
16418
- }
16419
- async function readLock2(repoRoot) {
16420
- const path = lockPath(repoRoot);
16421
- if (!existsSync10(path)) return null;
16422
- try {
16423
- const raw = await readFile11(path, "utf8");
16424
- return JSON.parse(raw);
16425
- } catch {
16426
- return null;
16427
- }
16428
- }
16429
-
16430
17338
  // src/installer/detection/project.js
16431
17339
  import { spawnSync as spawnSync5 } from "node:child_process";
16432
- import { existsSync as existsSync11, readFileSync } from "node:fs";
16433
- import { resolve as resolve11, join as join14 } from "node:path";
17340
+ import { existsSync as existsSync18, readFileSync as readFileSync2 } from "node:fs";
17341
+ import { resolve as resolve18, join as join22 } from "node:path";
16434
17342
  function runGit2(cwd, args) {
16435
17343
  const r = spawnSync5("git", ["-C", cwd, ...args], {
16436
17344
  stdio: ["ignore", "pipe", "pipe"],
@@ -16439,17 +17347,17 @@ function runGit2(cwd, args) {
16439
17347
  return { status: r.status ?? -1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
16440
17348
  }
16441
17349
  function detectPackageManager(repoRoot) {
16442
- if (existsSync11(join14(repoRoot, "pnpm-lock.yaml"))) return "pnpm";
16443
- if (existsSync11(join14(repoRoot, "yarn.lock"))) return "yarn";
16444
- if (existsSync11(join14(repoRoot, "bun.lockb"))) return "bun";
16445
- if (existsSync11(join14(repoRoot, "package-lock.json"))) return "npm";
17350
+ if (existsSync18(join22(repoRoot, "pnpm-lock.yaml"))) return "pnpm";
17351
+ if (existsSync18(join22(repoRoot, "yarn.lock"))) return "yarn";
17352
+ if (existsSync18(join22(repoRoot, "bun.lockb"))) return "bun";
17353
+ if (existsSync18(join22(repoRoot, "package-lock.json"))) return "npm";
16446
17354
  return null;
16447
17355
  }
16448
17356
  function readPackageJson(repoRoot) {
16449
- const path = join14(repoRoot, "package.json");
16450
- if (!existsSync11(path)) return null;
17357
+ const path = join22(repoRoot, "package.json");
17358
+ if (!existsSync18(path)) return null;
16451
17359
  try {
16452
- return JSON.parse(readFileSync(path, "utf8"));
17360
+ return JSON.parse(readFileSync2(path, "utf8"));
16453
17361
  } catch {
16454
17362
  return null;
16455
17363
  }
@@ -16524,7 +17432,7 @@ function detectOwner(repoRoot) {
16524
17432
  }
16525
17433
  function detectProject(repoRoot = process.cwd()) {
16526
17434
  const errors = [];
16527
- const cwd = resolve11(repoRoot);
17435
+ const cwd = resolve18(repoRoot);
16528
17436
  const inside = runGit2(cwd, ["rev-parse", "--show-toplevel"]);
16529
17437
  if (inside.status !== 0) {
16530
17438
  errors.push({ kind: "not-a-git-repo", path: cwd, detail: inside.stderr.trim() });
@@ -16573,8 +17481,8 @@ function detectProject(repoRoot = process.cwd()) {
16573
17481
  // src/installer/cleanup.js
16574
17482
  import { spawnSync as spawnSync6 } from "node:child_process";
16575
17483
  import { resolve as pathResolve } from "node:path";
16576
- import { existsSync as existsSync12, readFileSync as readFileSync2, writeFileSync, mkdirSync } from "node:fs";
16577
- function spawn5(repoRoot, args) {
17484
+ import { existsSync as existsSync19, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "node:fs";
17485
+ function spawn8(repoRoot, args) {
16578
17486
  const r = spawnSync6("git", ["-C", repoRoot, ...args], { encoding: "utf8" });
16579
17487
  return { status: r.status ?? -1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
16580
17488
  }
@@ -16585,10 +17493,10 @@ function casDeleteBranch2(repoRoot, branch, expectedSha) {
16585
17493
  } else {
16586
17494
  argv.push(`refs/heads/${branch}`);
16587
17495
  }
16588
- return spawn5(repoRoot, argv).status ?? -1;
17496
+ return spawn8(repoRoot, argv).status ?? -1;
16589
17497
  }
16590
17498
  function safeRemoveWorktree2(repoRoot, target) {
16591
- const r = spawn5(repoRoot, ["worktree", "remove", target]);
17499
+ const r = spawn8(repoRoot, ["worktree", "remove", target]);
16592
17500
  return { status: r.status, stderr: r.stderr };
16593
17501
  }
16594
17502
  function worktreeRootOf(adapter) {
@@ -16600,9 +17508,9 @@ async function cleanupPendingPath(repoRoot) {
16600
17508
  }
16601
17509
  async function loadCleanupPending(repoRoot) {
16602
17510
  const path = await cleanupPendingPath(repoRoot);
16603
- if (!existsSync12(path)) return [];
17511
+ if (!existsSync19(path)) return [];
16604
17512
  try {
16605
- const raw = await readFileSync2(path, "utf8");
17513
+ const raw = await readFileSync3(path, "utf8");
16606
17514
  const parsed = JSON.parse(raw);
16607
17515
  return Array.isArray(parsed) ? parsed : [];
16608
17516
  } catch {
@@ -16612,8 +17520,8 @@ async function loadCleanupPending(repoRoot) {
16612
17520
  async function saveCleanupPending(repoRoot, entries) {
16613
17521
  const path = await cleanupPendingPath(repoRoot);
16614
17522
  const dir = pathResolve(path, "..");
16615
- if (!existsSync12(dir)) mkdirSync(dir, { recursive: true });
16616
- writeFileSync(path, JSON.stringify(dedupePending(entries), null, 2) + "\n", "utf8");
17523
+ if (!existsSync19(dir)) mkdirSync2(dir, { recursive: true });
17524
+ writeFileSync2(path, JSON.stringify(dedupePending(entries), null, 2) + "\n", "utf8");
16617
17525
  }
16618
17526
  function dedupePending(entries) {
16619
17527
  const seen = /* @__PURE__ */ new Set();
@@ -16650,11 +17558,11 @@ async function tryImmediateCleanup({ repoRoot, taskId, adapter }) {
16650
17558
  if (!wtPath.startsWith(rootAbs + "/")) {
16651
17559
  return reject("worktree-out-of-root", { expected: rootAbs, got: wtPath });
16652
17560
  }
16653
- const status = spawn5(wtPath, ["status", "--porcelain"]);
17561
+ const status = spawn8(wtPath, ["status", "--porcelain"]);
16654
17562
  if (status.status === 0 && status.stdout.trim().length > 0) return reject("dirty-worktree");
16655
- const rebase = spawn5(wtPath, ["rev-parse", "--verify", "--quiet", "REBASE_HEAD"]);
17563
+ const rebase = spawn8(wtPath, ["rev-parse", "--verify", "--quiet", "REBASE_HEAD"]);
16656
17564
  if (rebase.status === 0) return reject("rebase-in-progress");
16657
- const head = spawn5(wtPath, ["rev-parse", "HEAD"]);
17565
+ const head = spawn8(wtPath, ["rev-parse", "HEAD"]);
16658
17566
  if (head.status !== 0) return reject("no-head");
16659
17567
  const headSha = head.stdout.trim();
16660
17568
  if (m.lastPrHeadSha && headSha !== m.lastPrHeadSha) {
@@ -16671,7 +17579,7 @@ async function tryImmediateCleanup({ repoRoot, taskId, adapter }) {
16671
17579
  return reject("remove-failed", { detail: removed.stderr });
16672
17580
  }
16673
17581
  const branchDelete = casDeleteBranch2(repoRoot, m.branch, headSha);
16674
- const branchStillThere = spawn5(repoRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${m.branch}`]);
17582
+ const branchStillThere = spawn8(repoRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${m.branch}`]);
16675
17583
  if (branchDelete !== 0 && branchStillThere.status === 0) {
16676
17584
  await appendCleanupPending(repoRoot, {
16677
17585
  taskId,
@@ -16700,7 +17608,7 @@ async function listPending(repoRoot) {
16700
17608
  }
16701
17609
 
16702
17610
  // src/installer/ship-adapter.js
16703
- import { resolve as resolve12 } from "node:path";
17611
+ import { resolve as resolve19 } from "node:path";
16704
17612
  var REQUIRED_DEFAULTS = {
16705
17613
  review: { agent: "delivery-reviewer", required: true, invalidateOnHeadChange: true },
16706
17614
  ci: { driver: "github-status-checks", requiredChecks: ["delivery-verify"], wait: true, flakyRetry: 1 },
@@ -16741,10 +17649,10 @@ function flattenShipConfig(ship) {
16741
17649
  }
16742
17650
 
16743
17651
  // src/version.js
16744
- import { readFileSync as readFileSync3, existsSync as existsSync13 } from "node:fs";
16745
- import { dirname as dirname7, resolve as resolve13 } from "node:path";
17652
+ import { readFileSync as readFileSync4, existsSync as existsSync20 } from "node:fs";
17653
+ import { dirname as dirname11, resolve as resolve20 } from "node:path";
16746
17654
  import { fileURLToPath } from "node:url";
16747
- var PACKAGE_VERSION = "1.1.1";
17655
+ var PACKAGE_VERSION = "1.1.2-rc.1";
16748
17656
  var TEMPLATE_SET = `v${PACKAGE_VERSION}`;
16749
17657
 
16750
17658
  // src/plugin.js
@@ -16769,10 +17677,18 @@ var toolDefs = [
16769
17677
  ["ship_plan_submit", "Planner-only immutable PlanV2 submission.", "planSubmit"],
16770
17678
  ["ship_plan_approve", "Interactive approval + immutable local seal.", "planApprove"],
16771
17679
  ["ship_run_start", "Start execution of an approved plan.", "runStart"],
17680
+ ["ship_task_start", "Dispatch a task to the configured builder agent.", "taskStart"],
17681
+ ["ship_task_commit", "Record the immutable commit binding for a reviewed task.", "taskCommit"],
17682
+ ["ship_task_complete", "Advance the run to the next task or to ALL_TASKS_DONE.", "taskComplete"],
16772
17683
  ["ship_task_report", "Builder-only immutable task report.", "taskReport"],
16773
17684
  ["ship_task_review", "Task reviewer Spec/Quality verdict.", "taskReview"],
17685
+ ["ship_final_review", "Record one final review axis (standards or spec).", "finalReview"],
16774
17686
  ["ship_resume", "Restore, reconcile, and continue idempotently.", "resume"],
16775
- ["ship_status", "Read-only compact workflow state.", "status"]
17687
+ ["ship_status", "Read-only compact workflow state.", "status"],
17688
+ ["ship_skill_discover", "Query the trusted skill registry and partition candidates.", "skillDiscover"],
17689
+ ["ship_skill_install", "Install a trusted skill into the active issue worktree.", "skillInstall"],
17690
+ ["ship_skill_audit", "Audit the installed trusted skills inventory.", "skillAudit"],
17691
+ ["ship_skill_uninstall", "Remove a trusted skill whose recorded sha256 still matches.", "skillUninstall"]
16776
17692
  ];
16777
17693
  function wrapEnvelopeV2(id, result) {
16778
17694
  if (result && typeof result === "object" && result.contractVersion === 2) {
@@ -16825,7 +17741,7 @@ async function resolveRepoSlug(repoRoot, detection, config2) {
16825
17741
  const fromConfig = config2?.value?.project?.repository;
16826
17742
  if (typeof fromConfig === "string" && fromConfig.includes("/")) return fromConfig;
16827
17743
  if (detection?.repository) return detection.repository;
16828
- const gitConfig = await readFile12(resolve14(repoRoot, ".git/config"), "utf8").catch(() => null);
17744
+ const gitConfig = await readFile19(resolve21(repoRoot, ".git/config"), "utf8").catch(() => null);
16829
17745
  if (gitConfig) {
16830
17746
  const m = gitConfig.match(/url\s*=\s*.*?github\.com[:/]([^/]+)\/([^/\s]+?)(?:\.git)?\b/);
16831
17747
  if (m) return `${m[1]}/${m[2]}`;
@@ -16857,7 +17773,7 @@ async function bestEffortCleanupQueue(repoRoot, adapter) {
16857
17773
  return { pending, manifestTasks: tasks.map((t) => t.taskId), ...out };
16858
17774
  }
16859
17775
  async function buildRuntime(worktree) {
16860
- const repoRootAbs = resolve14(worktree ?? process.cwd());
17776
+ const repoRootAbs = resolve21(worktree ?? process.cwd());
16861
17777
  const detection = detectProject(repoRootAbs);
16862
17778
  const legacyAdapter = await loadAdapter(repoRootAbs);
16863
17779
  const config2 = await loadConfig(repoRootAbs);
@@ -17115,6 +18031,116 @@ var factories = {
17115
18031
  config: rt.configValue
17116
18032
  })
17117
18033
  },
18034
+ taskStart: {
18035
+ args: {
18036
+ workflowId: tool.schema.string(),
18037
+ taskId: tool.schema.string(),
18038
+ briefHash: tool.schema.string(),
18039
+ sessionID: tool.schema.string(),
18040
+ submittedBy: tool.schema.string(),
18041
+ operationId: tool.schema.string().optional()
18042
+ },
18043
+ build: (rt) => createTaskStartTool({
18044
+ repoRoot: rt.repoRoot,
18045
+ owner: rt.owner,
18046
+ config: rt.configValue
18047
+ })
18048
+ },
18049
+ taskCommit: {
18050
+ args: {
18051
+ workflowId: tool.schema.string(),
18052
+ taskId: tool.schema.string(),
18053
+ expectedHead: tool.schema.string(),
18054
+ commitSha: tool.schema.string(),
18055
+ planHash: tool.schema.string(),
18056
+ reviewHash: tool.schema.string(),
18057
+ round: tool.schema.number(),
18058
+ operationId: tool.schema.string().optional()
18059
+ },
18060
+ build: (rt) => createTaskCommitTool({
18061
+ repoRoot: rt.repoRoot,
18062
+ owner: rt.owner,
18063
+ config: rt.configValue
18064
+ })
18065
+ },
18066
+ taskComplete: {
18067
+ args: {
18068
+ workflowId: tool.schema.string(),
18069
+ taskId: tool.schema.string(),
18070
+ moreTasks: tool.schema.boolean(),
18071
+ nextTaskId: tool.schema.string().optional(),
18072
+ operationId: tool.schema.string().optional()
18073
+ },
18074
+ build: (rt) => createTaskCompleteTool({
18075
+ repoRoot: rt.repoRoot,
18076
+ owner: rt.owner,
18077
+ config: rt.configValue
18078
+ })
18079
+ },
18080
+ finalReview: {
18081
+ args: {
18082
+ workflowId: tool.schema.string(),
18083
+ axis: tool.schema.enum(["standards", "spec"]),
18084
+ verdict: tool.schema.enum(["pass", "fail", "blocked"]),
18085
+ headSha: tool.schema.string(),
18086
+ mergeBaseSha: tool.schema.string(),
18087
+ packageHash: tool.schema.string(),
18088
+ submittedBy: tool.schema.string(),
18089
+ findings: tool.schema.array(tool.schema.unknown()).optional(),
18090
+ operationId: tool.schema.string().optional()
18091
+ },
18092
+ build: (rt) => createFinalReviewTool({
18093
+ repoRoot: rt.repoRoot,
18094
+ owner: rt.owner,
18095
+ config: rt.configValue
18096
+ })
18097
+ },
18098
+ skillDiscover: {
18099
+ args: {
18100
+ query: tool.schema.string(),
18101
+ operationId: tool.schema.string().optional()
18102
+ },
18103
+ build: (rt) => createSkillDiscoverTool({
18104
+ repoRoot: rt.repoRoot,
18105
+ owner: rt.owner,
18106
+ config: rt.configValue
18107
+ })
18108
+ },
18109
+ skillInstall: {
18110
+ args: {
18111
+ package: tool.schema.string(),
18112
+ skillName: tool.schema.string(),
18113
+ worktreePath: tool.schema.string(),
18114
+ version: tool.schema.string().optional(),
18115
+ operationId: tool.schema.string().optional()
18116
+ },
18117
+ build: (rt) => createSkillInstallTool({
18118
+ repoRoot: rt.repoRoot,
18119
+ owner: rt.owner,
18120
+ config: rt.configValue
18121
+ })
18122
+ },
18123
+ skillAudit: {
18124
+ args: {
18125
+ operationId: tool.schema.string().optional()
18126
+ },
18127
+ build: (rt) => createSkillAuditTool({
18128
+ repoRoot: rt.repoRoot,
18129
+ owner: rt.owner,
18130
+ config: rt.configValue
18131
+ })
18132
+ },
18133
+ skillUninstall: {
18134
+ args: {
18135
+ skill: tool.schema.string(),
18136
+ operationId: tool.schema.string().optional()
18137
+ },
18138
+ build: (rt) => createSkillUninstallTool({
18139
+ repoRoot: rt.repoRoot,
18140
+ owner: rt.owner,
18141
+ config: rt.configValue
18142
+ })
18143
+ },
17118
18144
  planSubmit: {
17119
18145
  args: {
17120
18146
  workflowId: tool.schema.string(),
@@ -17190,7 +18216,8 @@ var factories = {
17190
18216
  },
17191
18217
  build: (rt) => createTaskReviewTool({
17192
18218
  repoRoot: rt.repoRoot,
17193
- owner: rt.owner
18219
+ owner: rt.owner,
18220
+ config: rt.configValue
17194
18221
  })
17195
18222
  },
17196
18223
  resume: {