opencode-ship 1.1.8 → 1.1.9-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.
Files changed (42) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +10 -10
  3. package/THIRD_PARTY_NOTICES.md +1 -1
  4. package/assets/agents/ship-controller.md +50 -28
  5. package/assets/agents/ship-final-spec-reviewer.md +11 -11
  6. package/assets/agents/ship-final-standards-reviewer.md +11 -11
  7. package/assets/agents/ship-plan.md +24 -5
  8. package/assets/agents/ship-planner.md +46 -20
  9. package/assets/agents/{delivery-reviewer.md → ship-reviewer.md} +16 -16
  10. package/assets/agents/ship-task-builder.md +11 -11
  11. package/assets/agents/ship-task-reviewer.md +12 -12
  12. package/assets/agents/{delivery-verifier.md → ship-verifier.md} +13 -13
  13. package/assets/commands/setup-ship-workflow.md +6 -5
  14. package/assets/commands/ship-deliver.md +21 -9
  15. package/assets/defaults/workflow-models.history.json +7 -0
  16. package/assets/defaults/workflow-models.json +5 -0
  17. package/assets/skills/brainstorming/SKILL.md +10 -7
  18. package/assets/skills/dispatching-parallel-agents/SKILL.md +1 -1
  19. package/assets/skills/engineering-workflow/SKILL.md +10 -4
  20. package/assets/skills/executing-plans/SKILL.md +18 -63
  21. package/assets/skills/planning-research-checkpoint/SKILL.md +16 -13
  22. package/assets/skills/receiving-code-review/SKILL.md +1 -1
  23. package/assets/skills/requesting-code-review/SKILL.md +1 -1
  24. package/assets/skills/setup-ship-workflow/SKILL.md +27 -33
  25. package/assets/skills/{delivery-workflow → ship-workflow}/SKILL.md +37 -11
  26. package/assets/skills/skill-discovery/SKILL.md +17 -89
  27. package/assets/skills/subagent-driven-development/SKILL.md +6 -2
  28. package/assets/skills/systematic-debugging/SKILL.md +1 -1
  29. package/assets/skills/test-driven-development/SKILL.md +1 -1
  30. package/assets/skills/verification-before-completion/SKILL.md +1 -1
  31. package/assets/skills/wayfinder/SKILL.md +7 -1
  32. package/assets/skills/writing-plans/SKILL.md +31 -20
  33. package/dist/cli.js +1476 -174
  34. package/dist/core.js +99 -7
  35. package/dist/plugin.js +18510 -17921
  36. package/package.json +2 -1
  37. package/schema/project-adapter.example.json +2 -2
  38. package/schema/project-opencode-shim.json +2 -0
  39. package/schema/ship-lock.schema.json +23 -2
  40. package/tests/plugin/expected-tools.mjs +46 -5
  41. package/tests/plugin/plugin-load.test.mjs +27 -5
  42. package/vendor/sources.json +21 -21
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // opencode-ship CLI v1.1.8
2
+ // opencode-ship CLI v1.1.9-rc.1
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __esm = (fn, res) => function __init() {
@@ -110,11 +110,108 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
110
110
  var PACKAGE_VERSION, TEMPLATE_SET;
111
111
  var init_version = __esm({
112
112
  "src/version.js"() {
113
- PACKAGE_VERSION = "1.1.8";
113
+ PACKAGE_VERSION = "1.1.9-rc.1";
114
114
  TEMPLATE_SET = `v${PACKAGE_VERSION}`;
115
115
  }
116
116
  });
117
117
 
118
+ // src/installer/workflow-models.js
119
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
120
+ import { resolve as resolve3 } from "node:path";
121
+ function failDefaults(message) {
122
+ const err = new Error(message);
123
+ err.catalogValidation = true;
124
+ throw err;
125
+ }
126
+ function readJson(path) {
127
+ if (!existsSync3(path)) {
128
+ failDefaults(`defaults file missing: ${path}`);
129
+ }
130
+ let raw;
131
+ try {
132
+ raw = readFileSync3(path, "utf8");
133
+ } catch (e) {
134
+ failDefaults(`defaults file unreadable: ${path}: ${e?.message ?? e}`);
135
+ }
136
+ if (raw.trim().length === 0) {
137
+ failDefaults(`defaults file empty: ${path}`);
138
+ }
139
+ try {
140
+ return JSON.parse(raw);
141
+ } catch (e) {
142
+ failDefaults(`defaults file is not JSON: ${path}: ${e?.message ?? e}`);
143
+ }
144
+ }
145
+ function assertRoles(value, label) {
146
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
147
+ failDefaults(`${label} must be an object with planner, builder, finalReviewer`);
148
+ }
149
+ for (const role of MODEL_ROLES) {
150
+ if (typeof value[role] !== "string" || value[role].length === 0) {
151
+ failDefaults(`${label} missing role: ${role}`);
152
+ }
153
+ }
154
+ }
155
+ function loadWorkflowModelDefaults() {
156
+ const root = resolvePackageRoot(import.meta.url);
157
+ const current = readJson(resolve3(root, "assets/defaults/workflow-models.json"));
158
+ const history = readJson(resolve3(root, "assets/defaults/workflow-models.history.json"));
159
+ assertRoles(current, "workflow-models.json");
160
+ if (!Array.isArray(history)) {
161
+ failDefaults("workflow-models.history.json must be a JSON array");
162
+ }
163
+ for (let i = 0; i < history.length; i++) {
164
+ assertRoles(history[i], `workflow-models.history.json[${i}]`);
165
+ }
166
+ return { current, history };
167
+ }
168
+ function isNonEmptyString(value) {
169
+ return typeof value === "string" && value.length > 0;
170
+ }
171
+ function resolveWorkflowModels({ configModels, lockModels, cliModels, current, history }) {
172
+ const config = configModels && typeof configModels === "object" ? configModels : {};
173
+ const lock = lockModels && typeof lockModels === "object" ? lockModels : {};
174
+ const cli = cliModels && typeof cliModels === "object" ? cliModels : {};
175
+ const hist = Array.isArray(history) ? history : [];
176
+ const models = {};
177
+ const provenance = {};
178
+ const changedRoles = [];
179
+ for (const role of MODEL_ROLES) {
180
+ let id;
181
+ let source;
182
+ if (isNonEmptyString(cli[role])) {
183
+ source = "override";
184
+ id = cli[role];
185
+ } else if (lock[role]?.source === "override") {
186
+ source = "override";
187
+ id = isNonEmptyString(config[role]) ? config[role] : lock[role].applied;
188
+ } else if (lock[role]?.source === "default") {
189
+ source = "default";
190
+ id = current[role];
191
+ } else if (!isNonEmptyString(config[role])) {
192
+ source = "default";
193
+ id = current[role];
194
+ } else if (config[role] === current[role] || hist.some((entry) => entry?.[role] === config[role])) {
195
+ source = "default";
196
+ id = current[role];
197
+ } else {
198
+ source = "override";
199
+ id = config[role];
200
+ }
201
+ models[role] = id;
202
+ provenance[role] = { source, applied: id };
203
+ if (id !== config[role]) changedRoles.push(role);
204
+ }
205
+ return { models, provenance, changedRoles };
206
+ }
207
+ var MODEL_ROLES;
208
+ var init_workflow_models = __esm({
209
+ "src/installer/workflow-models.js"() {
210
+ init_package_root();
211
+ MODEL_ROLES = ["planner", "builder", "finalReviewer"];
212
+ }
213
+ });
214
+
118
215
  // src/installer/catalog.js
119
216
  var catalog_exports = {};
