opencode-ship 1.1.1 → 1.1.2-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/assets/commands/setup-ship-workflow.md +9 -167
- package/assets/skills/setup-ship-workflow/SKILL.md +78 -31
- package/dist/cli.js +379 -165
- package/dist/core.js +1 -1
- package/dist/plugin.js +1136 -109
- package/package.json +1 -1
- package/schema/ship-lock.schema.json +7 -18
- package/tests/plugin/expected-tools.mjs +13 -5
- package/tests/plugin/plugin-load.test.mjs +1 -1
- /package/assets/{skills/setup-ship-workflow → _archive}/issue-tracker-gitlab.md +0 -0
- /package/assets/{skills/setup-ship-workflow → _archive}/issue-tracker-local.md +0 -0
package/dist/plugin.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// opencode-ship v1.1.
|
|
1
|
+
// opencode-ship v1.1.2-rc.2
|
|
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
|
|
12432
|
-
import { readFile as
|
|
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((
|
|
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) =>
|
|
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-
|
|
15396
|
-
import { mkdir as
|
|
15397
|
-
import {
|
|
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/
|
|
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 || !
|
|
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 || !
|
|
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 =
|
|
15750
|
-
await
|
|
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(
|
|
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
|
|
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
|
|
15784
|
-
import { mkdir as
|
|
15785
|
-
import { join as
|
|
15786
|
-
var
|
|
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 || !
|
|
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 || !
|
|
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.
|
|
15822
|
-
return failure("task-review", `submittedBy must be the configured
|
|
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 =
|
|
15839
|
-
await
|
|
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.
|
|
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(
|
|
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
|
|
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
|
|
15877
|
-
import { existsSync as
|
|
15878
|
-
import { join as
|
|
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
|
|
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 || !
|
|
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
|
|
15976
|
-
import { existsSync as
|
|
15977
|
-
import { join as
|
|
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 =
|
|
15986
|
-
const runRoot =
|
|
15987
|
-
const indexPath =
|
|
15988
|
-
if (!
|
|
15989
|
-
const index = JSON.parse(await
|
|
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 =
|
|
15992
|
-
if (
|
|
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 =
|
|
15995
|
-
if (
|
|
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
|
|
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
|
|
16026
|
-
import { existsSync as
|
|
16027
|
-
import { dirname as
|
|
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
|
|
17269
|
+
return resolve17(repoRoot, ".opencode", "ship.config.json");
|
|
16344
17270
|
}
|
|
16345
17271
|
async function loadConfig(repoRoot) {
|
|
16346
17272
|
const path = configPath(repoRoot);
|
|
16347
|
-
if (!
|
|
16348
|
-
const raw = await
|
|
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
|
|
16433
|
-
import { resolve as
|
|
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 (
|
|
16443
|
-
if (
|
|
16444
|
-
if (
|
|
16445
|
-
if (
|
|
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 =
|
|
16450
|
-
if (!
|
|
17357
|
+
const path = join22(repoRoot, "package.json");
|
|
17358
|
+
if (!existsSync18(path)) return null;
|
|
16451
17359
|
try {
|
|
16452
|
-
return JSON.parse(
|
|
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 =
|
|
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
|
|
16577
|
-
function
|
|
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
|
|
17496
|
+
return spawn8(repoRoot, argv).status ?? -1;
|
|
16589
17497
|
}
|
|
16590
17498
|
function safeRemoveWorktree2(repoRoot, target) {
|
|
16591
|
-
const r =
|
|
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 (!
|
|
17511
|
+
if (!existsSync19(path)) return [];
|
|
16604
17512
|
try {
|
|
16605
|
-
const raw = await
|
|
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 (!
|
|
16616
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
16745
|
-
import { dirname as
|
|
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.
|
|
17655
|
+
var PACKAGE_VERSION = "1.1.2-rc.2";
|
|
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
|
|
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 =
|
|
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: {
|