120
217
  __export(catalog_exports, {
@@ -125,8 +222,8 @@ __export(catalog_exports, {
125
222
  filterCatalogByProfile: () => filterCatalogByProfile,
126
223
  validateCatalog: () => validateCatalog
127
224
  });
128
- import { resolve as resolve3, relative, sep } from "node:path";
129
- import { existsSync as existsSync3, statSync } from "node:fs";
225
+ import { resolve as resolve4, relative, sep } from "node:path";
226
+ import { existsSync as existsSync4, statSync } from "node:fs";
130
227
  function filterCatalogByProfile(catalog, profile) {
131
228
  const effective = profile === void 0 || profile === null ? DEFAULT_PROFILE : isValidProfile(profile) ? profile : profile === "core" ? DEFAULT_PROFILE : null;
132
229
  if (effective === null) {
@@ -166,7 +263,7 @@ function validateCatalog({ catalog = CATALOG } = {}) {
166
263
  }
167
264
  if (typeof source !== "string" || source.length === 0) {
168
265
  issues.push({ id, kind: "source", message: `source path missing: ${id}` });
169
- } else if (!existsSync3(source)) {
266
+ } else if (!existsSync4(source)) {
170
267
  issues.push({ id, kind: "source-missing", message: `source file not found: ${source}` });
171
268
  } else {
172
269
  try {
@@ -204,6 +301,7 @@ function validateCatalog({ catalog = CATALOG } = {}) {
204
301
  err.catalogValidation = true;
205
302
  throw err;
206
303
  }
304
+ loadWorkflowModelDefaults();
207
305
  return catalog;
208
306
  }
209
307
  var TEMPLATE_SET_ID, packageRoot, PACKAGE_ROOT, MATT_SKILLS, SUPER_SKILLS, ENGINEERING_AGENTS, ENGINEERING_COMMANDS, CATALOG, ALLOWED_KINDS;
@@ -212,6 +310,7 @@ var init_catalog = __esm({
212
310
  init_package_root();
213
311
  init_version();
214
312
  init_profile();
313
+ init_workflow_models();
215
314
  TEMPLATE_SET_ID = TEMPLATE_SET;
216
315
  packageRoot = resolvePackageRoot(import.meta.url);
217
316
  PACKAGE_ROOT = packageRoot;
@@ -261,7 +360,23 @@ var init_catalog = __esm({
261
360
  id: "plugin:opencode-ship",
262
361
  kind: "plugin",
263
362
  path: ".opencode/plugins/opencode-ship.js",
264
- source: resolve3(packageRoot, "dist/plugin.js"),
363
+ source: resolve4(packageRoot, "dist/plugin.js"),
364
+ mode: 420,
365
+ profiles: ["engineering"]
366
+ },
367
+ {
368
+ id: "agent:ship-reviewer",
369
+ kind: "agent",
370
+ path: ".opencode/agents/ship-reviewer.md",
371
+ source: resolve4(packageRoot, "assets/agents/ship-reviewer.md"),
372
+ mode: 420,
373
+ profiles: ["engineering"]
374
+ },
375
+ {
376
+ id: "agent:ship-verifier",
377
+ kind: "agent",
378
+ path: ".opencode/agents/ship-verifier.md",
379
+ source: resolve4(packageRoot, "assets/agents/ship-verifier.md"),
265
380
  mode: 420,
266
381
  profiles: ["engineering"]
267
382
  },
@@ -269,23 +384,25 @@ var init_catalog = __esm({
269
384
  id: "agent:delivery-reviewer",
270
385
  kind: "agent",
271
386
  path: ".opencode/agents/delivery-reviewer.md",
272
- source: resolve3(packageRoot, "assets/agents/delivery-reviewer.md"),
387
+ source: resolve4(packageRoot, "assets/agents/ship-reviewer.md"),
273
388
  mode: 420,
274
- profiles: ["engineering"]
389
+ profiles: ["engineering"],
390
+ legacy: true
275
391
  },
276
392
  {
277
393
  id: "agent:delivery-verifier",
278
394
  kind: "agent",
279
395
  path: ".opencode/agents/delivery-verifier.md",
280
- source: resolve3(packageRoot, "assets/agents/delivery-verifier.md"),
396
+ source: resolve4(packageRoot, "assets/agents/ship-verifier.md"),
281
397
  mode: 420,
282
- profiles: ["engineering"]
398
+ profiles: ["engineering"],
399
+ legacy: true
283
400
  },
284
401
  ...ENGINEERING_AGENTS.map((name) => ({
285
402
  id: `agent:${name}`,
286
403
  kind: "agent",
287
404
  path: `.opencode/agents/${name}.md`,
288
- source: resolve3(packageRoot, `assets/agents/${name}.md`),
405
+ source: resolve4(packageRoot, `assets/agents/${name}.md`),
289
406
  mode: 420,
290
407
  profiles: ["engineering"]
291
408
  })),
@@ -293,23 +410,32 @@ var init_catalog = __esm({
293
410
  id: `command:${name}`,
294
411
  kind: "support",
295
412
  path: `.opencode/commands/${name}.md`,
296
- source: resolve3(packageRoot, `assets/commands/${name}.md`),
413
+ source: resolve4(packageRoot, `assets/commands/${name}.md`),
297
414
  mode: 420,
298
415
  profiles: ["engineering"]
299
416
  })),
417
+ {
418
+ id: "skill:ship-workflow",
419
+ kind: "skill",
420
+ path: ".opencode/skills/ship-workflow/SKILL.md",
421
+ source: resolve4(packageRoot, "assets/skills/ship-workflow/SKILL.md"),
422
+ mode: 420,
423
+ profiles: ["engineering"]
424
+ },
300
425
  {
301
426
  id: "skill:delivery-workflow",
302
427
  kind: "skill",
303
428
  path: ".opencode/skills/delivery-workflow/SKILL.md",
304
- source: resolve3(packageRoot, "assets/skills/delivery-workflow/SKILL.md"),
429
+ source: resolve4(packageRoot, "assets/skills/ship-workflow/SKILL.md"),
305
430
  mode: 420,
306
- profiles: ["engineering"]
431
+ profiles: ["engineering"],
432
+ legacy: true
307
433
  },
308
434
  {
309
435
  id: "skill:planning-research-checkpoint",
310
436
  kind: "skill",
311
437
  path: ".opencode/skills/planning-research-checkpoint/SKILL.md",
312
- source: resolve3(packageRoot, "assets/skills/planning-research-checkpoint/SKILL.md"),
438
+ source: resolve4(packageRoot, "assets/skills/planning-research-checkpoint/SKILL.md"),
313
439
  mode: 420,
314
440
  profiles: ["engineering"]
315
441
  },
@@ -317,7 +443,7 @@ var init_catalog = __esm({
317
443
  id: `skill:matt:${name}`,
318
444
  kind: "skill",
319
445
  path: `.opencode/skills/${name}/SKILL.md`,
320
- source: resolve3(packageRoot, `assets/skills/${name}/SKILL.md`),
446
+ source: resolve4(packageRoot, `assets/skills/${name}/SKILL.md`),
321
447
  mode: 420,
322
448
  profiles: ["engineering"]
323
449
  })),
@@ -325,7 +451,7 @@ var init_catalog = __esm({
325
451
  id: `skill:super:${name}`,
326
452
  kind: "skill",
327
453
  path: `.opencode/skills/${name}/SKILL.md`,
328
- source: resolve3(packageRoot, `assets/skills/${name}/SKILL.md`),
454
+ source: resolve4(packageRoot, `assets/skills/${name}/SKILL.md`),
329
455
  mode: 420,
330
456
  profiles: ["engineering"]
331
457
  })),
@@ -333,7 +459,7 @@ var init_catalog = __esm({
333
459
  id: "skill:setup-ship-workflow",
334
460
  kind: "skill",
335
461
  path: ".opencode/skills/setup-ship-workflow/SKILL.md",
336
- source: resolve3(packageRoot, "assets/skills/setup-ship-workflow/SKILL.md"),
462
+ source: resolve4(packageRoot, "assets/skills/setup-ship-workflow/SKILL.md"),
337
463
  mode: 420,
338
464
  profiles: ["engineering"]
339
465
  },
@@ -341,7 +467,7 @@ var init_catalog = __esm({
341
467
  id: "skill:skill-discovery",
342
468
  kind: "skill",
343
469
  path: ".opencode/skills/skill-discovery/SKILL.md",
344
- source: resolve3(packageRoot, "assets/skills/skill-discovery/SKILL.md"),
470
+ source: resolve4(packageRoot, "assets/skills/skill-discovery/SKILL.md"),
345
471
  mode: 420,
346
472
  profiles: ["engineering"]
347
473
  },
@@ -349,7 +475,7 @@ var init_catalog = __esm({
349
475
  id: "command:setup-ship-workflow",
350
476
  kind: "support",
351
477
  path: ".opencode/commands/setup-ship-workflow.md",
352
- source: resolve3(packageRoot, "assets/commands/setup-ship-workflow.md"),
478
+ source: resolve4(packageRoot, "assets/commands/setup-ship-workflow.md"),
353
479
  mode: 420,
354
480
  profiles: ["engineering"]
355
481
  }
@@ -772,14 +898,14 @@ __export(config_exports, {
772
898
  writeConfig: () => writeConfig
773
899
  });
774
900
  import { readFile, writeFile, rename, mkdir } from "node:fs/promises";
775
- import { existsSync as existsSync4 } from "node:fs";
776
- import { dirname as dirname3, resolve as resolve4 } from "node:path";
901
+ import { existsSync as existsSync5 } from "node:fs";
902
+ import { dirname as dirname3, resolve as resolve5 } from "node:path";
777
903
  function configPath(repoRoot) {
778
- return resolve4(repoRoot, ".opencode", "ship.config.json");
904
+ return resolve5(repoRoot, ".opencode", "ship.config.json");
779
905
  }
780
906
  async function loadConfig(repoRoot) {
781
907
  const path = configPath(repoRoot);
782
- if (!existsSync4(path)) return null;
908
+ if (!existsSync5(path)) return null;
783
909
  const raw = await readFile(path, "utf8");
784
910
  let parsed;
785
911
  try {
@@ -835,10 +961,10 @@ function renderDefaultConfig(detection, overrides = {}) {
835
961
  requireCleanDiffAfter: true,
836
962
  invalidateOnHeadChange: true
837
963
  },
838
- review: { agent: "delivery-reviewer", required: true, invalidateOnHeadChange: true },
964
+ review: { agent: "ship-reviewer", required: true, invalidateOnHeadChange: true },
839
965
  ci: {
840
966
  driver: "github-status-checks",
841
- requiredChecks: ["delivery-verify"],
967
+ requiredChecks: ["opencode-ship-verify"],
842
968
  wait: true,
843
969
  flakyRetry: 1
844
970
  },
@@ -869,10 +995,10 @@ var init_config = __esm({
869
995
 
870
996
  // src/installer/lock.js
871
997
  import { readFile as readFile2, writeFile as writeFile2, rename as rename2, mkdir as mkdir2 } from "node:fs/promises";
872
- import { existsSync as existsSync5 } from "node:fs";
873
- import { dirname as dirname4, resolve as resolve5, posix } from "node:path";
998
+ import { existsSync as existsSync6 } from "node:fs";
999
+ import { dirname as dirname4, resolve as resolve6, posix } from "node:path";
874
1000
  function lockPath(repoRoot) {
875
- return resolve5(repoRoot, ".opencode", "ship.lock.json");
1001
+ return resolve6(repoRoot, ".opencode", "ship.lock.json");
876
1002
  }
877
1003
  function computeIntegrity(lock) {
878
1004
  const { integrity: _ignored, ...without } = lock ?? {};
@@ -896,8 +1022,8 @@ function validateLock(rawLock) {
896
1022
  }
897
1023
  const issues = [];
898
1024
  let kind = "ok";
899
- if (rawLock.contractVersion !== CURRENT_LOCK_SCHEMA && rawLock.contractVersion !== 3 && rawLock.contractVersion !== 2 && rawLock.contractVersion !== 1) {
900
- issues.push(`unsupported contractVersion: ${JSON.stringify(rawLock.contractVersion)} (expected ${CURRENT_LOCK_SCHEMA}, 3, 2, or 1)`);
1025
+ if (rawLock.contractVersion !== CURRENT_LOCK_SCHEMA && rawLock.contractVersion !== 4 && rawLock.contractVersion !== 3 && rawLock.contractVersion !== 2 && rawLock.contractVersion !== 1) {
1026
+ issues.push(`unsupported contractVersion: ${JSON.stringify(rawLock.contractVersion)} (expected ${CURRENT_LOCK_SCHEMA}, 4, 3, 2, or 1)`);
901
1027
  kind = "schema";
902
1028
  }
903
1029
  const manager = rawLock.manager;
@@ -907,8 +1033,8 @@ function validateLock(rawLock) {
907
1033
  } else if (typeof manager !== "object" || manager === null) {
908
1034
  issues.push("manager section must be an object");
909
1035
  kind = kind === "ok" ? "shape" : kind;
910
- } else if (manager.schemaVersion !== CURRENT_LOCK_SCHEMA && manager.schemaVersion !== 3 && manager.schemaVersion !== 2 && manager.schemaVersion !== 1) {
911
- issues.push(`unsupported manager.schemaVersion: ${JSON.stringify(manager.schemaVersion)} (expected ${CURRENT_LOCK_SCHEMA}, 3, 2, or 1)`);
1036
+ } else if (manager.schemaVersion !== CURRENT_LOCK_SCHEMA && manager.schemaVersion !== 4 && manager.schemaVersion !== 3 && manager.schemaVersion !== 2 && manager.schemaVersion !== 1) {
1037
+ issues.push(`unsupported manager.schemaVersion: ${JSON.stringify(manager.schemaVersion)} (expected ${CURRENT_LOCK_SCHEMA}, 4, 3, 2, or 1)`);
912
1038
  kind = "schema";
913
1039
  } else if (manager.name !== "opencode-ship") {
914
1040
  issues.push(`unknown manager.name: ${JSON.stringify(manager.name)}`);
@@ -947,7 +1073,7 @@ function isSafeManagedPath(value) {
947
1073
  }
948
1074
  async function readValidatedLock(repoRoot) {
949
1075
  const path = lockPath(repoRoot);
950
- if (!existsSync5(path)) {
1076
+ if (!existsSync6(path)) {
951
1077
  return { kind: "missing", lock: null, issues: [] };
952
1078
  }
953
1079
  let raw;
@@ -983,7 +1109,7 @@ var init_lock = __esm({
983
1109
  init_hash();
984
1110
  init_json_pointer();
985
1111
  init_profile();
986
- CURRENT_LOCK_SCHEMA = 4;
1112
+ CURRENT_LOCK_SCHEMA = 5;
987
1113
  }
988
1114
  });
989
1115
 
@@ -1007,7 +1133,16 @@ function planModePermissions() {
1007
1133
  delivery_pr: DENY_DEFAULT,
1008
1134
  delivery_ready: DENY_DEFAULT,
1009
1135
  delivery_merge: DENY_DEFAULT,
1010
- delivery_cleanup: DENY_DEFAULT
1136
+ delivery_cleanup: DENY_DEFAULT,
1137
+ ship_inspect: DENY_DEFAULT,
1138
+ ship_issue: DENY_DEFAULT,
1139
+ ship_worktree: DENY_DEFAULT,
1140
+ ship_verify: DENY_DEFAULT,
1141
+ ship_review: DENY_DEFAULT,
1142
+ ship_pr: DENY_DEFAULT,
1143
+ ship_ready: DENY_DEFAULT,
1144
+ ship_merge: DENY_DEFAULT,
1145
+ ship_cleanup: DENY_DEFAULT
1011
1146
  }
1012
1147
  };
1013
1148
  }
@@ -1089,24 +1224,24 @@ __export(root_config_exports, {
1089
1224
  readRootConfig: () => readRootConfig,
1090
1225
  synthesizeDefaultRootConfig: () => synthesizeDefaultRootConfig
1091
1226
  });
1092
- import { existsSync as existsSync6, readFileSync as readFileSync3 } from "node:fs";
1227
+ import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
1093
1228
  import { readFile as readFile3 } from "node:fs/promises";
1094
- import { resolve as resolve6 } from "node:path";
1229
+ import { resolve as resolve7 } from "node:path";
1095
1230
  function findRootConfig(repoRoot) {
1096
1231
  for (const rel of ROOT_PATH_CANDIDATES) {
1097
- const abs = resolve6(repoRoot, rel);
1098
- if (existsSync6(abs)) return { path: abs, relative: rel, format: rel.endsWith(".jsonc") ? "jsonc" : "json" };
1232
+ const abs = resolve7(repoRoot, rel);
1233
+ if (existsSync7(abs)) return { path: abs, relative: rel, format: rel.endsWith(".jsonc") ? "jsonc" : "json" };
1099
1234
  }
1100
1235
  return { path: null, relative: ROOT_PATH_CANDIDATES[0], format: "json" };
1101
1236
  }
1102
1237
  function defaultRootConfigPath(repoRoot) {
1103
- return resolve6(repoRoot, ROOT_PATH_CANDIDATES[0]);
1238
+ return resolve7(repoRoot, ROOT_PATH_CANDIDATES[0]);
1104
1239
  }
1105
1240
  function readRootConfig(absPath) {
1106
- if (!existsSync6(absPath)) {
1241
+ if (!existsSync7(absPath)) {
1107
1242
  return { ok: false, error: { kind: "missing", path: absPath } };
1108
1243
  }
1109
- const raw = readFileSync3(absPath, "utf8");
1244
+ const raw = readFileSync4(absPath, "utf8");
1110
1245
  const stripped = stripJsonc(raw);
1111
1246
  try {
1112
1247
  const value = JSON.parse(stripped);
@@ -1320,6 +1455,61 @@ var init_root_config = __esm({
1320
1455
  strategy: "value",
1321
1456
  value: "allow"
1322
1457
  },
1458
+ {
1459
+ pointer: "/agent/build/permission/ship_inspect",
1460
+ strategy: "value",
1461
+ value: "allow"
1462
+ },
1463
+ {
1464
+ pointer: "/agent/build/permission/ship_issue",
1465
+ strategy: "value",
1466
+ value: "allow"
1467
+ },
1468
+ {
1469
+ pointer: "/agent/build/permission/ship_worktree",
1470
+ strategy: "value",
1471
+ value: "allow"
1472
+ },
1473
+ {
1474
+ pointer: "/agent/build/permission/ship_verify",
1475
+ strategy: "value",
1476
+ value: "deny"
1477
+ },
1478
+ {
1479
+ pointer: "/agent/build/permission/ship_review",
1480
+ strategy: "value",
1481
+ value: "deny"
1482
+ },
1483
+ {
1484
+ pointer: "/agent/build/permission/ship_pr",
1485
+ strategy: "value",
1486
+ value: "allow"
1487
+ },
1488
+ {
1489
+ pointer: "/agent/build/permission/ship_ready",
1490
+ strategy: "value",
1491
+ value: "allow"
1492
+ },
1493
+ {
1494
+ pointer: "/agent/build/permission/ship_merge",
1495
+ strategy: "value",
1496
+ value: "ask"
1497
+ },
1498
+ {
1499
+ pointer: "/agent/build/permission/ship_cleanup",
1500
+ strategy: "value",
1501
+ value: "allow"
1502
+ },
1503
+ {
1504
+ pointer: "/agent/build/permission/task/ship-reviewer",
1505
+ strategy: "value",
1506
+ value: "allow"
1507
+ },
1508
+ {
1509
+ pointer: "/agent/build/permission/task/ship-verifier",
1510
+ strategy: "value",
1511
+ value: "allow"
1512
+ },
1323
1513
  // Build -> ship-controller delegation so the deep plan/build/review
1324
1514
  // chain works with subagent_depth=2.
1325
1515
  {
@@ -1556,9 +1746,9 @@ __export(setup_state_exports, {
1556
1746
  modelsComplete: () => modelsComplete,
1557
1747
  setupComplete: () => setupComplete
1558
1748
  });
1559
- import { existsSync as existsSync14 } from "node:fs";
1749
+ import { existsSync as existsSync15 } from "node:fs";
1560
1750
  import { readFile as readFile10 } from "node:fs/promises";
1561
- import { resolve as resolve13 } from "node:path";
1751
+ import { resolve as resolve14 } from "node:path";
1562
1752
  function modelsComplete(repoRoot, configValue) {
1563
1753
  if (configValue === void 0) return false;
1564
1754
  return hasCompletedModels(configValue);
@@ -1566,12 +1756,12 @@ function modelsComplete(repoRoot, configValue) {
1566
1756
  async function setupComplete(repoRoot, configValue) {
1567
1757
  const missing = [];
1568
1758
  for (const rel of REQUIRED_DOCS) {
1569
- const path = resolve13(repoRoot, rel);
1570
- if (!existsSync14(path)) missing.push(rel);
1759
+ const path = resolve14(repoRoot, rel);
1760
+ if (!existsSync15(path)) missing.push(rel);
1571
1761
  }
1572
- const agentsPath = resolve13(repoRoot, "AGENTS.md");
1762
+ const agentsPath = resolve14(repoRoot, "AGENTS.md");
1573
1763
  let agentsOk = false;
1574
- if (existsSync14(agentsPath)) {
1764
+ if (existsSync15(agentsPath)) {
1575
1765
  try {
1576
1766
  const raw = await readFile10(agentsPath, "utf8");
1577
1767
  agentsOk = /##\s+Ship workflow\b/.test(raw);
@@ -1611,6 +1801,840 @@ var init_setup_state = __esm({
1611
1801
  }
1612
1802
  });
1613
1803
 
1804
+ // src/tools/skill-discovery.js
1805
+ import { spawn as spawn2 } from "node:child_process";
1806
+ import { existsSync as existsSync18, readFileSync as readFileSync8, writeFileSync, mkdirSync, readdirSync, statSync as statSync2 } from "node:fs";
1807
+ import { dirname as dirname10, join as join7, normalize, resolve as resolve17, sep as sep3 } from "node:path";
1808
+ function parseFindOutput(text) {
1809
+ if (typeof text !== "string") return [];
1810
+ const stripped = text.replace(ANSI_RE, "");
1811
+ const lines = stripped.split(/\r?\n/);
1812
+ const candidates = [];
1813
+ for (const raw of lines) {
1814
+ const line = raw.trim();
1815
+ if (!line) continue;
1816
+ const match = line.match(/^([a-zA-Z0-9_.\-]+)\/([a-zA-Z0-9_.\-]+)@([a-zA-Z0-9_.\-]+)\s+([0-9]+(?:\.[0-9]+)?)([KM]?)\s+installs\b/i);
1817
+ if (!match) continue;
1818
+ const num = Number.parseFloat(match[4]);
1819
+ if (!Number.isFinite(num)) continue;
1820
+ let installs = Math.round(num);
1821
+ const suffix = match[5].toUpperCase();
1822
+ if (suffix === "K") installs = Math.round(num * 1e3);
1823
+ else if (suffix === "M") installs = Math.round(num * 1e6);
1824
+ candidates.push({
1825
+ package: `${match[1]}/${match[2]}`,
1826
+ skill: match[3],
1827
+ installs
1828
+ });
1829
+ }
1830
+ return candidates;
1831
+ }
1832
+ function discoverSkillsWithStdout(text) {
1833
+ const raw = typeof text === "string" ? text : "";
1834
+ const candidates = parseFindOutput(raw);
1835
+ if (candidates.length > 0) {
1836
+ return { ok: true, candidates, raw };
1837
+ }
1838
+ if (raw.trim().length === 0) {
1839
+ return { ok: true, candidates, raw };
1840
+ }
1841
+ return {
1842
+ ok: false,
1843
+ error: { kind: "registry-contract-mismatch", raw }
1844
+ };
1845
+ }
1846
+ function runCapture(cmd, args, options) {
1847
+ const cwd = options?.cwd;
1848
+ const timeoutMs = options?.timeoutMs ?? 6e4;
1849
+ return new Promise((resolveP, rejectP) => {
1850
+ const child = spawn2(cmd, args, { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
1851
+ let stdout = "";
1852
+ let stderr = "";
1853
+ const timer = setTimeout(() => {
1854
+ child.kill("SIGKILL");
1855
+ rejectP(new Error(`skill-discovery: timeout running '${cmd} ${args.join(" ")}'`));
1856
+ }, timeoutMs);
1857
+ child.stdout.on("data", (chunk) => {
1858
+ stdout += chunk.toString("utf8");
1859
+ });
1860
+ child.stderr.on("data", (chunk) => {
1861
+ stderr += chunk.toString("utf8");
1862
+ });
1863
+ child.on("error", (err) => {
1864
+ clearTimeout(timer);
1865
+ rejectP(err);
1866
+ });
1867
+ child.on("close", (code) => {
1868
+ clearTimeout(timer);
1869
+ resolveP({ code, stdout, stderr });
1870
+ });
1871
+ });
1872
+ }
1873
+ async function discoverSkills({ repoRoot, query, npmBin = "npx" }) {
1874
+ if (!repoRoot || !query) {
1875
+ return { ok: false, error: { kind: "missing-args" } };
1876
+ }
1877
+ const r = await runCapture(npmBin, ["skills", "find", query], { cwd: repoRoot, timeoutMs: 6e4 });
1878
+ if (r.code !== 0 && !r.stdout.trim()) {
1879
+ return { ok: false, error: { kind: "registry-unavailable", stderr: r.stderr } };
1880
+ }
1881
+ return discoverSkillsWithStdout(r.stdout);
1882
+ }
1883
+ var DEFAULT_TRUSTED_OWNERS, ANSI_RE;
1884
+ var init_skill_discovery = __esm({
1885
+ "src/tools/skill-discovery.js"() {
1886
+ DEFAULT_TRUSTED_OWNERS = Object.freeze([
1887
+ "vercel-labs",
1888
+ "anthropics",
1889
+ "obra",
1890
+ "mattpocock",
1891
+ "ComposioHQ"
1892
+ ]);
1893
+ ANSI_RE = /\x1B\[[0-9;]*m/g;
1894
+ }
1895
+ });
1896
+
1897
+ // src/skills/registry.js
1898
+ import { spawn as spawn3 } from "node:child_process";
1899
+ import { readFile as readFile11, writeFile as writeFile8, mkdir as mkdir7 } from "node:fs/promises";
1900
+ import { resolve as resolve18, dirname as dirname11 } from "node:path";
1901
+ import { createHash as createHash3 } from "node:crypto";
1902
+ async function listSkills({ repoRoot, query, npmBin = "npx" }) {
1903
+ return discoverSkills({ repoRoot, query, npmBin });
1904
+ }
1905
+ var SKILLS_CLI_TIMEOUT_MS, SKILLS_INSTALL_TIMEOUT_MS;
1906
+ var init_registry = __esm({
1907
+ "src/skills/registry.js"() {
1908
+ init_skill_discovery();
1909
+ SKILLS_CLI_TIMEOUT_MS = 60 * 1e3;
1910
+ SKILLS_INSTALL_TIMEOUT_MS = 120 * 1e3;
1911
+ }
1912
+ });
1913
+
1914
+ // src/skills/policy.js
1915
+ import { readFile as readFile12, writeFile as writeFile9 } from "node:fs/promises";
1916
+ import { existsSync as existsSync19 } from "node:fs";
1917
+ import { resolve as resolve19 } from "node:path";
1918
+ function policyPath(repoRoot) {
1919
+ return resolve19(repoRoot, POLICY_PATH);
1920
+ }
1921
+ function defaultPolicy() {
1922
+ return {
1923
+ trustedOwners: [...DEFAULT_TRUSTED_OWNERS2],
1924
+ minInstalls: DEFAULT_MIN_INSTALLS,
1925
+ blocklist: [],
1926
+ maxTrustedPerRun: MAX_TRUSTED_PER_RUN
1927
+ };
1928
+ }
1929
+ async function readPolicy(repoRoot) {
1930
+ const path = policyPath(repoRoot);
1931
+ if (!existsSync19(path)) return defaultPolicy();
1932
+ try {
1933
+ const raw = await readFile12(path, "utf8");
1934
+ const parsed = JSON.parse(raw);
1935
+ return mergePolicy(defaultPolicy(), parsed);
1936
+ } catch {
1937
+ return defaultPolicy();
1938
+ }
1939
+ }
1940
+ function mergePolicy(base, override) {
1941
+ const out = { ...base };
1942
+ if (Array.isArray(override?.trustedOwners)) {
1943
+ out.trustedOwners = [...new Set(override.trustedOwners)];
1944
+ }
1945
+ if (Number.isInteger(override?.minInstalls)) {
1946
+ out.minInstalls = override.minInstalls;
1947
+ }
1948
+ if (Array.isArray(override?.blocklist)) {
1949
+ out.blocklist = [...new Set(override.blocklist)];
1950
+ }
1951
+ if (Number.isInteger(override?.maxTrustedPerRun)) {
1952
+ out.maxTrustedPerRun = override.maxTrustedPerRun;
1953
+ }
1954
+ return out;
1955
+ }
1956
+ function isAutoInstallable(candidate, policy) {
1957
+ if (!candidate || typeof candidate !== "object") {
1958
+ return { ok: false, reason: "missing-candidate" };
1959
+ }
1960
+ if ((policy.blocklist ?? []).includes(candidate.package)) {
1961
+ return { ok: false, reason: "blocked" };
1962
+ }
1963
+ const owner = String(candidate.package).split("/")[0];
1964
+ if (!(policy.trustedOwners ?? []).includes(owner)) {
1965
+ return { ok: false, reason: "untrusted-owner" };
1966
+ }
1967
+ if (candidate.installs < (policy.minInstalls ?? DEFAULT_MIN_INSTALLS)) {
1968
+ return { ok: false, reason: "below-threshold" };
1969
+ }
1970
+ return { ok: true };
1971
+ }
1972
+ var DEFAULT_TRUSTED_OWNERS2, DEFAULT_MIN_INSTALLS, MAX_TRUSTED_PER_RUN, POLICY_PATH;
1973
+ var init_policy = __esm({
1974
+ "src/skills/policy.js"() {
1975
+ init_hash();
1976
+ DEFAULT_TRUSTED_OWNERS2 = Object.freeze([
1977
+ "vercel-labs",
1978
+ "anthropics",
1979
+ "obra",
1980
+ "mattpocock",
1981
+ "ComposioHQ"
1982
+ ]);
1983
+ DEFAULT_MIN_INSTALLS = 1e3;
1984
+ MAX_TRUSTED_PER_RUN = 5;
1985
+ POLICY_PATH = ".opencode/ship.skills.policy.json";
1986
+ }
1987
+ });
1988
+
1989
+ // src/tools/envelope.js
1990
+ import { randomBytes as randomBytes2 } from "node:crypto";
1991
+ function operationId(prefix = "op") {
1992
+ return `${prefix}-${Date.now().toString(36)}-${randomBytes2(4).toString("hex")}`;
1993
+ }
1994
+ function success(kind, data, options = {}) {
1995
+ if (typeof kind !== "string" || kind.length === 0) {
1996
+ throw new Error("envelope.success: kind must be a non-empty string");
1997
+ }
1998
+ return {
1999
+ contractVersion: CONTRACT_VERSION,
2000
+ ok: true,
2001
+ kind,
2002
+ operationId: options.operationId ?? operationId(kind),
2003
+ idempotent: options.idempotent !== false,
2004
+ data
2005
+ };
2006
+ }
2007
+ function failure(kind, message, options = {}) {
2008
+ if (typeof kind !== "string" || kind.length === 0) {
2009
+ throw new Error("envelope.failure: kind must be a non-empty string");
2010
+ }
2011
+ if (typeof message !== "string" || message.length === 0) {
2012
+ throw new Error("envelope.failure: message must be a non-empty string");
2013
+ }
2014
+ const details = options.details ?? {};
2015
+ return {
2016
+ contractVersion: CONTRACT_VERSION,
2017
+ ok: false,
2018
+ kind,
2019
+ operationId: options.operationId ?? operationId(`${kind}-err`),
2020
+ retryable: options.retryable === true,
2021
+ message,
2022
+ details
2023
+ };
2024
+ }
2025
+ var CONTRACT_VERSION;
2026
+ var init_envelope = __esm({
2027
+ "src/tools/envelope.js"() {
2028
+ CONTRACT_VERSION = 2;
2029
+ }
2030
+ });
2031
+
2032
+ // src/skills/inventory.js
2033
+ import { readFile as readFile13, writeFile as writeFile10, mkdir as mkdir8, rename as rename6 } from "node:fs/promises";
2034
+ import { existsSync as existsSync20 } from "node:fs";
2035
+ import { resolve as resolve20, dirname as dirname12, isAbsolute, posix as posix2 } from "node:path";
2036
+ import { createHash as createHash4 } from "node:crypto";
2037
+ function inventoryPath(repoRoot) {
2038
+ return resolve20(repoRoot, INVENTORY_PATH);
2039
+ }
2040
+ async function readInventory(repoRoot) {
2041
+ const path = inventoryPath(repoRoot);
2042
+ if (!existsSync20(path)) {
2043
+ return { schemaVersion: INVENTORY_SCHEMA, events: [] };
2044
+ }
2045
+ let raw;
2046
+ try {
2047
+ raw = await readFile13(path, "utf8");
2048
+ } catch (err) {
2049
+ return { schemaVersion: INVENTORY_SCHEMA, events: [], parseError: `read failed: ${err?.message ?? err}` };
2050
+ }
2051
+ let parsed;
2052
+ try {
2053
+ parsed = JSON.parse(raw);
2054
+ } catch (err) {
2055
+ return { schemaVersion: INVENTORY_SCHEMA, events: [], parseError: `malformed JSON: ${err?.message ?? err}` };
2056
+ }
2057
+ if (!parsed || typeof parsed !== "object") {
2058
+ return { schemaVersion: INVENTORY_SCHEMA, events: [], parseError: "inventory root is not an object" };
2059
+ }
2060
+ if (!Array.isArray(parsed.events)) {
2061
+ return { schemaVersion: INVENTORY_SCHEMA, events: [], parseError: "inventory.events is not an array" };
2062
+ }
2063
+ if (parsed.schemaVersion !== INVENTORY_SCHEMA) {
2064
+ return {
2065
+ schemaVersion: parsed.schemaVersion,
2066
+ events: [],
2067
+ parseError: `unsupported inventory schemaVersion ${parsed.schemaVersion} (expected ${INVENTORY_SCHEMA})`
2068
+ };
2069
+ }
2070
+ return { schemaVersion: INVENTORY_SCHEMA, events: parsed.events };
2071
+ }
2072
+ async function writeInventory(repoRoot, inventory) {
2073
+ const path = inventoryPath(repoRoot);
2074
+ await mkdir8(dirname12(path), { recursive: true });
2075
+ const tmp = `${path}.${Date.now().toString(36)}.tmp`;
2076
+ await writeFile10(tmp, JSON.stringify({ schemaVersion: INVENTORY_SCHEMA, events: inventory.events }, null, 2) + "\n", "utf8");
2077
+ await rename6(tmp, path);
2078
+ return path;
2079
+ }
2080
+ function canonicalize2(value) {
2081
+ const seen = /* @__PURE__ */ new WeakSet();
2082
+ const sort = (v) => {
2083
+ if (v === null || typeof v !== "object") return v;
2084
+ if (seen.has(v)) return null;
2085
+ seen.add(v);
2086
+ if (Array.isArray(v)) return v.map(sort);
2087
+ const out = {};
2088
+ for (const k of Object.keys(v).sort()) out[k] = sort(v[k]);
2089
+ return out;
2090
+ };
2091
+ return JSON.stringify(sort(value));
2092
+ }
2093
+ function hashEvent(event) {
2094
+ return createHash4("sha256").update(canonicalize2(event), "utf8").digest("hex");
2095
+ }
2096
+ async function appendEvent(repoRoot, eventInput) {
2097
+ const inventory = await readInventory(repoRoot);
2098
+ if (inventory.parseError) {
2099
+ throw new Error(`inventory is unreadable: ${inventory.parseError}`);
2100
+ }
2101
+ const existingChain = await verifyInventory(repoRoot);
2102
+ if (!existingChain.ok) {
2103
+ throw new Error(`inventory chain invalid: ${existingChain.reason}`);
2104
+ }
2105
+ const previousHash = inventory.events.length > 0 ? inventory.events[inventory.events.length - 1].hash : "0".repeat(64);
2106
+ const sequence = inventory.events.length + 1;
2107
+ if (eventInput.destination && isAbsolute(eventInput.destination)) {
2108
+ throw new Error(`inventory refuses absolute destination: ${eventInput.destination}`);
2109
+ }
2110
+ const base = {
2111
+ sequence,
2112
+ type: eventInput.type,
2113
+ previousHash,
2114
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString()
2115
+ };
2116
+ const {
2117
+ hash: _hash,
2118
+ sequence: _sequence,
2119
+ previousHash: _previousHash,
2120
+ recordedAt: _recordedAt,
2121
+ payload: legacyPayload,
2122
+ type: _type,
2123
+ ...fields
2124
+ } = eventInput;
2125
+ const payload = { ...legacyPayload ?? {}, ...fields, ...base };
2126
+ const shape = validateEventShape(payload);
2127
+ if (!shape.ok) throw new Error(shape.reason);
2128
+ delete payload.hash;
2129
+ const stamped = { ...payload, hash: hashEvent(payload) };
2130
+ inventory.events.push(stamped);
2131
+ await writeInventory(repoRoot, inventory);
2132
+ return stamped;
2133
+ }
2134
+ async function verifyInventory(repoRoot) {
2135
+ const inventory = await readInventory(repoRoot);
2136
+ if (inventory.parseError) {
2137
+ return { ok: false, reason: inventory.parseError };
2138
+ }
2139
+ if (inventory.events.length === 0) return { ok: true, count: 0 };
2140
+ let prev = "0".repeat(64);
2141
+ for (const ev of inventory.events) {
2142
+ const shape = validateEventShape(ev);
2143
+ if (!shape.ok) {
2144
+ return { ok: false, reason: shape.reason, sequence: ev.sequence };
2145
+ }
2146
+ if (ev.sequence !== inventory.events.indexOf(ev) + 1) {
2147
+ return { ok: false, reason: "sequence-gap", sequence: ev.sequence };
2148
+ }
2149
+ if (ev.previousHash !== prev) {
2150
+ return { ok: false, reason: "chain-break", sequence: ev.sequence };
2151
+ }
2152
+ const { hash: _h, ...rest } = ev;
2153
+ const recomputed = hashEvent(rest);
2154
+ if (recomputed !== ev.hash) {
2155
+ return { ok: false, reason: "hash-mismatch", sequence: ev.sequence };
2156
+ }
2157
+ prev = ev.hash;
2158
+ }
2159
+ return { ok: true, count: inventory.events.length };
2160
+ }
2161
+ function validateEventShape(event) {
2162
+ if (event?.type !== "install" && event?.type !== "uninstall") {
2163
+ return { ok: false, reason: `unsupported inventory event type: ${JSON.stringify(event?.type)}` };
2164
+ }
2165
+ if (typeof event.skill !== "string" || !/^[A-Za-z0-9._-]{1,128}$/.test(event.skill)) {
2166
+ return { ok: false, reason: `invalid skill id: ${JSON.stringify(event.skill)}` };
2167
+ }
2168
+ if (!isSafeRelativePosix(event.destination)) {
2169
+ return { ok: false, reason: `unsafe destination: ${JSON.stringify(event.destination)}` };
2170
+ }
2171
+ if (event.type === "install") {
2172
+ if (!Array.isArray(event.files)) return { ok: false, reason: "install event files must be an array" };
2173
+ for (const file of event.files) {
2174
+ if (!file || !isSafeRelativePosix(file.path)) {
2175
+ return { ok: false, reason: `unsafe file path: ${JSON.stringify(file?.path)}` };
2176
+ }
2177
+ if (typeof file.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(file.sha256)) {
2178
+ return { ok: false, reason: `invalid file sha256: ${JSON.stringify(file.sha256)}` };
2179
+ }
2180
+ }
2181
+ }
2182
+ return { ok: true };
2183
+ }
2184
+ function isSafeRelativePosix(value) {
2185
+ if (typeof value !== "string" || value.length === 0 || value.includes("\\")) return false;
2186
+ if (posix2.isAbsolute(value) || posix2.normalize(value) !== value) return false;
2187
+ return value.split("/").every((part) => part !== "" && part !== "." && part !== "..");
2188
+ }
2189
+ var INVENTORY_PATH, INVENTORY_SCHEMA;
2190
+ var init_inventory = __esm({
2191
+ "src/skills/inventory.js"() {
2192
+ INVENTORY_PATH = ".opencode/ship.skills.lock.json";
2193
+ INVENTORY_SCHEMA = 2;
2194
+ }
2195
+ });
2196
+
2197
+ // src/skills/worktree.js
2198
+ import { execFile } from "node:child_process";
2199
+ import { promises as fs, existsSync as existsSync21 } from "node:fs";
2200
+ import { resolve as resolve21, dirname as dirname13, sep as sep4, isAbsolute as isAbsolute2, join as join9 } from "node:path";
2201
+ function listRegisteredWorktrees(mainRepo) {
2202
+ return new Promise((resolveP, rejectP) => {
2203
+ execFile(
2204
+ "git",
2205
+ ["-C", mainRepo, "worktree", "list", "--porcelain", "-z"],
2206
+ { shell: false, maxBuffer: 1024 * 1024 },
2207
+ (err, stdout) => {
2208
+ if (err) return rejectP(err);
2209
+ const records = parsePorcelain(stdout);
2210
+ const mainRecord = records.shift();
2211
+ const mainPath = mainRecord?.worktree ? resolve21(mainRecord.worktree) : null;
2212
+ const linked = [];
2213
+ for (const r of records) {
2214
+ if (!r.worktree) continue;
2215
+ const p = resolve21(r.worktree);
2216
+ if (mainPath && p === mainPath) continue;
2217
+ linked.push({ path: p, branch: r.HEAD ?? null });
2218
+ }
2219
+ resolveP(linked);
2220
+ }
2221
+ );
2222
+ });
2223
+ }
2224
+ function parsePorcelain(text) {
2225
+ const tokens = text.split("\0");
2226
+ const out = [];
2227
+ let current = {};
2228
+ for (const tok of tokens) {
2229
+ if (tok.length === 0) {
2230
+ if (Object.keys(current).length > 0) {
2231
+ out.push(current);
2232
+ current = {};
2233
+ }
2234
+ continue;
2235
+ }
2236
+ const idx = tok.indexOf(" ");
2237
+ const key = idx === -1 ? tok : tok.slice(0, idx);
2238
+ const value = idx === -1 ? "" : tok.slice(idx + 1);
2239
+ if (key === "branch") {
2240
+ current.HEAD = value.startsWith("refs/heads/") ? value : `refs/heads/${value}`;
2241
+ } else {
2242
+ current[key] = value;
2243
+ }
2244
+ }
2245
+ if (Object.keys(current).length > 0) out.push(current);
2246
+ return out;
2247
+ }
2248
+ async function validateLinkedWorktree(mainRepo, worktreePath, options = {}) {
2249
+ const main2 = resolve21(mainRepo);
2250
+ if (!existsSync21(main2)) {
2251
+ return { ok: false, kind: "missing", message: `main repository ${main2} does not exist` };
2252
+ }
2253
+ if (!worktreePath) {
2254
+ return { ok: false, kind: "unlinked", message: "worktreePath is required" };
2255
+ }
2256
+ const wt = resolve21(worktreePath);
2257
+ if (!existsSync21(wt)) {
2258
+ return { ok: false, kind: "missing", message: `worktree ${wt} does not exist` };
2259
+ }
2260
+ const isCurrent = wt === main2;
2261
+ if (isCurrent) {
2262
+ const gitEntry = options.allowCurrentLinked ? await fs.lstat(join9(main2, ".git")).catch(() => null) : null;
2263
+ if (!gitEntry?.isFile()) {
2264
+ return { ok: false, kind: "main", message: "installs into the main worktree are forbidden" };
2265
+ }
2266
+ }
2267
+ let cursor = wt;
2268
+ while (cursor !== dirname13(cursor)) {
2269
+ const stat5 = await fs.lstat(cursor).catch(() => null);
2270
+ if (stat5?.isSymbolicLink()) {
2271
+ return {
2272
+ ok: false,
2273
+ kind: "ancestor-symlink",
2274
+ message: `worktree path contains a symlink at ${cursor}`
2275
+ };
2276
+ }
2277
+ cursor = dirname13(cursor);
2278
+ }
2279
+ const real = await fs.realpath(wt).catch(() => null);
2280
+ if (real && real !== wt) {
2281
+ return {
2282
+ ok: false,
2283
+ kind: "symlink",
2284
+ message: `worktree ${wt} resolves through a symlink to ${real}`
2285
+ };
2286
+ }
2287
+ const linked = await listRegisteredWorktrees(main2);
2288
+ const matched = linked.find((entry) => entry.path === wt);
2289
+ if (!matched) {
2290
+ return {
2291
+ ok: false,
2292
+ kind: "unlinked",
2293
+ message: `worktree ${wt} is not registered (git worktree list)`
2294
+ };
2295
+ }
2296
+ return { ok: true, path: wt, registered: !!matched };
2297
+ }
2298
+ function isProjectSkillDest(destRel) {
2299
+ return PROJECT_SKILL_DEST_RE.test(String(destRel ?? ""));
2300
+ }
2301
+ function validateRelativeInstallPath(destRel) {
2302
+ if (typeof destRel !== "string" || destRel.length === 0) {
2303
+ return { ok: false, kind: "absolute", message: "destination path required" };
2304
+ }
2305
+ if (isAbsolute2(destRel)) {
2306
+ return { ok: false, kind: "absolute", message: `destination must be relative: ${destRel}` };
2307
+ }
2308
+ if (destRel.includes("\\")) {
2309
+ return { ok: false, kind: "parent-relative", message: `destination must use POSIX separators: ${destRel}` };
2310
+ }
2311
+ const parts = destRel.split("/");
2312
+ for (const p of parts) {
2313
+ if (p === "" || p === "." || p === "..") {
2314
+ return { ok: false, kind: "parent-relative", message: `destination escapes worktree: ${destRel}` };
2315
+ }
2316
+ }
2317
+ return { ok: true };
2318
+ }
2319
+ async function validateInstallDestination(worktreeRoot, destRel) {
2320
+ const relativeCheck = validateRelativeInstallPath(destRel);
2321
+ if (!relativeCheck.ok) return relativeCheck;
2322
+ const root = resolve21(worktreeRoot);
2323
+ const destination = resolve21(root, ...destRel.split("/"));
2324
+ if (destination !== root && !destination.startsWith(root + sep4)) {
2325
+ return { ok: false, kind: "escape", message: `destination escapes worktree: ${destRel}` };
2326
+ }
2327
+ let cursor = root;
2328
+ for (const part of destRel.split("/")) {
2329
+ cursor = join9(cursor, part);
2330
+ const entry = await fs.lstat(cursor).catch(() => null);
2331
+ if (!entry) continue;
2332
+ if (entry.isSymbolicLink()) {
2333
+ return { ok: false, kind: "symlink", message: `destination path contains a symlink at ${cursor}` };
2334
+ }
2335
+ if (cursor !== destination && !entry.isDirectory()) {
2336
+ return { ok: false, kind: "not-directory", message: `destination ancestor is not a directory: ${cursor}` };
2337
+ }
2338
+ }
2339
+ return { ok: true, path: destination };
2340
+ }
2341
+ var PROJECT_SKILL_DEST_RE;
2342
+ var init_worktree = __esm({
2343
+ "src/skills/worktree.js"() {
2344
+ PROJECT_SKILL_DEST_RE = /^\.opencode\/skills\/[A-Za-z0-9._-]{1,128}$/;
2345
+ }
2346
+ });
2347
+
2348
+ // src/tools/ship-skill-install.js
2349
+ var ship_skill_install_exports = {};
2350
+ __export(ship_skill_install_exports, {
2351
+ createSkillInstallTool: () => createSkillInstallTool,
2352
+ findStagedSkillDir: () => findStagedSkillDir
2353
+ });
2354
+ import { readFile as readFile14, writeFile as writeFile11, mkdir as mkdir9, rm, rename as rename7, stat as stat4 } from "node:fs/promises";
2355
+ import { existsSync as existsSync22 } from "node:fs";
2356
+ import { resolve as resolve22, join as join10, dirname as dirname14, sep as sep5, isAbsolute as isAbsolute3 } from "node:path";
2357
+ import { createHash as createHash5 } from "node:crypto";
2358
+ import { execFile as execFile2 } from "node:child_process";
2359
+ import { mkdtemp } from "node:fs/promises";
2360
+ import { tmpdir } from "node:os";
2361
+ import { randomBytes as randomBytes3 } from "node:crypto";
2362
+ function createSkillInstallTool(deps) {
2363
+ return async function skillInstall(input) {
2364
+ const opId = input.operationId ?? `skill-install-${Date.now().toString(36)}`;
2365
+ const packageSpec = String(input.package ?? "");
2366
+ const worktreePath = input.worktreePath == null ? "" : String(input.worktreePath);
2367
+ const skillName = String(input.skillName ?? "");
2368
+ const version = String(input.version ?? "");
2369
+ if (!packageSpec || !SAFE_NAME_RE.test(packageSpec)) {
2370
+ return failure("skill-install", "package required (safe npm spec)", { operationId: opId, retryable: false });
2371
+ }
2372
+ if (!skillName || !SAFE_ID_RE.test(skillName)) {
2373
+ return failure("skill-install", "skillName required (safe id)", { operationId: opId, retryable: false });
2374
+ }
2375
+ if (version && !SAFE_VERSION_RE.test(version)) {
2376
+ return failure("skill-install", "version must match a safe semver spec", { operationId: opId, retryable: false });
2377
+ }
2378
+ const destRel = `.opencode/skills/${skillName}`;
2379
+ if (!isProjectSkillDest(destRel)) {
2380
+ return failure("skill-install", `destination rejected: ${destRel}`, { operationId: opId, retryable: false });
2381
+ }
2382
+ const policy = await readPolicy(deps.repoRoot);
2383
+ const ownerCandidate = {
2384
+ package: packageSpec,
2385
+ skill: skillName,
2386
+ installs: Number.MAX_SAFE_INTEGER
2387
+ };
2388
+ const ownerDecision = isAutoInstallable(ownerCandidate, policy);
2389
+ if (!ownerDecision.ok) {
2390
+ return failure("skill-install", `policy forbids install: ${ownerDecision.reason}`, { operationId: opId, retryable: false });
2391
+ }
2392
+ const requested = worktreePath ? resolve22(worktreePath) : resolve22(deps.repoRoot);
2393
+ const main2 = resolve22(deps.repoRoot);
2394
+ let installRoot;
2395
+ if (requested === main2) {
2396
+ installRoot = main2;
2397
+ } else {
2398
+ const wtCheck = await validateLinkedWorktree(deps.repoRoot, worktreePath);
2399
+ if (!wtCheck.ok) {
2400
+ return failure("skill-install", `worktree rejected: ${wtCheck.message}`, { operationId: opId, retryable: false });
2401
+ }
2402
+ installRoot = wtCheck.path;
2403
+ }
2404
+ const pathCheck = validateRelativeInstallPath(destRel);
2405
+ if (!pathCheck.ok) {
2406
+ return failure("skill-install", `destination rejected: ${pathCheck.message}`, { operationId: opId, retryable: false });
2407
+ }
2408
+ const destinationCheck = await validateInstallDestination(installRoot, destRel);
2409
+ if (!destinationCheck.ok) {
2410
+ return failure("skill-install", `destination rejected: ${destinationCheck.message}`, { operationId: opId, retryable: false });
2411
+ }
2412
+ const destAbs = destinationCheck.path;
2413
+ if (existsSync22(destAbs)) {
2414
+ return failure("skill-install", "destination already exists; use ship_skill_audit to detect drift", { operationId: opId, retryable: false });
2415
+ }
2416
+ const managedCatalog = (deps.config?.value?.skills ?? []).map((s) => s?.name).filter(Boolean);
2417
+ if (managedCatalog.includes(skillName)) {
2418
+ return failure("skill-install", "candidate shadows a managed skill", { operationId: opId, retryable: false });
2419
+ }
2420
+ const discover = deps.discoverSkills ?? listSkills;
2421
+ const discovery = await discover({ repoRoot: deps.repoRoot, query: packageSpec });
2422
+ if (!discovery?.ok) {
2423
+ return failure("skill-install", "registry metadata unavailable; refusing unverified install", { operationId: opId, retryable: true });
2424
+ }
2425
+ const candidate = discovery.candidates?.find((entry) => entry.package === packageSpec && entry.skill === skillName);
2426
+ if (!candidate) {
2427
+ return failure("skill-install", "exact skill package was not found in registry metadata", { operationId: opId, retryable: false });
2428
+ }
2429
+ const decision = isAutoInstallable(candidate, policy);
2430
+ if (!decision.ok) {
2431
+ return failure("skill-install", `policy forbids install: ${decision.reason}`, { operationId: opId, retryable: false });
2432
+ }
2433
+ const stage = await mkdtemp(join10(tmpdir(), `ship-skill-stage-${randomBytes3(4).toString("hex")}-`));
2434
+ let installedFiles;
2435
+ try {
2436
+ const materialise = deps.materialiseFromSkillsCli ?? materialiseFromSkillsCli;
2437
+ installedFiles = await materialise({
2438
+ packageSpec,
2439
+ skillName,
2440
+ version,
2441
+ stageDir: stage
2442
+ });
2443
+ if (!installedFiles.ok) {
2444
+ return failure("skill-install", installedFiles.message, { operationId: opId, retryable: installedFiles.retryable ?? false });
2445
+ }
2446
+ const fileRecords = await hashDir(installedFiles.stagedDir);
2447
+ if (fileRecords.length === 0) {
2448
+ return failure("skill-install", "skills CLI produced an empty staging directory", { operationId: opId, retryable: false });
2449
+ }
2450
+ await mkdir9(dirname14(destAbs), { recursive: true });
2451
+ const destTmp = `${destAbs}.${randomBytes3(4).toString("hex")}.tmp`;
2452
+ await copyDir(installedFiles.stagedDir, destTmp);
2453
+ const finalDestinationCheck = await validateInstallDestination(installRoot, destRel);
2454
+ if (!finalDestinationCheck.ok) {
2455
+ await rm(destTmp, { recursive: true, force: true });
2456
+ return failure("skill-install", `destination rejected: ${finalDestinationCheck.message}`, { operationId: opId, retryable: false });
2457
+ }
2458
+ await rename7(destTmp, destAbs);
2459
+ const onDisk = await hashDir(destAbs);
2460
+ if (!hashesEqual(fileRecords, onDisk)) {
2461
+ await rm(destAbs, { recursive: true, force: true });
2462
+ return failure("skill-install", "drift detected after copy; rolled back", { operationId: opId, retryable: false });
2463
+ }
2464
+ const recorded = await appendEvent(installRoot, {
2465
+ type: "install",
2466
+ skill: skillName,
2467
+ package: packageSpec,
2468
+ version: version || null,
2469
+ source: installedFiles.source,
2470
+ destination: destRel,
2471
+ files: fileRecords
2472
+ });
2473
+ return success("skill-install", {
2474
+ skill: skillName,
2475
+ package: packageSpec,
2476
+ version: version || null,
2477
+ destination: destRel,
2478
+ worktree: installRoot,
2479
+ source: installedFiles.source,
2480
+ files: fileRecords,
2481
+ sequence: recorded.sequence
2482
+ }, { operationId: opId });
2483
+ } catch (err) {
2484
+ if (existsSync22(destAbs)) {
2485
+ await rm(destAbs, { recursive: true, force: true }).catch(() => null);
2486
+ }
2487
+ return failure("skill-install", String(err?.message ?? err), { operationId: opId, retryable: true });
2488
+ } finally {
2489
+ await rm(stage, { recursive: true, force: true }).catch(() => null);
2490
+ }
2491
+ };
2492
+ }
2493
+ function findStagedSkillDir(stageDir, skillName) {
2494
+ const candidates = [
2495
+ join10(stageDir, ".opencode", "skills", skillName),
2496
+ join10(stageDir, ".agents", "skills", skillName),
2497
+ join10(stageDir, "skills", skillName)
2498
+ ];
2499
+ return candidates.find((path) => existsSync22(path)) ?? null;
2500
+ }
2501
+ async function materialiseFromSkillsCli({ packageSpec, skillName, version, stageDir }) {
2502
+ const cliPkg = `skills@${SKILLS_CLI_VERSION}`;
2503
+ const resolvedPackageSpec = version ? `${packageSpec}@${version}` : packageSpec;
2504
+ const args = [
2505
+ "exec",
2506
+ "--yes",
2507
+ `--package=${cliPkg}`,
2508
+ "--",
2509
+ "skills",
2510
+ "add",
2511
+ resolvedPackageSpec,
2512
+ "--skill",
2513
+ skillName,
2514
+ "--agent",
2515
+ "opencode",
2516
+ "--copy",
2517
+ "-y"
2518
+ ];
2519
+ const result = await new Promise((resolveP, rejectP) => {
2520
+ execFile2(
2521
+ "npm",
2522
+ args,
2523
+ { cwd: stageDir, shell: false, maxBuffer: 1024 * 1024, timeout: SKILLS_INSTALL_TIMEOUT_MS },
2524
+ (err, stdout, stderr) => {
2525
+ if (err) {
2526
+ const code = typeof err?.code === "number" ? err.code : -1;
2527
+ return resolveP({
2528
+ ok: false,
2529
+ retryable: code === -2 || code === 124,
2530
+ message: `skills CLI failed (code ${code}): ${(stderr || stdout || "").toString().trim().split("\n").slice(-5).join(" | ")}`
2531
+ });
2532
+ }
2533
+ resolveP({ ok: true, stdout: stdout?.toString?.() ?? "", stderr: stderr?.toString?.() ?? "" });
2534
+ }
2535
+ );
2536
+ });
2537
+ if (!result.ok) return result;
2538
+ const stagedDir = findStagedSkillDir(stageDir, skillName);
2539
+ if (!stagedDir) {
2540
+ return {
2541
+ ok: false,
2542
+ retryable: false,
2543
+ message: `skills CLI did not produce any of: ${[".opencode/skills", ".agents/skills", "skills"].map((p) => join10(stageDir, p, skillName)).join(", ")}`
2544
+ };
2545
+ }
2546
+ const skillMd = join10(stagedDir, "SKILL.md");
2547
+ if (!existsSync22(skillMd)) {
2548
+ return {
2549
+ ok: false,
2550
+ retryable: false,
2551
+ message: "skills CLI did not produce a SKILL.md"
2552
+ };
2553
+ }
2554
+ return {
2555
+ ok: true,
2556
+ stagedDir,
2557
+ source: {
2558
+ packageSpec: resolvedPackageSpec,
2559
+ skillName,
2560
+ cliPackage: cliPkg,
2561
+ registryId: `${resolvedPackageSpec}/${skillName}`,
2562
+ // The CLI does not currently expose a registry snapshot
2563
+ // hash; we record the staged directory's hash instead so
2564
+ // the audit tool can prove the staged bytes equal the
2565
+ // installed bytes.
2566
+ registrySnapshotHash: hashBytes(Buffer.from(result.stdout + "\n" + result.stderr, "utf8"))
2567
+ }
2568
+ };
2569
+ }
2570
+ async function hashDir(rootDir) {
2571
+ const out = [];
2572
+ await walk(rootDir, rootDir, out);
2573
+ return out;
2574
+ }
2575
+ async function walk(rootDir, currentDir, out) {
2576
+ const { readdir: readdir2 } = await import("node:fs/promises");
2577
+ const entries = await readdir2(currentDir, { withFileTypes: true });
2578
+ for (const e of entries) {
2579
+ const abs = join10(currentDir, e.name);
2580
+ if (e.isDirectory()) {
2581
+ if (e.name === ".git") continue;
2582
+ await walk(rootDir, abs, out);
2583
+ continue;
2584
+ }
2585
+ if (!e.isFile()) continue;
2586
+ const raw = await readFile14(abs);
2587
+ const fileStat = await stat4(abs);
2588
+ out.push({
2589
+ path: abs.slice(rootDir.length + 1).split(sep5).join("/"),
2590
+ sha256: createHash5("sha256").update(raw).digest("hex"),
2591
+ mode: fileStat.mode & 511,
2592
+ size: fileStat.size
2593
+ });
2594
+ }
2595
+ }
2596
+ function hashBytes(bytes) {
2597
+ return createHash5("sha256").update(bytes).digest("hex");
2598
+ }
2599
+ function hashesEqual(a, b) {
2600
+ if (a.length !== b.length) return false;
2601
+ const map = new Map(a.map((f) => [f.path, f.sha256]));
2602
+ for (const f of b) {
2603
+ if (map.get(f.path) !== f.sha256) return false;
2604
+ }
2605
+ return true;
2606
+ }
2607
+ async function copyDir(srcDir, destDir) {
2608
+ await mkdir9(destDir, { recursive: true });
2609
+ const { readdir: readdir2 } = await import("node:fs/promises");
2610
+ const entries = await readdir2(srcDir, { withFileTypes: true });
2611
+ for (const e of entries) {
2612
+ const src = join10(srcDir, e.name);
2613
+ const dest = join10(destDir, e.name);
2614
+ if (e.isDirectory()) {
2615
+ if (e.name === ".git") continue;
2616
+ await copyDir(src, dest);
2617
+ } else if (e.isFile()) {
2618
+ const raw = await readFile14(src);
2619
+ await writeFile11(dest, raw, { mode: 420 });
2620
+ }
2621
+ }
2622
+ }
2623
+ var SAFE_ID_RE, SAFE_NAME_RE, SAFE_VERSION_RE, SKILLS_CLI_VERSION;
2624
+ var init_ship_skill_install = __esm({
2625
+ "src/tools/ship-skill-install.js"() {
2626
+ init_envelope();
2627
+ init_policy();
2628
+ init_inventory();
2629
+ init_worktree();
2630
+ init_registry();
2631
+ SAFE_ID_RE = /^[A-Za-z0-9._-]{1,128}$/;
2632
+ SAFE_NAME_RE = /^[A-Za-z0-9._/-]{1,160}$/;
2633
+ SAFE_VERSION_RE = /^[A-Za-z0-9._+-]{1,64}$/;
2634
+ SKILLS_CLI_VERSION = "1.0.4";
2635
+ }
2636
+ });
2637
+
1614
2638
  // src/installer/cli-args.js
1615
2639
  init_profile();
1616
2640
  var USAGE = `opencode-ship <command> [options]
@@ -1725,24 +2749,25 @@ function helpText() {
1725
2749
 
1726
2750
  // src/installer/commands/init.js
1727
2751
  import { promisify as promisify2 } from "node:util";
1728
- import { writeFile as writeFile8, mkdir as mkdirAsync2 } from "node:fs/promises";
1729
- import { dirname as dirname10, resolve as resolvePath } from "node:path";
2752
+ import { writeFile as writeFile12, mkdir as mkdirAsync2 } from "node:fs/promises";
2753
+ import { dirname as dirname15, resolve as resolvePath } from "node:path";
1730
2754
 
1731
2755
  // src/installer/executor.js
1732
2756
  init_catalog();
1733
2757
  init_version();
1734
- import { existsSync as existsSync13 } from "node:fs";
2758
+ import { existsSync as existsSync14 } from "node:fs";
1735
2759
  import { mkdir as mkdir6, readFile as readFile9, rename as rename5, unlink as unlink3, writeFile as writeFile6 } from "node:fs/promises";
1736
- import { dirname as dirname8, relative as relative2, resolve as resolve12, sep as sep2 } from "node:path";
2760
+ import { dirname as dirname8, relative as relative2, resolve as resolve13, sep as sep2 } from "node:path";
1737
2761
 
1738
2762
  // src/installer/planner.js
1739
2763
  init_catalog();
1740
2764
  init_hash();
1741
2765
  init_config();
1742
2766
  init_lock();
2767
+ init_workflow_models();
1743
2768
  init_json_pointer();
1744
2769
  init_root_config();
1745
- import { existsSync as existsSync8 } from "node:fs";
2770
+ import { existsSync as existsSync9 } from "node:fs";
1746
2771
  import { readFile as readFile4, stat } from "node:fs/promises";
1747
2772
 
1748
2773
  // src/installer/root-reconciliation.js
@@ -1750,7 +2775,7 @@ init_root_config();
1750
2775
  init_json_pointer();
1751
2776
  init_hash();
1752
2777
  init_plan_mode_permissions();
1753
- import { existsSync as existsSync7 } from "node:fs";
2778
+ import { existsSync as existsSync8 } from "node:fs";
1754
2779
  import { join } from "node:path";
1755
2780
 
1756
2781
  // src/installer/root-permissions.js
@@ -1760,15 +2785,29 @@ var ASK = "ask";
1760
2785
  var ALLOW = "allow";
1761
2786
  var DENY = "deny";
1762
2787
  var SUBAGENT_DEPTH = 2;
2788
+ function withShipAliases(ids) {
2789
+ const out = [];
2790
+ const seen = /* @__PURE__ */ new Set();
2791
+ for (const id of ids) {
2792
+ const extras = id.startsWith("delivery_") ? [`ship_${id.slice("delivery_".length)}`] : [];
2793
+ for (const candidate of [id, ...extras]) {
2794
+ if (seen.has(candidate)) continue;
2795
+ seen.add(candidate);
2796
+ out.push(candidate);
2797
+ }
2798
+ }
2799
+ return out;
2800
+ }
1763
2801
  var CONTROLLER_TASK_ALLOW = [
1764
2802
  "ship-planner",
1765
2803
  "ship-task-builder",
1766
2804
  "ship-task-reviewer",
1767
2805
  "ship-final-standards-reviewer",
1768
2806
  "ship-final-spec-reviewer",
2807
+ "ship-verifier",
1769
2808
  "delivery-verifier"
1770
2809
  ];
1771
- var PUBLIC_TOOL_IDS = [
2810
+ var PUBLIC_TOOL_IDS = withShipAliases([
1772
2811
  "delivery_abandon",
1773
2812
  "delivery_cleanup",
1774
2813
  "delivery_github_read",
@@ -1803,8 +2842,8 @@ var PUBLIC_TOOL_IDS = [
1803
2842
  "ship_task_report",
1804
2843
  "ship_task_review",
1805
2844
  "ship_task_start"
1806
- ];
1807
- var BUILD_TOOL_ALLOW = [
2845
+ ]);
2846
+ var BUILD_TOOL_ALLOW = withShipAliases([
1808
2847
  "delivery_cleanup",
1809
2848
  "delivery_inspect",
1810
2849
  "delivery_issue",
@@ -1814,15 +2853,15 @@ var BUILD_TOOL_ALLOW = [
1814
2853
  "ship_deliver",
1815
2854
  "ship_status",
1816
2855
  "ship_resume"
1817
- ];
1818
- var BUILD_TOOL_ASK = [
2856
+ ]);
2857
+ var BUILD_TOOL_ASK = withShipAliases([
1819
2858
  "ship_plan_approve",
1820
2859
  "delivery_merge",
1821
2860
  "delivery_issue_close",
1822
2861
  "delivery_abandon",
1823
2862
  "ship_skill_install"
1824
- ];
1825
- var CONTROLLER_TOOL_ALLOW = [
2863
+ ]);
2864
+ var CONTROLLER_TOOL_ALLOW = withShipAliases([
1826
2865
  "delivery_inspect",
1827
2866
  "delivery_cleanup",
1828
2867
  "delivery_github_read",
@@ -1846,13 +2885,13 @@ var CONTROLLER_TOOL_ALLOW = [
1846
2885
  "ship_skill_install",
1847
2886
  "ship_skill_audit",
1848
2887
  "ship_skill_uninstall"
1849
- ];
1850
- var CONTROLLER_TOOL_ASK = [
2888
+ ]);
2889
+ var CONTROLLER_TOOL_ASK = withShipAliases([
1851
2890
  "ship_plan_approve",
1852
2891
  "delivery_merge",
1853
2892
  "delivery_issue_close",
1854
2893
  "delivery_abandon"
1855
- ];
2894
+ ]);
1856
2895
  var H = "git";
1857
2896
  var RESET = "--hard";
1858
2897
  var PUSH = "--force";
@@ -1896,6 +2935,8 @@ function rootPermissionMatrix() {
1896
2935
  "ship-controller": "allow",
1897
2936
  "general": "allow",
1898
2937
  "plan": "deny",
2938
+ "ship-reviewer": "allow",
2939
+ "ship-verifier": "allow",
1899
2940
  "delivery-reviewer": "allow",
1900
2941
  "delivery-verifier": "allow"
1901
2942
  },
@@ -2741,11 +3782,11 @@ function parseTree(text, errors = [], options = ParseOptions.DEFAULT) {
2741
3782
  onValue({ type: getNodeType(value), offset, length, parent: currentParent, value });
2742
3783
  ensurePropertyComplete(offset + length);
2743
3784
  },
2744
- onSeparator: (sep3, offset, length) => {
3785
+ onSeparator: (sep6, offset, length) => {
2745
3786
  if (currentParent.type === "property") {
2746
- if (sep3 === ":") {
3787
+ if (sep6 === ":") {
2747
3788
  currentParent.colonOffset = offset;
2748
- } else if (sep3 === ",") {
3789
+ } else if (sep6 === ",") {
2749
3790
  ensurePropertyComplete(offset);
2750
3791
  }
2751
3792
  }
@@ -3458,7 +4499,7 @@ async function planRootReconciliation(input) {
3458
4499
  previousDocument: input.previousDocument
3459
4500
  });
3460
4501
  }
3461
- const fileMissing = !existsSync7(target);
4502
+ const fileMissing = !existsSync8(target);
3462
4503
  if (fileMissing && !input.forceRepair) {
3463
4504
  if (mode === "profile-transition") {
3464
4505
  return {
@@ -4105,13 +5146,13 @@ function mergePointerRecords(descriptors, previousRecords, result, beforeSnapsho
4105
5146
 
4106
5147
  // src/installer/planner.js
4107
5148
  async function readBytes(path) {
4108
- if (!existsSync8(path)) return null;
5149
+ if (!existsSync9(path)) return null;
4109
5150
  const buf = await readFile4(path);
4110
5151
  const fileStat = await stat(path);
4111
5152
  return { bytes: buf, hash: bytesHashString(buf.toString("utf8")), mode: fileStat.mode & 511 };
4112
5153
  }
4113
5154
  async function readDesiredBytes(source) {
4114
- if (!source || !existsSync8(source)) return null;
5155
+ if (!source || !existsSync9(source)) return null;
4115
5156
  const buf = await readFile4(source);
4116
5157
  return { bytes: buf, hash: bytesHashString(buf.toString("utf8")) };
4117
5158
  }
@@ -4314,7 +5355,15 @@ async function planUninstall({ repoRoot, lock }) {
4314
5355
  async function planConfigSynthesis({ repoRoot, detection, lock, forceOverwrite, migrationSeed = null, models = null }) {
4315
5356
  const existing = await loadConfig(repoRoot);
4316
5357
  const hasModelFlags = Boolean(models && (models.planner || models.builder || models.finalReviewer));
4317
- if (existing?.ok && !forceOverwrite && !hasModelFlags) {
5358
+ const { current, history } = loadWorkflowModelDefaults();
5359
+ const resolved = resolveWorkflowModels({
5360
+ configModels: existing?.ok ? existing.value.workflow?.models ?? {} : {},
5361
+ lockModels: lock?.manager?.models ?? null,
5362
+ cliModels: models,
5363
+ current,
5364
+ history
5365
+ });
5366
+ if (existing?.ok && !forceOverwrite && resolved.changedRoles.length === 0) {
4318
5367
  return {
4319
5368
  kind: "noop",
4320
5369
  op: "config",
@@ -4323,35 +5372,34 @@ async function planConfigSynthesis({ repoRoot, detection, lock, forceOverwrite,
4323
5372
  currentSha: existing.sha256,
4324
5373
  desiredSha: existing.sha256,
4325
5374
  configValue: existing.value,
5375
+ modelsProvenance: resolved.provenance,
4326
5376
  reason: "user config already present"
4327
5377
  };
4328
5378
  }
4329
5379
  let desiredValue = migrationSeed ?? (existing?.ok ? structuredClone(existing.value) : renderDefaultConfig(detection));
4330
- if (hasModelFlags) {
4331
- desiredValue = {
4332
- ...desiredValue,
4333
- schemaVersion: 2,
4334
- profile: "engineering",
4335
- workflow: {
4336
- ...desiredValue.workflow ?? {},
4337
- models: {
4338
- planner: models.planner ?? desiredValue?.workflow?.models?.planner,
4339
- builder: models.builder ?? desiredValue?.workflow?.models?.builder,
4340
- finalReviewer: models.finalReviewer ?? desiredValue?.workflow?.models?.finalReviewer
4341
- },
4342
- approval: {
4343
- mirrorToIssue: true,
4344
- maxFailedRounds: 3,
4345
- ...desiredValue?.workflow?.approval ?? {}
4346
- }
4347
- }
4348
- };
4349
- }
4350
5380
  if (desiredValue.profile === "core") desiredValue.profile = "engineering";
5381
+ desiredValue.workflow = {
5382
+ ...desiredValue.workflow ?? {},
5383
+ models: resolved.models,
5384
+ approval: {
5385
+ mirrorToIssue: true,
5386
+ maxFailedRounds: 3,
5387
+ ...desiredValue.workflow?.approval ?? {}
5388
+ }
5389
+ };
4351
5390
  const desiredJson = JSON.stringify(desiredValue, null, 2) + "\n";
4352
5391
  const desiredSha = bytesHashString(desiredJson);
4353
- const kind = existing?.ok && (forceOverwrite || hasModelFlags) ? "update" : "create";
4354
- const reason = existing?.ok ? hasModelFlags && !forceOverwrite ? "patching workflow.models from CLI model flags" : "user config overwritten via --force-config" : migrationSeed ? "synthesising a default config from legacy adapter migration" : "synthesising a default config from detection";
5392
+ const kind = existing?.ok ? "update" : "create";
5393
+ let reason;
5394
+ if (!existing?.ok) {
5395
+ reason = migrationSeed ? "synthesising a default config from legacy adapter migration" : "synthesising a default config from detection";
5396
+ } else if (forceOverwrite) {
5397
+ reason = "user config overwritten via --force-config";
5398
+ } else if (hasModelFlags) {
5399
+ reason = "patching workflow.models from CLI model flags";
5400
+ } else {
5401
+ reason = "applying packaged workflow model defaults";
5402
+ }
4355
5403
  return {
4356
5404
  kind,
4357
5405
  op: "config",
@@ -4361,6 +5409,7 @@ async function planConfigSynthesis({ repoRoot, detection, lock, forceOverwrite,
4361
5409
  desiredSha,
4362
5410
  bytes: Buffer.from(desiredJson, "utf8"),
4363
5411
  configValue: desiredValue,
5412
+ modelsProvenance: resolved.provenance,
4364
5413
  reason
4365
5414
  };
4366
5415
  }
@@ -4387,8 +5436,8 @@ init_json_pointer();
4387
5436
 
4388
5437
  // src/installer/detection/project.js
4389
5438
  import { spawnSync } from "node:child_process";
4390
- import { existsSync as existsSync9, readFileSync as readFileSync4 } from "node:fs";
4391
- import { resolve as resolve7, join as join2 } from "node:path";
5439
+ import { existsSync as existsSync10, readFileSync as readFileSync5 } from "node:fs";
5440
+ import { resolve as resolve8, join as join2 } from "node:path";
4392
5441
  function runGit(cwd, args) {
4393
5442
  const r = spawnSync("git", ["-C", cwd, ...args], {
4394
5443
  stdio: ["ignore", "pipe", "pipe"],
@@ -4397,17 +5446,17 @@ function runGit(cwd, args) {
4397
5446
  return { status: r.status ?? -1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
4398
5447
  }
4399
5448
  function detectPackageManager(repoRoot) {
4400
- if (existsSync9(join2(repoRoot, "pnpm-lock.yaml"))) return "pnpm";
4401
- if (existsSync9(join2(repoRoot, "yarn.lock"))) return "yarn";
4402
- if (existsSync9(join2(repoRoot, "bun.lockb"))) return "bun";
4403
- if (existsSync9(join2(repoRoot, "package-lock.json"))) return "npm";
5449
+ if (existsSync10(join2(repoRoot, "pnpm-lock.yaml"))) return "pnpm";
5450
+ if (existsSync10(join2(repoRoot, "yarn.lock"))) return "yarn";
5451
+ if (existsSync10(join2(repoRoot, "bun.lockb"))) return "bun";
5452
+ if (existsSync10(join2(repoRoot, "package-lock.json"))) return "npm";
4404
5453
  return null;
4405
5454
  }
4406
5455
  function readPackageJson(repoRoot) {
4407
5456
  const path = join2(repoRoot, "package.json");
4408
- if (!existsSync9(path)) return null;
5457
+ if (!existsSync10(path)) return null;
4409
5458
  try {
4410
- return JSON.parse(readFileSync4(path, "utf8"));
5459
+ return JSON.parse(readFileSync5(path, "utf8"));
4411
5460
  } catch {
4412
5461
  return null;
4413
5462
  }
@@ -4482,7 +5531,7 @@ function detectOwner(repoRoot) {
4482
5531
  }
4483
5532
  function detectProject(repoRoot = process.cwd()) {
4484
5533
  const errors = [];
4485
- const cwd = resolve7(repoRoot);
5534
+ const cwd = resolve8(repoRoot);
4486
5535
  const inside = runGit(cwd, ["rev-parse", "--show-toplevel"]);
4487
5536
  if (inside.status !== 0) {
4488
5537
  errors.push({ kind: "not-a-git-repo", path: cwd, detail: inside.stderr.trim() });
@@ -4544,12 +5593,12 @@ import {
4544
5593
  stat as stat3,
4545
5594
  open
4546
5595
  } from "node:fs/promises";
4547
- import { existsSync as existsSync11 } from "node:fs";
4548
- import { dirname as dirname6, resolve as resolve10, join as join5 } from "node:path";
5596
+ import { existsSync as existsSync12 } from "node:fs";
5597
+ import { dirname as dirname6, resolve as resolve11, join as join5 } from "node:path";
4549
5598
 
4550
5599
  // src/state/git-common-dir.js
4551
5600
  import { spawn } from "node:child_process";
4552
- import { resolve as resolve8, join as join3 } from "node:path";
5601
+ import { resolve as resolve9, join as join3 } from "node:path";
4553
5602
  var STATE_DIRNAME = "opencode-ship";
4554
5603
  async function resolveGitCommonDir(repoRoot) {
4555
5604
  if (typeof repoRoot !== "string" || repoRoot.length === 0) {
@@ -4582,7 +5631,7 @@ async function resolveGitCommonDir(repoRoot) {
4582
5631
  reject(new Error("git rev-parse --git-common-dir returned an empty path"));
4583
5632
  return;
4584
5633
  }
4585
- resolveP(resolve8(repoRoot, trimmed));
5634
+ resolveP(resolve9(repoRoot, trimmed));
4586
5635
  });
4587
5636
  });
4588
5637
  }
@@ -4605,8 +5654,8 @@ import {
4605
5654
  unlink,
4606
5655
  stat as stat2
4607
5656
  } from "node:fs/promises";
4608
- import { existsSync as existsSync10 } from "node:fs";
4609
- import { dirname as dirname5, join as join4, resolve as resolve9 } from "node:path";
5657
+ import { existsSync as existsSync11 } from "node:fs";
5658
+ import { dirname as dirname5, join as join4, resolve as resolve10 } from "node:path";
4610
5659
  import { createHash as createHash2, randomBytes } from "node:crypto";
4611
5660
  import { hostname as osHostname } from "node:os";
4612
5661
  var STALE_LOCK_MS = 120 * 1e3;
@@ -4631,7 +5680,7 @@ async function atomicReplaceJson(path, value) {
4631
5680
  if (typeof path !== "string" || path.length === 0) {
4632
5681
  throw new Error("atomicReplaceJson: path must be a non-empty string");
4633
5682
  }
4634
- const target = resolve9(path);
5683
+ const target = resolve10(path);
4635
5684
  const parent = dirname5(target);
4636
5685
  await mkdir3(parent, { recursive: true });
4637
5686
  const tmp = `${target}.${randomToken()}.tmp`;
@@ -4655,10 +5704,10 @@ async function lockDirForRepo(repoRoot) {
4655
5704
  return lockDirFromCommonDir(commonDir);
4656
5705
  }
4657
5706
  function transactionLockPath(lockDir) {
4658
- return resolve10(lockDir, ".txn.lock");
5707
+ return resolve11(lockDir, ".txn.lock");
4659
5708
  }
4660
5709
  function journalPath(lockDir, txnId) {
4661
- return resolve10(lockDir, `.txn-${txnId}.journal`);
5710
+ return resolve11(lockDir, `.txn-${txnId}.journal`);
4662
5711
  }
4663
5712
  function backupPath(target, token) {
4664
5713
  return `${target}.txn-${token}-backup`;
@@ -4693,7 +5742,7 @@ async function releaseLock(lockDir) {
4693
5742
  }
4694
5743
  async function liveLockOwner(lockDir) {
4695
5744
  const path = transactionLockPath(lockDir);
4696
- if (!existsSync11(path)) return false;
5745
+ if (!existsSync12(path)) return false;
4697
5746
  try {
4698
5747
  const lock = JSON.parse(await readFile6(path, "utf8"));
4699
5748
  if (!Number.isInteger(lock?.pid) || lock.pid <= 0) return false;
@@ -4739,7 +5788,7 @@ async function clearJournal(lockDir, txnId) {
4739
5788
  }
4740
5789
  }
4741
5790
  async function readJournal(lockDir, name) {
4742
- const path = resolve10(lockDir, name);
5791
+ const path = resolve11(lockDir, name);
4743
5792
  let raw;
4744
5793
  try {
4745
5794
  raw = await readFile6(path, "utf8");
@@ -4760,7 +5809,7 @@ async function readJournal(lockDir, name) {
4760
5809
  async function isCommitted(journal) {
4761
5810
  if (journal.committed) return true;
4762
5811
  const marker = journal.ledger?.find((entry) => entry.commitMarker);
4763
- if (!marker?.target || !marker.installedSha256 || !existsSync11(marker.target)) return false;
5812
+ if (!marker?.target || !marker.installedSha256 || !existsSync12(marker.target)) return false;
4764
5813
  try {
4765
5814
  return bytesHashString(await readFile6(marker.target, "utf8")) === marker.installedSha256;
4766
5815
  } catch {
@@ -4769,13 +5818,13 @@ async function isCommitted(journal) {
4769
5818
  }
4770
5819
  async function restoreEntry(entry) {
4771
5820
  if (entry.op === "write") {
4772
- if (entry.backup && existsSync11(entry.backup)) {
5821
+ if (entry.backup && existsSync12(entry.backup)) {
4773
5822
  await rename4(entry.backup, entry.target);
4774
- } else if (entry.hadOriginal === false && existsSync11(entry.target)) {
5823
+ } else if (entry.hadOriginal === false && existsSync12(entry.target)) {
4775
5824
  await unlink2(entry.target);
4776
5825
  }
4777
- if (entry.staged && existsSync11(entry.staged)) await unlink2(entry.staged);
4778
- } else if (entry.op === "delete" && entry.backup && existsSync11(entry.backup)) {
5826
+ if (entry.staged && existsSync12(entry.staged)) await unlink2(entry.staged);
5827
+ } else if (entry.op === "delete" && entry.backup && existsSync12(entry.backup)) {
4779
5828
  await rename4(entry.backup, entry.target);
4780
5829
  }
4781
5830
  await fsyncDir2(dirname6(entry.target));
@@ -4798,11 +5847,11 @@ async function recoverJournal(lockDir, name) {
4798
5847
  complete = false;
4799
5848
  }
4800
5849
  }
4801
- if (complete) await unlink2(resolve10(lockDir, name)).catch(() => null);
5850
+ if (complete) await unlink2(resolve11(lockDir, name)).catch(() => null);
4802
5851
  return { ok: complete };
4803
5852
  }
4804
5853
  async function recover(repoRoot, lockDir) {
4805
- if (!existsSync11(lockDir)) return { recovered: false, recoveredCount: 0 };
5854
+ if (!existsSync12(lockDir)) return { recovered: false, recoveredCount: 0 };
4806
5855
  if (await liveLockOwner(lockDir)) {
4807
5856
  return { recovered: false, recoveredCount: 0, blocked: true };
4808
5857
  }
@@ -4836,11 +5885,11 @@ async function recover(repoRoot, lockDir) {
4836
5885
  }
4837
5886
  async function commitEntry(entry) {
4838
5887
  let changed = false;
4839
- if (entry.backup && existsSync11(entry.backup)) {
5888
+ if (entry.backup && existsSync12(entry.backup)) {
4840
5889
  await unlink2(entry.backup);
4841
5890
  changed = true;
4842
5891
  }
4843
- if (entry.staged && existsSync11(entry.staged)) {
5892
+ if (entry.staged && existsSync12(entry.staged)) {
4844
5893
  await unlink2(entry.staged);
4845
5894
  changed = true;
4846
5895
  }
@@ -4870,7 +5919,7 @@ async function executePlan({ repoRoot, plan, newLockBuilder }) {
4870
5919
  const backup = backupPath(op.target, token);
4871
5920
  const staged = stagedPath(op.target, token);
4872
5921
  if (op.kind === "delete") {
4873
- if (!existsSync11(op.target)) continue;
5922
+ if (!existsSync12(op.target)) continue;
4874
5923
  journal.entries.push({
4875
5924
  op: "delete",
4876
5925
  target: op.target,
@@ -4883,7 +5932,7 @@ async function executePlan({ repoRoot, plan, newLockBuilder }) {
4883
5932
  await rename4(op.target, backup);
4884
5933
  } else {
4885
5934
  await mkdirp(dirname6(op.target));
4886
- const hadOriginal = existsSync11(op.target);
5935
+ const hadOriginal = existsSync12(op.target);
4887
5936
  journal.entries.push({
4888
5937
  op: "write",
4889
5938
  target: op.target,
@@ -4909,7 +5958,7 @@ async function executePlan({ repoRoot, plan, newLockBuilder }) {
4909
5958
  const token = randomToken2();
4910
5959
  const backup = backupPath(lockPathTarget, token);
4911
5960
  const staged = stagedPath(lockPathTarget, token);
4912
- const hadOriginal = existsSync11(lockPathTarget);
5961
+ const hadOriginal = existsSync12(lockPathTarget);
4913
5962
  const finalLock = { ...lockValue, integrity: computeIntegrity(lockValue) };
4914
5963
  const lockBytes = JSON.stringify(finalLock, null, 2) + "\n";
4915
5964
  journal.entries.push({
@@ -4973,16 +6022,16 @@ async function rollback(lockDir, journal) {
4973
6022
  init_lock();
4974
6023
  init_config();
4975
6024
  import { readFile as readFile7 } from "node:fs/promises";
4976
- import { existsSync as existsSync12 } from "node:fs";
4977
- import { resolve as resolve11 } from "node:path";
6025
+ import { existsSync as existsSync13 } from "node:fs";
6026
+ import { resolve as resolve12 } from "node:path";
4978
6027
  function legacyAdapterPath(repoRoot) {
4979
- return resolve11(repoRoot, ".opencode", "delivery.json");
6028
+ return resolve12(repoRoot, ".opencode", "delivery.json");
4980
6029
  }
4981
6030
  function legacyLockPath(repoRoot) {
4982
- return resolve11(repoRoot, ".opencode", "delivery.lock.json");
6031
+ return resolve12(repoRoot, ".opencode", "delivery.lock.json");
4983
6032
  }
4984
6033
  function legacyPluginPath(repoRoot) {
4985
- return resolve11(repoRoot, ".opencode", "plugin", "delivery.ts");
6034
+ return resolve12(repoRoot, ".opencode", "plugin", "delivery.ts");
4986
6035
  }
4987
6036
  async function detectLegacyShapes(repoRoot) {
4988
6037
  const out = {
@@ -4993,17 +6042,17 @@ async function detectLegacyShapes(repoRoot) {
4993
6042
  reviewer: false,
4994
6043
  verifier: false
4995
6044
  };
4996
- if (existsSync12(legacyAdapterPath(repoRoot))) out.adapter = true;
4997
- if (existsSync12(legacyLockPath(repoRoot))) out.legacyLock = true;
4998
- if (existsSync12(legacyPluginPath(repoRoot))) out.plugin = true;
4999
- if (existsSync12(resolve11(repoRoot, ".opencode/plugin/opencode-ship.js"))) out.pluginOld = true;
5000
- if (existsSync12(resolve11(repoRoot, ".opencode/agents/delivery-reviewer.md"))) out.reviewer = true;
5001
- if (existsSync12(resolve11(repoRoot, ".opencode/agents/delivery-verifier.md"))) out.verifier = true;
6045
+ if (existsSync13(legacyAdapterPath(repoRoot))) out.adapter = true;
6046
+ if (existsSync13(legacyLockPath(repoRoot))) out.legacyLock = true;
6047
+ if (existsSync13(legacyPluginPath(repoRoot))) out.plugin = true;
6048
+ if (existsSync13(resolve12(repoRoot, ".opencode/plugin/opencode-ship.js"))) out.pluginOld = true;
6049
+ if (existsSync13(resolve12(repoRoot, ".opencode/agents/delivery-reviewer.md"))) out.reviewer = true;
6050
+ if (existsSync13(resolve12(repoRoot, ".opencode/agents/delivery-verifier.md"))) out.verifier = true;
5002
6051
  return out;
5003
6052
  }
5004
6053
  async function readLegacyAdapter(repoRoot) {
5005
6054
  const path = legacyAdapterPath(repoRoot);
5006
- if (!existsSync12(path)) return null;
6055
+ if (!existsSync13(path)) return null;
5007
6056
  try {
5008
6057
  const raw = await readFile7(path, "utf8");
5009
6058
  return { path, raw, value: JSON.parse(raw) };
@@ -5024,13 +6073,13 @@ async function migration({ repoRoot, lock, forceRepair, detection = null }) {
5024
6073
  if (legacy && shapes.legacyLock && !lock?.manager) {
5025
6074
  actions.push({ kind: "kept-legacy-lock", path: legacyLockPath(repoRoot) });
5026
6075
  }
5027
- if (shapes.plugin && existsSync12(resolve11(repoRoot, ".opencode/plugins/opencode-ship.js"))) {
6076
+ if (shapes.plugin && existsSync13(resolve12(repoRoot, ".opencode/plugins/opencode-ship.js"))) {
5028
6077
  if (!forceRepair) {
5029
6078
  actions.push({ kind: "candidate-remove-legacy-plugin", path: legacyPluginPath(repoRoot) });
5030
6079
  }
5031
6080
  }
5032
6081
  if (shapes.pluginOld) {
5033
- actions.push({ kind: "candidate-remove-legacy-plugin-path", path: resolve11(repoRoot, ".opencode/plugin/opencode-ship.js") });
6082
+ actions.push({ kind: "candidate-remove-legacy-plugin-path", path: resolve12(repoRoot, ".opencode/plugin/opencode-ship.js") });
5034
6083
  }
5035
6084
  return { shapes, actions, legacyPresent: Boolean(legacy), proposedConfigSeed };
5036
6085
  }
@@ -5078,7 +6127,7 @@ function legacyToShipConfig(legacy, detection = null) {
5078
6127
  // src/installer/executor.js
5079
6128
  init_profile();
5080
6129
  async function readCurrentBytes(targetPath) {
5081
- if (!existsSync13(targetPath)) return null;
6130
+ if (!existsSync14(targetPath)) return null;
5082
6131
  const buf = await readFile9(targetPath);
5083
6132
  return { bytes: buf, hash: bytesHashString(buf.toString("utf8")) };
5084
6133
  }
@@ -5169,7 +6218,7 @@ async function previewInstall({ rootPath, profile = null, replaceManaged, forceC
5169
6218
  });
5170
6219
  const planMode = null;
5171
6220
  const rootPlan = await planRootConfigApply({ repoRoot, lock, forceRepair: Boolean(forceRootConfig), planMode });
5172
- const setupPending = resolved.profile === "engineering" && !hasCompletedModels(configValue);
6221
+ const setupPending = resolved.profile === "engineering" && !hasCompletedModels(configPlan.configValue);
5173
6222
  const plan = [...filePlan ?? [], ...staleFilePlan, ...migrationPlan, configPlan, rootPlan];
5174
6223
  const conflicts = plan.filter((p) => p && p.kind === "conflict");
5175
6224
  const summary = summarise(plan);
@@ -5185,7 +6234,8 @@ async function previewInstall({ rootPath, profile = null, replaceManaged, forceC
5185
6234
  conflicts,
5186
6235
  summary,
5187
6236
  migrationReport,
5188
- setupPending
6237
+ setupPending,
6238
+ modelsProvenance: configPlan.modelsProvenance
5189
6239
  };
5190
6240
  }
5191
6241
  async function previewUninstall({ rootPath }) {
@@ -5220,7 +6270,7 @@ function summarise(plan) {
5220
6270
  }
5221
6271
  return counts;
5222
6272
  }
5223
- async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profile = null, models = null, fullSetupComplete = false }) {
6273
+ async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profile = null, models = null, modelsProvenance = null, fullSetupComplete = false }) {
5224
6274
  const files = [];
5225
6275
  const remain = lock?.files?.filter((f) => !plan.some((op) => op?.relPath === f.path)) ?? [];
5226
6276
  for (const op of plan) {
@@ -5256,7 +6306,7 @@ async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profil
5256
6306
  manager: {
5257
6307
  schemaVersion: CURRENT_LOCK_SCHEMA,
5258
6308
  name: "opencode-ship",
5259
- version: "1.1.8",
6309
+ version: "1.1.9-rc.1",
5260
6310
  templateSet: TEMPLATE_SET_ID,
5261
6311
  profile: resolvedProfile,
5262
6312
  appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -5266,6 +6316,7 @@ async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profil
5266
6316
  sha256: configSha ?? lock?.manager?.config?.sha256 ?? "",
5267
6317
  existed: Boolean(lock?.manager?.config?.existed)
5268
6318
  },
6319
+ ...modelsProvenance ? { models: modelsProvenance } : {},
5269
6320
  rootDocuments: hasRootDocuments && (hasRootPlan || (lock?.manager?.rootDocuments?.length ?? 0) > 0) ? [{
5270
6321
  path: rootPlan?.relPath ?? lock?.manager?.rootDocuments?.[0]?.path ?? "opencode.json",
5271
6322
  format: rootPlan?.format ?? lock?.manager?.rootDocuments?.[0]?.format ?? "json",
@@ -5312,6 +6363,7 @@ async function commitInstall(preview, { json, command, fullSetupComplete = false
5312
6363
  configPlan,
5313
6364
  rootPlan,
5314
6365
  profile: preview.profile?.profile,
6366
+ modelsProvenance: preview.modelsProvenance,
5315
6367
  fullSetupComplete
5316
6368
  });
5317
6369
  const txPlan = await stageFiles(fileOnly, repoRoot);
@@ -5403,12 +6455,12 @@ function relativeTemplate(source) {
5403
6455
  // src/installer/commands/doctor.js
5404
6456
  init_lock();
5405
6457
  init_config();
5406
- import { existsSync as existsSync15, readFileSync as readFileSync5 } from "node:fs";
6458
+ import { existsSync as existsSync16, readFileSync as readFileSync6 } from "node:fs";
5407
6459
  import { spawnSync as spawnSync2 } from "node:child_process";
5408
6460
  init_hash();
5409
6461
  init_catalog();
5410
6462
  init_root_config();
5411
- import { resolve as resolve14 } from "node:path";
6463
+ import { resolve as resolve15 } from "node:path";
5412
6464
 
5413
6465
  // src/installer/report.js
5414
6466
  var REPORT_VERSION = 1;
@@ -5459,6 +6511,7 @@ function renderJson({ command, plan, conflicts, summary, diagnostics = [], exitC
5459
6511
 
5460
6512
  // src/installer/commands/doctor.js
5461
6513
  init_profile();
6514
+ init_workflow_models();
5462
6515
  function checkNode() {
5463
6516
  return { name: "node>=22.6.0", ok: /^v2[2-9]/.test(process.version), detail: process.version };
5464
6517
  }
@@ -5497,9 +6550,9 @@ function checkPackageIntegrity() {
5497
6550
  function buildSourceHashIndex() {
5498
6551
  const idx = /* @__PURE__ */ new Map();
5499
6552
  for (const entry of CATALOG) {
5500
- if (!existsSync15(entry.source)) continue;
6553
+ if (!existsSync16(entry.source)) continue;
5501
6554
  try {
5502
- const buf = readFileSync5(entry.source, "utf8");
6555
+ const buf = readFileSync6(entry.source, "utf8");
5503
6556
  idx.set(entry.source, bytesHashString(buf));
5504
6557
  } catch {
5505
6558
  }
@@ -5510,13 +6563,13 @@ async function checkCatalogInstall(repoRoot, sourceHashes, profile, renderedAgen
5510
6563
  const rows = [];
5511
6564
  const scoped = profile ? filterCatalogByProfile(CATALOG, profile) : CATALOG;
5512
6565
  for (const entry of scoped) {
5513
- const target = resolve14(repoRoot, entry.path);
5514
- if (!existsSync15(target)) {
6566
+ const target = resolve15(repoRoot, entry.path);
6567
+ if (!existsSync16(target)) {
5515
6568
  rows.push(`${entry.id}: missing`);
5516
6569
  continue;
5517
6570
  }
5518
6571
  try {
5519
- const buf = readFileSync5(target, "utf8");
6572
+ const buf = readFileSync6(target, "utf8");
5520
6573
  const actual = bytesHashString(buf);
5521
6574
  const rendered = renderedAgentMap.get(entry.path);
5522
6575
  const expected = rendered ? rendered.sha256 : sourceHashes.get(entry.source);
@@ -5572,12 +6625,12 @@ async function checkManagedHashes(repoRoot, validatedLock) {
5572
6625
  const drift = [];
5573
6626
  const renderedAgents = await loadRenderedAgentOverrides(repoRoot);
5574
6627
  for (const entry of validatedLock.lock.files ?? []) {
5575
- const p = resolve14(repoRoot, entry.path);
5576
- if (!existsSync15(p)) {
6628
+ const p = resolve15(repoRoot, entry.path);
6629
+ if (!existsSync16(p)) {
5577
6630
  drift.push(`missing:${entry.path}`);
5578
6631
  continue;
5579
6632
  }
5580
- const buf = readFileSync5(p, "utf8");
6633
+ const buf = readFileSync6(p, "utf8");
5581
6634
  const actual = bytesHashString(buf);
5582
6635
  if (actual !== entry.sha256) drift.push(`drift:${entry.path}`);
5583
6636
  }
@@ -5627,6 +6680,41 @@ async function checkRootConfig(repoRoot) {
5627
6680
  detail: conflict ? `conflict on ${conflict.pointer}` : `applied=${r.applied.length}, skipped=${r.skipped.length}`
5628
6681
  };
5629
6682
  }
6683
+ async function checkWorkflowModelDefaults(repoRoot) {
6684
+ const cfg = await loadConfig(repoRoot);
6685
+ const lockResult = await readValidatedLock(repoRoot);
6686
+ if (!cfg?.ok) {
6687
+ return { name: "workflow model defaults", ok: true, detail: "no config; n/a" };
6688
+ }
6689
+ const { current, history } = loadWorkflowModelDefaults();
6690
+ const resolved = resolveWorkflowModels({
6691
+ configModels: cfg.value.workflow?.models ?? {},
6692
+ lockModels: lockResult.kind === "ok" ? lockResult.lock.manager?.models ?? null : null,
6693
+ cliModels: null,
6694
+ current,
6695
+ history
6696
+ });
6697
+ const stale = [];
6698
+ const parts = [];
6699
+ for (const role of ["planner", "builder", "finalReviewer"]) {
6700
+ parts.push(`${role}=${resolved.provenance[role].source}`);
6701
+ if (resolved.provenance[role].source === "default" && resolved.models[role] !== current[role]) {
6702
+ stale.push(role);
6703
+ }
6704
+ const live = cfg.value.workflow?.models?.[role];
6705
+ if (resolved.provenance[role].source === "default" && live && live !== current[role]) {
6706
+ if (!stale.includes(role)) stale.push(role);
6707
+ }
6708
+ }
6709
+ if (stale.length) {
6710
+ return {
6711
+ name: "workflow model defaults",
6712
+ ok: false,
6713
+ detail: `stale default; run update (${stale.join(", ")}); ${parts.join(",")}`
6714
+ };
6715
+ }
6716
+ return { name: "workflow model defaults", ok: true, detail: parts.join(",") };
6717
+ }
5630
6718
  async function checkSetupState(repoRoot, configValue) {
5631
6719
  const { setupComplete: setupComplete2 } = await Promise.resolve().then(() => (init_setup_state(), setup_state_exports));
5632
6720
  const state = await setupComplete2(repoRoot, configValue);
@@ -5687,7 +6775,8 @@ async function runDoctor({ rootPath, profile, json, writeOutput = true }) {
5687
6775
  await checkManagedHashes(repoRoot, validatedLock),
5688
6776
  await checkActiveProfileFootprint(repoRoot, validatedLock, resolved.profile),
5689
6777
  await checkRootConfig(repoRoot),
5690
- await checkSetupState(repoRoot, configValue)
6778
+ await checkSetupState(repoRoot, configValue),
6779
+ await checkWorkflowModelDefaults(repoRoot)
5691
6780
  ];
5692
6781
  const issues = checks.filter((c) => !c.ok).map((c) => `${c.name}: ${c.detail}`);
5693
6782
  const plan = checks.map((c) => ({
@@ -5709,20 +6798,19 @@ async function runDoctor({ rootPath, profile, json, writeOutput = true }) {
5709
6798
 
5710
6799
  // src/installer/commands/init.js
5711
6800
  init_catalog();
5712
- init_config();
5713
6801
 
5714
6802
  // src/installer/setup-pending.js
5715
- import { existsSync as existsSync16, readFileSync as readFileSync6, unlinkSync, writeFile as writeFile7, mkdir as mkdirAsync } from "node:fs";
6803
+ import { existsSync as existsSync17, readFileSync as readFileSync7, unlinkSync, writeFile as writeFile7, mkdir as mkdirAsync } from "node:fs";
5716
6804
  import { promisify } from "node:util";
5717
- import { resolve as resolve15, dirname as dirname9 } from "node:path";
6805
+ import { resolve as resolve16, dirname as dirname9 } from "node:path";
5718
6806
  var writeFileAsync = promisify(writeFile7);
5719
6807
  var mkdirAsyncAsync = promisify(mkdirAsync);
5720
6808
  var REL_PATH = ".opencode/ship.setup-pending.json";
5721
6809
  function setupPendingPath(repoRoot) {
5722
- return resolve15(repoRoot, REL_PATH);
6810
+ return resolve16(repoRoot, REL_PATH);
5723
6811
  }
5724
6812
  function isSetupPending(repoRoot) {
5725
- return existsSync16(setupPendingPath(repoRoot));
6813
+ return existsSync17(setupPendingPath(repoRoot));
5726
6814
  }
5727
6815
  async function writeSetupPending(repoRoot, payload) {
5728
6816
  const path = setupPendingPath(repoRoot);
@@ -5731,8 +6819,161 @@ async function writeSetupPending(repoRoot, payload) {
5731
6819
  }
5732
6820
  var SETUP_PENDING_REL_PATH2 = REL_PATH;
5733
6821
 
6822
+ // src/skills/sync.js
6823
+ init_registry();
6824
+ init_policy();
6825
+
6826
+ // src/skills/stack-queries.js
6827
+ import { readFileSync as readFileSync9 } from "node:fs";
6828
+ import { join as join8 } from "node:path";
6829
+ var STACK_DEP_QUERIES = {
6830
+ react: "react",
6831
+ "react-dom": "react",
6832
+ next: "nextjs",
6833
+ vitest: "vitest",
6834
+ playwright: "playwright",
6835
+ tailwindcss: "tailwind",
6836
+ express: "express",
6837
+ fastify: "fastify",
6838
+ prisma: "prisma",
6839
+ "drizzle-orm": "drizzle"
6840
+ };
6841
+ function stackQueries({ packageJson, issueText } = {}) {
6842
+ const found = [];
6843
+ const seen = /* @__PURE__ */ new Set();
6844
+ const add = (q) => {
6845
+ if (!q || seen.has(q) || found.length >= 5) return;
6846
+ seen.add(q);
6847
+ found.push(q);
6848
+ };
6849
+ const deps = {
6850
+ ...packageJson?.dependencies ?? {},
6851
+ ...packageJson?.devDependencies ?? {}
6852
+ };
6853
+ for (const name of Object.keys(deps)) {
6854
+ add(STACK_DEP_QUERIES[name]);
6855
+ }
6856
+ const text = String(issueText ?? "");
6857
+ if (/playwright/i.test(text)) add("playwright");
6858
+ if (/\breact\b/i.test(text)) add("react");
6859
+ if (/nextjs|next\.js|\bnext\b/i.test(text)) add("nextjs");
6860
+ if (/vitest|testing library/i.test(text)) add("vitest");
6861
+ if (/tailwind/i.test(text)) add("tailwind");
6862
+ return found;
6863
+ }
6864
+ function readPackageJson2(repoRoot) {
6865
+ try {
6866
+ const raw = readFileSync9(join8(repoRoot, "package.json"), "utf8");
6867
+ const parsed = JSON.parse(raw);
6868
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
6869
+ return parsed;
6870
+ } catch {
6871
+ return null;
6872
+ }
6873
+ }
6874
+
6875
+ // src/skills/sync.js
6876
+ var MANAGED_SKILL_NAMES = /* @__PURE__ */ new Set([
6877
+ "ship-workflow",
6878
+ "delivery-workflow",
6879
+ "planning-research-checkpoint",
6880
+ "skill-discovery",
6881
+ "setup-ship-workflow"
6882
+ ]);
6883
+ async function syncSkills({
6884
+ repoRoot,
6885
+ mode: _mode,
6886
+ issueText = "",
6887
+ listSkillsFn,
6888
+ installFn,
6889
+ policy
6890
+ } = {}) {
6891
+ const queries = stackQueries({
6892
+ packageJson: readPackageJson2(repoRoot),
6893
+ issueText
6894
+ });
6895
+ const installed = [];
6896
+ const skippedUntrusted = [];
6897
+ const skippedPolicy = [];
6898
+ const errors = [];
6899
+ let registryUnavailable = false;
6900
+ const seen = /* @__PURE__ */ new Set();
6901
+ const resolvedPolicy = policy ?? await readPolicy(repoRoot);
6902
+ const listFn = listSkillsFn ?? listSkills;
6903
+ const cap = resolvedPolicy.maxTrustedPerRun ?? 5;
6904
+ for (const query of queries) {
6905
+ let result;
6906
+ try {
6907
+ result = await listFn({ repoRoot, query });
6908
+ } catch (err) {
6909
+ registryUnavailable = true;
6910
+ errors.push(String(err?.message ?? err));
6911
+ continue;
6912
+ }
6913
+ if (!result?.ok) {
6914
+ registryUnavailable = true;
6915
+ errors.push(result?.error?.kind ?? "registry-unavailable");
6916
+ continue;
6917
+ }
6918
+ for (const candidate of result.candidates ?? []) {
6919
+ const skillName = candidate.skill;
6920
+ const pkg = candidate.package;
6921
+ if (!skillName || seen.has(skillName)) continue;
6922
+ if (MANAGED_SKILL_NAMES.has(skillName)) {
6923
+ skippedPolicy.push({ package: pkg, skillName, reason: "managed-skill" });
6924
+ seen.add(skillName);
6925
+ continue;
6926
+ }
6927
+ const decision = isAutoInstallable(candidate, resolvedPolicy);
6928
+ if (!decision.ok) {
6929
+ const entry = { package: pkg, skillName, reason: decision.reason };
6930
+ if (decision.reason === "untrusted-owner") skippedUntrusted.push(entry);
6931
+ else skippedPolicy.push(entry);
6932
+ seen.add(skillName);
6933
+ continue;
6934
+ }
6935
+ if (installed.length >= cap) {
6936
+ skippedPolicy.push({ package: pkg, skillName, reason: "max-per-run" });
6937
+ seen.add(skillName);
6938
+ continue;
6939
+ }
6940
+ if (typeof installFn !== "function") {
6941
+ errors.push(`installFn missing for ${skillName}`);
6942
+ continue;
6943
+ }
6944
+ let outcome;
6945
+ try {
6946
+ outcome = await installFn({ package: pkg, skillName, version: candidate.version });
6947
+ } catch (err) {
6948
+ errors.push(String(err?.message ?? err));
6949
+ continue;
6950
+ }
6951
+ if (outcome?.ok) {
6952
+ installed.push({ package: pkg, skillName });
6953
+ seen.add(skillName);
6954
+ continue;
6955
+ }
6956
+ const message = String(outcome?.message ?? "");
6957
+ if (/already exists/i.test(message)) {
6958
+ skippedPolicy.push({ package: pkg, skillName, reason: "destination-exists" });
6959
+ seen.add(skillName);
6960
+ continue;
6961
+ }
6962
+ errors.push(message || `install failed: ${skillName}`);
6963
+ }
6964
+ }
6965
+ return {
6966
+ queries,
6967
+ installed,
6968
+ skippedUntrusted,
6969
+ skippedPolicy,
6970
+ errors,
6971
+ registryUnavailable
6972
+ };
6973
+ }
6974
+
5734
6975
  // src/installer/commands/init.js
5735
- var writeFileAsync2 = promisify2(writeFile8);
6976
+ var writeFileAsync2 = promisify2(writeFile12);
5736
6977
  var mkdirAsyncAsync2 = promisify2(mkdirAsync2);
5737
6978
  async function runInit(options) {
5738
6979
  try {
@@ -5776,6 +7017,28 @@ async function runInit(options) {
5776
7017
  if (exitCode === 4) return emitFailure(4, committed?.diagnostics?.[0] ?? "transaction failure", options.json, "init");
5777
7018
  return emitFailure(exitCode, committed?.diagnostics?.[0] ?? "unknown", options.json, "init");
5778
7019
  }
7020
+ let skillsReport = { installed: [], skippedUntrusted: [], skippedPolicy: [], registryUnavailable: false, errors: [] };
7021
+ try {
7022
+ const syncFn = options.syncSkills ?? syncSkills;
7023
+ skillsReport = await syncFn({
7024
+ repoRoot: preview.repoRoot,
7025
+ mode: "init",
7026
+ installFn: async ({ package: pkg, skillName, version }) => {
7027
+ const { createSkillInstallTool: createSkillInstallTool2 } = await Promise.resolve().then(() => (init_ship_skill_install(), ship_skill_install_exports));
7028
+ const tool = createSkillInstallTool2({ repoRoot: preview.repoRoot, config: { value: { skills: [] } } });
7029
+ return tool({ package: pkg, skillName, version });
7030
+ }
7031
+ });
7032
+ } catch (err) {
7033
+ skillsReport = {
7034
+ installed: [],
7035
+ skippedUntrusted: [],
7036
+ skippedPolicy: [],
7037
+ registryUnavailable: true,
7038
+ errors: [String(err?.message ?? err)]
7039
+ };
7040
+ }
7041
+ committed.extra = { ...committed.extra ?? {}, skills: skillsReport };
5779
7042
  const doctor = await runDoctor({
5780
7043
  rootPath: options.rootPath ?? null,
5781
7044
  profile: options.profile ?? null,
@@ -5794,7 +7057,7 @@ async function runInit(options) {
5794
7057
  if (setupPending && preview.repoRoot) {
5795
7058
  await writeSetupPending(preview.repoRoot, {
5796
7059
  profile: preview.profile?.profile ?? "engineering",
5797
- reason: "workflow.models is empty; run /setup-ship-workflow to fill in model roles",
7060
+ reason: "docs/AGENTS.md setup incomplete; run /setup-ship-workflow",
5798
7061
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
5799
7062
  });
5800
7063
  }
@@ -5819,19 +7082,38 @@ async function runInit(options) {
5819
7082
  prefix: "opencode-ship",
5820
7083
  exitCode,
5821
7084
  doctorIssues: doctor.issues,
5822
- setupPending
7085
+ setupPending,
7086
+ skillsReport
5823
7087
  });
5824
7088
  }
5825
7089
  process.exitCode = exitCode;
5826
- return { ok: exitCode === 0, exitCode, setupPending };
7090
+ return { ok: exitCode === 0, exitCode, setupPending, extra: committed.extra };
5827
7091
  }
5828
- function printHumanResult({ prefix, exitCode, doctorIssues, setupPending }) {
7092
+ function printHumanResult({ prefix, exitCode, doctorIssues, setupPending, skillsReport }) {
5829
7093
  const lines = [];
5830
7094
  if (exitCode === 0) {
5831
7095
  lines.push(`${prefix}: installed; doctor OK`);
5832
7096
  } else {
5833
7097
  lines.push(`${prefix}: installed with warnings`);
5834
7098
  }
7099
+ const installed = Array.isArray(skillsReport?.installed) ? skillsReport.installed : [];
7100
+ if (installed.length > 0) {
7101
+ lines.push(`Skill discovery: ${installed.length} installed (${installed.map((i) => i.skillName).join(", ")}).`);
7102
+ } else {
7103
+ lines.push("Skill discovery: 0 installed, continuing with catalog skills only.");
7104
+ }
7105
+ const untrusted = Array.isArray(skillsReport?.skippedUntrusted) ? skillsReport.skippedUntrusted : [];
7106
+ if (untrusted.length > 0) {
7107
+ lines.push(`Untrusted skill candidates: ${untrusted.map((s) => `${s.package}/${s.skillName}`).join(", ")}`);
7108
+ }
7109
+ if (skillsReport?.registryUnavailable) {
7110
+ lines.push("Skill registry: unavailable; trusted skill discovery skipped this run.");
7111
+ }
7112
+ const errors = Array.isArray(skillsReport?.errors) ? skillsReport.errors : [];
7113
+ if (errors.length > 0) {
7114
+ lines.push("Skill discovery errors:");
7115
+ for (const err of errors) lines.push(` - ${err}`);
7116
+ }
5835
7117
  if (Array.isArray(doctorIssues) && doctorIssues.length > 0) {
5836
7118
  lines.push("");
5837
7119
  lines.push("Doctor reported:");
@@ -6005,15 +7287,11 @@ async function runUpdate(options) {
6005
7287
  }
6006
7288
  throw e;
6007
7289
  }
6008
- const hasExplicitModels = options.models && (options.models.planner || options.models.builder || options.models.finalReviewer);
6009
7290
  const preview = await previewInstall({
6010
7291
  rootPath: options.rootPath,
6011
7292
  profile: options.profile ?? null,
6012
7293
  replaceManaged: options.replaceManaged,
6013
- // If the user passes model flags, the run is explicitly
6014
- // populating workflow.models. We must rewrite the config in
6015
- // that case even when the existing config is otherwise valid.
6016
- forceConfig: Boolean(options.forceConfig || hasExplicitModels),
7294
+ forceConfig: Boolean(options.forceConfig),
6017
7295
  forceRootConfig: options.forceRootConfig,
6018
7296
  models: options.models ?? null
6019
7297
  });
@@ -6030,6 +7308,30 @@ async function runUpdate(options) {
6030
7308
  return emitFailure2(3, "modified managed files; rerun with --replace-managed", options.json, "update");
6031
7309
  }
6032
7310
  const committed = await commitInstall(preview, { json: options.json, command: "update" });
7311
+ if (committed.extra?.exitCode === 0 && preview.repoRoot) {
7312
+ let skillsReport = { installed: [], skippedUntrusted: [], skippedPolicy: [], registryUnavailable: false, errors: [] };
7313
+ try {
7314
+ const syncFn = options.syncSkills ?? syncSkills;
7315
+ skillsReport = await syncFn({
7316
+ repoRoot: preview.repoRoot,
7317
+ mode: "deliver",
7318
+ installFn: async ({ package: pkg, skillName, version }) => {
7319
+ const { createSkillInstallTool: createSkillInstallTool2 } = await Promise.resolve().then(() => (init_ship_skill_install(), ship_skill_install_exports));
7320
+ const tool = createSkillInstallTool2({ repoRoot: preview.repoRoot, config: { value: { skills: [] } } });
7321
+ return tool({ package: pkg, skillName, version });
7322
+ }
7323
+ });
7324
+ } catch (err) {
7325
+ skillsReport = {
7326
+ installed: [],
7327
+ skippedUntrusted: [],
7328
+ skippedPolicy: [],
7329
+ registryUnavailable: true,
7330
+ errors: [String(err?.message ?? err)]
7331
+ };
7332
+ }
7333
+ committed.extra = { ...committed.extra ?? {}, skills: skillsReport };
7334
+ }
6033
7335
  if (options.json) {
6034
7336
  process.stdout.write(JSON.stringify({
6035
7337
  reportVersion: 1,
@@ -6073,11 +7375,11 @@ function emitFailure2(code, message, json, command) {
6073
7375
  // src/installer/commands/setup-complete.js
6074
7376
  init_config();
6075
7377
  init_setup_state();
6076
- import { existsSync as existsSync17 } from "node:fs";
6077
- import { resolve as resolve16 } from "node:path";
7378
+ import { existsSync as existsSync23 } from "node:fs";
7379
+ import { resolve as resolve23 } from "node:path";
6078
7380
  async function runSetupComplete(options) {
6079
7381
  const repoRoot = options.rootPath ?? process.cwd();
6080
- if (!existsSync17(resolve16(repoRoot, ".git"))) {
7382
+ if (!existsSync23(resolve23(repoRoot, ".git"))) {
6081
7383
  return emitFailure3(2, "not a git repository", options.json);
6082
7384
  }
6083
7385
  const config = await loadConfig(repoRoot);