opencode-ship 1.1.7 → 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 (44) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/README.md +13 -13
  3. package/THIRD_PARTY_NOTICES.md +1 -1
  4. package/assets/agents/ship-controller.md +47 -22
  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 +27 -30
  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/ship-workflow/SKILL.md +83 -0
  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 +1482 -175
  34. package/dist/core.d.ts +18 -0
  35. package/dist/core.js +528 -11
  36. package/dist/plugin.js +18613 -17483
  37. package/package.json +2 -1
  38. package/schema/project-adapter.example.json +2 -2
  39. package/schema/project-opencode-shim.json +2 -0
  40. package/schema/ship-lock.schema.json +23 -2
  41. package/tests/plugin/expected-tools.mjs +48 -5
  42. package/tests/plugin/plugin-load.test.mjs +27 -5
  43. package/vendor/sources.json +21 -21
  44. package/assets/skills/delivery-workflow/SKILL.md +0 -64
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // opencode-ship CLI v1.1.7
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.7";
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,30 @@ 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([
2811
+ "delivery_abandon",
1772
2812
  "delivery_cleanup",
1773
2813
  "delivery_github_read",
1774
2814
  "delivery_inspect",
@@ -1785,6 +2825,7 @@ var PUBLIC_TOOL_IDS = [
1785
2825
  "delivery_sync",
1786
2826
  "delivery_verify",
1787
2827
  "delivery_worktree",
2828
+ "ship_deliver",
1788
2829
  "ship_final_review",
1789
2830
  "ship_plan_approve",
1790
2831
  "ship_plan_start",
@@ -1801,24 +2842,26 @@ var PUBLIC_TOOL_IDS = [
1801
2842
  "ship_task_report",
1802
2843
  "ship_task_review",
1803
2844
  "ship_task_start"
1804
- ];
1805
- var BUILD_TOOL_ALLOW = [
2845
+ ]);
2846
+ var BUILD_TOOL_ALLOW = withShipAliases([
1806
2847
  "delivery_cleanup",
1807
2848
  "delivery_inspect",
1808
2849
  "delivery_issue",
1809
2850
  "delivery_pr",
1810
2851
  "delivery_ready",
1811
2852
  "delivery_worktree",
2853
+ "ship_deliver",
1812
2854
  "ship_status",
1813
2855
  "ship_resume"
1814
- ];
1815
- var BUILD_TOOL_ASK = [
2856
+ ]);
2857
+ var BUILD_TOOL_ASK = withShipAliases([
1816
2858
  "ship_plan_approve",
1817
2859
  "delivery_merge",
1818
2860
  "delivery_issue_close",
2861
+ "delivery_abandon",
1819
2862
  "ship_skill_install"
1820
- ];
1821
- var CONTROLLER_TOOL_ALLOW = [
2863
+ ]);
2864
+ var CONTROLLER_TOOL_ALLOW = withShipAliases([
1822
2865
  "delivery_inspect",
1823
2866
  "delivery_cleanup",
1824
2867
  "delivery_github_read",
@@ -1842,12 +2885,13 @@ var CONTROLLER_TOOL_ALLOW = [
1842
2885
  "ship_skill_install",
1843
2886
  "ship_skill_audit",
1844
2887
  "ship_skill_uninstall"
1845
- ];
1846
- var CONTROLLER_TOOL_ASK = [
2888
+ ]);
2889
+ var CONTROLLER_TOOL_ASK = withShipAliases([
1847
2890
  "ship_plan_approve",
1848
2891
  "delivery_merge",
1849
- "delivery_issue_close"
1850
- ];
2892
+ "delivery_issue_close",
2893
+ "delivery_abandon"
2894
+ ]);
1851
2895
  var H = "git";
1852
2896
  var RESET = "--hard";
1853
2897
  var PUSH = "--force";
@@ -1891,6 +2935,8 @@ function rootPermissionMatrix() {
1891
2935
  "ship-controller": "allow",
1892
2936
  "general": "allow",
1893
2937
  "plan": "deny",
2938
+ "ship-reviewer": "allow",
2939
+ "ship-verifier": "allow",
1894
2940
  "delivery-reviewer": "allow",
1895
2941
  "delivery-verifier": "allow"
1896
2942
  },
@@ -2736,11 +3782,11 @@ function parseTree(text, errors = [], options = ParseOptions.DEFAULT) {
2736
3782
  onValue({ type: getNodeType(value), offset, length, parent: currentParent, value });
2737
3783
  ensurePropertyComplete(offset + length);
2738
3784
  },
2739
- onSeparator: (sep3, offset, length) => {
3785
+ onSeparator: (sep6, offset, length) => {
2740
3786
  if (currentParent.type === "property") {
2741
- if (sep3 === ":") {
3787
+ if (sep6 === ":") {
2742
3788
  currentParent.colonOffset = offset;
2743
- } else if (sep3 === ",") {
3789
+ } else if (sep6 === ",") {
2744
3790
  ensurePropertyComplete(offset);
2745
3791
  }
2746
3792
  }
@@ -3453,7 +4499,7 @@ async function planRootReconciliation(input) {
3453
4499
  previousDocument: input.previousDocument
3454
4500
  });
3455
4501
  }
3456
- const fileMissing = !existsSync7(target);
4502
+ const fileMissing = !existsSync8(target);
3457
4503
  if (fileMissing && !input.forceRepair) {
3458
4504
  if (mode === "profile-transition") {
3459
4505
  return {
@@ -4100,13 +5146,13 @@ function mergePointerRecords(descriptors, previousRecords, result, beforeSnapsho
4100
5146
 
4101
5147
  // src/installer/planner.js
4102
5148
  async function readBytes(path) {
4103
- if (!existsSync8(path)) return null;
5149
+ if (!existsSync9(path)) return null;
4104
5150
  const buf = await readFile4(path);
4105
5151
  const fileStat = await stat(path);
4106
5152
  return { bytes: buf, hash: bytesHashString(buf.toString("utf8")), mode: fileStat.mode & 511 };
4107
5153
  }
4108
5154
  async function readDesiredBytes(source) {
4109
- if (!source || !existsSync8(source)) return null;
5155
+ if (!source || !existsSync9(source)) return null;
4110
5156
  const buf = await readFile4(source);
4111
5157
  return { bytes: buf, hash: bytesHashString(buf.toString("utf8")) };
4112
5158
  }
@@ -4309,7 +5355,15 @@ async function planUninstall({ repoRoot, lock }) {
4309
5355
  async function planConfigSynthesis({ repoRoot, detection, lock, forceOverwrite, migrationSeed = null, models = null }) {
4310
5356
  const existing = await loadConfig(repoRoot);
4311
5357
  const hasModelFlags = Boolean(models && (models.planner || models.builder || models.finalReviewer));
4312
- 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) {
4313
5367
  return {
4314
5368
  kind: "noop",
4315
5369
  op: "config",
@@ -4318,35 +5372,34 @@ async function planConfigSynthesis({ repoRoot, detection, lock, forceOverwrite,
4318
5372
  currentSha: existing.sha256,
4319
5373
  desiredSha: existing.sha256,
4320
5374
  configValue: existing.value,
5375
+ modelsProvenance: resolved.provenance,
4321
5376
  reason: "user config already present"
4322
5377
  };
4323
5378
  }
4324
5379
  let desiredValue = migrationSeed ?? (existing?.ok ? structuredClone(existing.value) : renderDefaultConfig(detection));
4325
- if (hasModelFlags) {
4326
- desiredValue = {
4327
- ...desiredValue,
4328
- schemaVersion: 2,
4329
- profile: "engineering",
4330
- workflow: {
4331
- ...desiredValue.workflow ?? {},
4332
- models: {
4333
- planner: models.planner ?? desiredValue?.workflow?.models?.planner,
4334
- builder: models.builder ?? desiredValue?.workflow?.models?.builder,
4335
- finalReviewer: models.finalReviewer ?? desiredValue?.workflow?.models?.finalReviewer
4336
- },
4337
- approval: {
4338
- mirrorToIssue: true,
4339
- maxFailedRounds: 3,
4340
- ...desiredValue?.workflow?.approval ?? {}
4341
- }
4342
- }
4343
- };
4344
- }
4345
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
+ };
4346
5390
  const desiredJson = JSON.stringify(desiredValue, null, 2) + "\n";
4347
5391
  const desiredSha = bytesHashString(desiredJson);
4348
- const kind = existing?.ok && (forceOverwrite || hasModelFlags) ? "update" : "create";
4349
- 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
+ }
4350
5403
  return {
4351
5404
  kind,
4352
5405
  op: "config",
@@ -4356,6 +5409,7 @@ async function planConfigSynthesis({ repoRoot, detection, lock, forceOverwrite,
4356
5409
  desiredSha,
4357
5410
  bytes: Buffer.from(desiredJson, "utf8"),
4358
5411
  configValue: desiredValue,
5412
+ modelsProvenance: resolved.provenance,
4359
5413
  reason
4360
5414
  };
4361
5415
  }
@@ -4382,8 +5436,8 @@ init_json_pointer();
4382
5436
 
4383
5437
  // src/installer/detection/project.js
4384
5438
  import { spawnSync } from "node:child_process";
4385
- import { existsSync as existsSync9, readFileSync as readFileSync4 } from "node:fs";
4386
- 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";
4387
5441
  function runGit(cwd, args) {
4388
5442
  const r = spawnSync("git", ["-C", cwd, ...args], {
4389
5443
  stdio: ["ignore", "pipe", "pipe"],
@@ -4392,17 +5446,17 @@ function runGit(cwd, args) {
4392
5446
  return { status: r.status ?? -1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
4393
5447
  }
4394
5448
  function detectPackageManager(repoRoot) {
4395
- if (existsSync9(join2(repoRoot, "pnpm-lock.yaml"))) return "pnpm";
4396
- if (existsSync9(join2(repoRoot, "yarn.lock"))) return "yarn";
4397
- if (existsSync9(join2(repoRoot, "bun.lockb"))) return "bun";
4398
- 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";
4399
5453
  return null;
4400
5454
  }
4401
5455
  function readPackageJson(repoRoot) {
4402
5456
  const path = join2(repoRoot, "package.json");
4403
- if (!existsSync9(path)) return null;
5457
+ if (!existsSync10(path)) return null;
4404
5458
  try {
4405
- return JSON.parse(readFileSync4(path, "utf8"));
5459
+ return JSON.parse(readFileSync5(path, "utf8"));
4406
5460
  } catch {
4407
5461
  return null;
4408
5462
  }
@@ -4477,7 +5531,7 @@ function detectOwner(repoRoot) {
4477
5531
  }
4478
5532
  function detectProject(repoRoot = process.cwd()) {
4479
5533
  const errors = [];
4480
- const cwd = resolve7(repoRoot);
5534
+ const cwd = resolve8(repoRoot);
4481
5535
  const inside = runGit(cwd, ["rev-parse", "--show-toplevel"]);
4482
5536
  if (inside.status !== 0) {
4483
5537
  errors.push({ kind: "not-a-git-repo", path: cwd, detail: inside.stderr.trim() });
@@ -4539,12 +5593,12 @@ import {
4539
5593
  stat as stat3,
4540
5594
  open
4541
5595
  } from "node:fs/promises";
4542
- import { existsSync as existsSync11 } from "node:fs";
4543
- 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";
4544
5598
 
4545
5599
  // src/state/git-common-dir.js
4546
5600
  import { spawn } from "node:child_process";
4547
- import { resolve as resolve8, join as join3 } from "node:path";
5601
+ import { resolve as resolve9, join as join3 } from "node:path";
4548
5602
  var STATE_DIRNAME = "opencode-ship";
4549
5603
  async function resolveGitCommonDir(repoRoot) {
4550
5604
  if (typeof repoRoot !== "string" || repoRoot.length === 0) {
@@ -4577,7 +5631,7 @@ async function resolveGitCommonDir(repoRoot) {
4577
5631
  reject(new Error("git rev-parse --git-common-dir returned an empty path"));
4578
5632
  return;
4579
5633
  }
4580
- resolveP(resolve8(repoRoot, trimmed));
5634
+ resolveP(resolve9(repoRoot, trimmed));
4581
5635
  });
4582
5636
  });
4583
5637
  }
@@ -4600,8 +5654,8 @@ import {
4600
5654
  unlink,
4601
5655
  stat as stat2
4602
5656
  } from "node:fs/promises";
4603
- import { existsSync as existsSync10 } from "node:fs";
4604
- 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";
4605
5659
  import { createHash as createHash2, randomBytes } from "node:crypto";
4606
5660
  import { hostname as osHostname } from "node:os";
4607
5661
  var STALE_LOCK_MS = 120 * 1e3;
@@ -4626,7 +5680,7 @@ async function atomicReplaceJson(path, value) {
4626
5680
  if (typeof path !== "string" || path.length === 0) {
4627
5681
  throw new Error("atomicReplaceJson: path must be a non-empty string");
4628
5682
  }
4629
- const target = resolve9(path);
5683
+ const target = resolve10(path);
4630
5684
  const parent = dirname5(target);
4631
5685
  await mkdir3(parent, { recursive: true });
4632
5686
  const tmp = `${target}.${randomToken()}.tmp`;
@@ -4650,10 +5704,10 @@ async function lockDirForRepo(repoRoot) {
4650
5704
  return lockDirFromCommonDir(commonDir);
4651
5705
  }
4652
5706
  function transactionLockPath(lockDir) {
4653
- return resolve10(lockDir, ".txn.lock");
5707
+ return resolve11(lockDir, ".txn.lock");
4654
5708
  }
4655
5709
  function journalPath(lockDir, txnId) {
4656
- return resolve10(lockDir, `.txn-${txnId}.journal`);
5710
+ return resolve11(lockDir, `.txn-${txnId}.journal`);
4657
5711
  }
4658
5712
  function backupPath(target, token) {
4659
5713
  return `${target}.txn-${token}-backup`;
@@ -4688,7 +5742,7 @@ async function releaseLock(lockDir) {
4688
5742
  }
4689
5743
  async function liveLockOwner(lockDir) {
4690
5744
  const path = transactionLockPath(lockDir);
4691
- if (!existsSync11(path)) return false;
5745
+ if (!existsSync12(path)) return false;
4692
5746
  try {
4693
5747
  const lock = JSON.parse(await readFile6(path, "utf8"));
4694
5748
  if (!Number.isInteger(lock?.pid) || lock.pid <= 0) return false;
@@ -4734,7 +5788,7 @@ async function clearJournal(lockDir, txnId) {
4734
5788
  }
4735
5789
  }
4736
5790
  async function readJournal(lockDir, name) {
4737
- const path = resolve10(lockDir, name);
5791
+ const path = resolve11(lockDir, name);
4738
5792
  let raw;
4739
5793
  try {
4740
5794
  raw = await readFile6(path, "utf8");
@@ -4755,7 +5809,7 @@ async function readJournal(lockDir, name) {
4755
5809
  async function isCommitted(journal) {
4756
5810
  if (journal.committed) return true;
4757
5811
  const marker = journal.ledger?.find((entry) => entry.commitMarker);
4758
- if (!marker?.target || !marker.installedSha256 || !existsSync11(marker.target)) return false;
5812
+ if (!marker?.target || !marker.installedSha256 || !existsSync12(marker.target)) return false;
4759
5813
  try {
4760
5814
  return bytesHashString(await readFile6(marker.target, "utf8")) === marker.installedSha256;
4761
5815
  } catch {
@@ -4764,13 +5818,13 @@ async function isCommitted(journal) {
4764
5818
  }
4765
5819
  async function restoreEntry(entry) {
4766
5820
  if (entry.op === "write") {
4767
- if (entry.backup && existsSync11(entry.backup)) {
5821
+ if (entry.backup && existsSync12(entry.backup)) {
4768
5822
  await rename4(entry.backup, entry.target);
4769
- } else if (entry.hadOriginal === false && existsSync11(entry.target)) {
5823
+ } else if (entry.hadOriginal === false && existsSync12(entry.target)) {
4770
5824
  await unlink2(entry.target);
4771
5825
  }
4772
- if (entry.staged && existsSync11(entry.staged)) await unlink2(entry.staged);
4773
- } 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)) {
4774
5828
  await rename4(entry.backup, entry.target);
4775
5829
  }
4776
5830
  await fsyncDir2(dirname6(entry.target));
@@ -4793,11 +5847,11 @@ async function recoverJournal(lockDir, name) {
4793
5847
  complete = false;
4794
5848
  }
4795
5849
  }
4796
- if (complete) await unlink2(resolve10(lockDir, name)).catch(() => null);
5850
+ if (complete) await unlink2(resolve11(lockDir, name)).catch(() => null);
4797
5851
  return { ok: complete };
4798
5852
  }
4799
5853
  async function recover(repoRoot, lockDir) {
4800
- if (!existsSync11(lockDir)) return { recovered: false, recoveredCount: 0 };
5854
+ if (!existsSync12(lockDir)) return { recovered: false, recoveredCount: 0 };
4801
5855
  if (await liveLockOwner(lockDir)) {
4802
5856
  return { recovered: false, recoveredCount: 0, blocked: true };
4803
5857
  }
@@ -4831,11 +5885,11 @@ async function recover(repoRoot, lockDir) {
4831
5885
  }
4832
5886
  async function commitEntry(entry) {
4833
5887
  let changed = false;
4834
- if (entry.backup && existsSync11(entry.backup)) {
5888
+ if (entry.backup && existsSync12(entry.backup)) {
4835
5889
  await unlink2(entry.backup);
4836
5890
  changed = true;
4837
5891
  }
4838
- if (entry.staged && existsSync11(entry.staged)) {
5892
+ if (entry.staged && existsSync12(entry.staged)) {
4839
5893
  await unlink2(entry.staged);
4840
5894
  changed = true;
4841
5895
  }
@@ -4865,7 +5919,7 @@ async function executePlan({ repoRoot, plan, newLockBuilder }) {
4865
5919
  const backup = backupPath(op.target, token);
4866
5920
  const staged = stagedPath(op.target, token);
4867
5921
  if (op.kind === "delete") {
4868
- if (!existsSync11(op.target)) continue;
5922
+ if (!existsSync12(op.target)) continue;
4869
5923
  journal.entries.push({
4870
5924
  op: "delete",
4871
5925
  target: op.target,
@@ -4878,7 +5932,7 @@ async function executePlan({ repoRoot, plan, newLockBuilder }) {
4878
5932
  await rename4(op.target, backup);
4879
5933
  } else {
4880
5934
  await mkdirp(dirname6(op.target));
4881
- const hadOriginal = existsSync11(op.target);
5935
+ const hadOriginal = existsSync12(op.target);
4882
5936
  journal.entries.push({
4883
5937
  op: "write",
4884
5938
  target: op.target,
@@ -4904,7 +5958,7 @@ async function executePlan({ repoRoot, plan, newLockBuilder }) {
4904
5958
  const token = randomToken2();
4905
5959
  const backup = backupPath(lockPathTarget, token);
4906
5960
  const staged = stagedPath(lockPathTarget, token);
4907
- const hadOriginal = existsSync11(lockPathTarget);
5961
+ const hadOriginal = existsSync12(lockPathTarget);
4908
5962
  const finalLock = { ...lockValue, integrity: computeIntegrity(lockValue) };
4909
5963
  const lockBytes = JSON.stringify(finalLock, null, 2) + "\n";
4910
5964
  journal.entries.push({
@@ -4968,16 +6022,16 @@ async function rollback(lockDir, journal) {
4968
6022
  init_lock();
4969
6023
  init_config();
4970
6024
  import { readFile as readFile7 } from "node:fs/promises";
4971
- import { existsSync as existsSync12 } from "node:fs";
4972
- import { resolve as resolve11 } from "node:path";
6025
+ import { existsSync as existsSync13 } from "node:fs";
6026
+ import { resolve as resolve12 } from "node:path";
4973
6027
  function legacyAdapterPath(repoRoot) {
4974
- return resolve11(repoRoot, ".opencode", "delivery.json");
6028
+ return resolve12(repoRoot, ".opencode", "delivery.json");
4975
6029
  }
4976
6030
  function legacyLockPath(repoRoot) {
4977
- return resolve11(repoRoot, ".opencode", "delivery.lock.json");
6031
+ return resolve12(repoRoot, ".opencode", "delivery.lock.json");
4978
6032
  }
4979
6033
  function legacyPluginPath(repoRoot) {
4980
- return resolve11(repoRoot, ".opencode", "plugin", "delivery.ts");
6034
+ return resolve12(repoRoot, ".opencode", "plugin", "delivery.ts");
4981
6035
  }
4982
6036
  async function detectLegacyShapes(repoRoot) {
4983
6037
  const out = {
@@ -4988,17 +6042,17 @@ async function detectLegacyShapes(repoRoot) {
4988
6042
  reviewer: false,
4989
6043
  verifier: false
4990
6044
  };
4991
- if (existsSync12(legacyAdapterPath(repoRoot))) out.adapter = true;
4992
- if (existsSync12(legacyLockPath(repoRoot))) out.legacyLock = true;
4993
- if (existsSync12(legacyPluginPath(repoRoot))) out.plugin = true;
4994
- if (existsSync12(resolve11(repoRoot, ".opencode/plugin/opencode-ship.js"))) out.pluginOld = true;
4995
- if (existsSync12(resolve11(repoRoot, ".opencode/agents/delivery-reviewer.md"))) out.reviewer = true;
4996
- 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;
4997
6051
  return out;
4998
6052
  }
4999
6053
  async function readLegacyAdapter(repoRoot) {
5000
6054
  const path = legacyAdapterPath(repoRoot);
5001
- if (!existsSync12(path)) return null;
6055
+ if (!existsSync13(path)) return null;
5002
6056
  try {
5003
6057
  const raw = await readFile7(path, "utf8");
5004
6058
  return { path, raw, value: JSON.parse(raw) };
@@ -5019,13 +6073,13 @@ async function migration({ repoRoot, lock, forceRepair, detection = null }) {
5019
6073
  if (legacy && shapes.legacyLock && !lock?.manager) {
5020
6074
  actions.push({ kind: "kept-legacy-lock", path: legacyLockPath(repoRoot) });
5021
6075
  }
5022
- if (shapes.plugin && existsSync12(resolve11(repoRoot, ".opencode/plugins/opencode-ship.js"))) {
6076
+ if (shapes.plugin && existsSync13(resolve12(repoRoot, ".opencode/plugins/opencode-ship.js"))) {
5023
6077
  if (!forceRepair) {
5024
6078
  actions.push({ kind: "candidate-remove-legacy-plugin", path: legacyPluginPath(repoRoot) });
5025
6079
  }
5026
6080
  }
5027
6081
  if (shapes.pluginOld) {
5028
- 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") });
5029
6083
  }
5030
6084
  return { shapes, actions, legacyPresent: Boolean(legacy), proposedConfigSeed };
5031
6085
  }
@@ -5073,7 +6127,7 @@ function legacyToShipConfig(legacy, detection = null) {
5073
6127
  // src/installer/executor.js
5074
6128
  init_profile();
5075
6129
  async function readCurrentBytes(targetPath) {
5076
- if (!existsSync13(targetPath)) return null;
6130
+ if (!existsSync14(targetPath)) return null;
5077
6131
  const buf = await readFile9(targetPath);
5078
6132
  return { bytes: buf, hash: bytesHashString(buf.toString("utf8")) };
5079
6133
  }
@@ -5164,7 +6218,7 @@ async function previewInstall({ rootPath, profile = null, replaceManaged, forceC
5164
6218
  });
5165
6219
  const planMode = null;
5166
6220
  const rootPlan = await planRootConfigApply({ repoRoot, lock, forceRepair: Boolean(forceRootConfig), planMode });
5167
- const setupPending = resolved.profile === "engineering" && !hasCompletedModels(configValue);
6221
+ const setupPending = resolved.profile === "engineering" && !hasCompletedModels(configPlan.configValue);
5168
6222
  const plan = [...filePlan ?? [], ...staleFilePlan, ...migrationPlan, configPlan, rootPlan];
5169
6223
  const conflicts = plan.filter((p) => p && p.kind === "conflict");
5170
6224
  const summary = summarise(plan);
@@ -5180,7 +6234,8 @@ async function previewInstall({ rootPath, profile = null, replaceManaged, forceC
5180
6234
  conflicts,
5181
6235
  summary,
5182
6236
  migrationReport,
5183
- setupPending
6237
+ setupPending,
6238
+ modelsProvenance: configPlan.modelsProvenance
5184
6239
  };
5185
6240
  }
5186
6241
  async function previewUninstall({ rootPath }) {
@@ -5215,7 +6270,7 @@ function summarise(plan) {
5215
6270
  }
5216
6271
  return counts;
5217
6272
  }
5218
- 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 }) {
5219
6274
  const files = [];
5220
6275
  const remain = lock?.files?.filter((f) => !plan.some((op) => op?.relPath === f.path)) ?? [];
5221
6276
  for (const op of plan) {
@@ -5251,7 +6306,7 @@ async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profil
5251
6306
  manager: {
5252
6307
  schemaVersion: CURRENT_LOCK_SCHEMA,
5253
6308
  name: "opencode-ship",
5254
- version: "1.1.7",
6309
+ version: "1.1.9-rc.1",
5255
6310
  templateSet: TEMPLATE_SET_ID,
5256
6311
  profile: resolvedProfile,
5257
6312
  appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -5261,6 +6316,7 @@ async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profil
5261
6316
  sha256: configSha ?? lock?.manager?.config?.sha256 ?? "",
5262
6317
  existed: Boolean(lock?.manager?.config?.existed)
5263
6318
  },
6319
+ ...modelsProvenance ? { models: modelsProvenance } : {},
5264
6320
  rootDocuments: hasRootDocuments && (hasRootPlan || (lock?.manager?.rootDocuments?.length ?? 0) > 0) ? [{
5265
6321
  path: rootPlan?.relPath ?? lock?.manager?.rootDocuments?.[0]?.path ?? "opencode.json",
5266
6322
  format: rootPlan?.format ?? lock?.manager?.rootDocuments?.[0]?.format ?? "json",
@@ -5307,6 +6363,7 @@ async function commitInstall(preview, { json, command, fullSetupComplete = false
5307
6363
  configPlan,
5308
6364
  rootPlan,
5309
6365
  profile: preview.profile?.profile,
6366
+ modelsProvenance: preview.modelsProvenance,
5310
6367
  fullSetupComplete
5311
6368
  });
5312
6369
  const txPlan = await stageFiles(fileOnly, repoRoot);
@@ -5398,12 +6455,12 @@ function relativeTemplate(source) {
5398
6455
  // src/installer/commands/doctor.js
5399
6456
  init_lock();
5400
6457
  init_config();
5401
- import { existsSync as existsSync15, readFileSync as readFileSync5 } from "node:fs";
6458
+ import { existsSync as existsSync16, readFileSync as readFileSync6 } from "node:fs";
5402
6459
  import { spawnSync as spawnSync2 } from "node:child_process";
5403
6460
  init_hash();
5404
6461
  init_catalog();
5405
6462
  init_root_config();
5406
- import { resolve as resolve14 } from "node:path";
6463
+ import { resolve as resolve15 } from "node:path";
5407
6464
 
5408
6465
  // src/installer/report.js
5409
6466
  var REPORT_VERSION = 1;
@@ -5454,6 +6511,7 @@ function renderJson({ command, plan, conflicts, summary, diagnostics = [], exitC
5454
6511
 
5455
6512
  // src/installer/commands/doctor.js
5456
6513
  init_profile();
6514
+ init_workflow_models();
5457
6515
  function checkNode() {
5458
6516
  return { name: "node>=22.6.0", ok: /^v2[2-9]/.test(process.version), detail: process.version };
5459
6517
  }
@@ -5492,9 +6550,9 @@ function checkPackageIntegrity() {
5492
6550
  function buildSourceHashIndex() {
5493
6551
  const idx = /* @__PURE__ */ new Map();
5494
6552
  for (const entry of CATALOG) {
5495
- if (!existsSync15(entry.source)) continue;
6553
+ if (!existsSync16(entry.source)) continue;
5496
6554
  try {
5497
- const buf = readFileSync5(entry.source, "utf8");
6555
+ const buf = readFileSync6(entry.source, "utf8");
5498
6556
  idx.set(entry.source, bytesHashString(buf));
5499
6557
  } catch {
5500
6558
  }
@@ -5505,13 +6563,13 @@ async function checkCatalogInstall(repoRoot, sourceHashes, profile, renderedAgen
5505
6563
  const rows = [];
5506
6564
  const scoped = profile ? filterCatalogByProfile(CATALOG, profile) : CATALOG;
5507
6565
  for (const entry of scoped) {
5508
- const target = resolve14(repoRoot, entry.path);
5509
- if (!existsSync15(target)) {
6566
+ const target = resolve15(repoRoot, entry.path);
6567
+ if (!existsSync16(target)) {
5510
6568
  rows.push(`${entry.id}: missing`);
5511
6569
  continue;
5512
6570
  }
5513
6571
  try {
5514
- const buf = readFileSync5(target, "utf8");
6572
+ const buf = readFileSync6(target, "utf8");
5515
6573
  const actual = bytesHashString(buf);
5516
6574
  const rendered = renderedAgentMap.get(entry.path);
5517
6575
  const expected = rendered ? rendered.sha256 : sourceHashes.get(entry.source);
@@ -5567,12 +6625,12 @@ async function checkManagedHashes(repoRoot, validatedLock) {
5567
6625
  const drift = [];
5568
6626
  const renderedAgents = await loadRenderedAgentOverrides(repoRoot);
5569
6627
  for (const entry of validatedLock.lock.files ?? []) {
5570
- const p = resolve14(repoRoot, entry.path);
5571
- if (!existsSync15(p)) {
6628
+ const p = resolve15(repoRoot, entry.path);
6629
+ if (!existsSync16(p)) {
5572
6630
  drift.push(`missing:${entry.path}`);
5573
6631
  continue;
5574
6632
  }
5575
- const buf = readFileSync5(p, "utf8");
6633
+ const buf = readFileSync6(p, "utf8");
5576
6634
  const actual = bytesHashString(buf);
5577
6635
  if (actual !== entry.sha256) drift.push(`drift:${entry.path}`);
5578
6636
  }
@@ -5622,6 +6680,41 @@ async function checkRootConfig(repoRoot) {
5622
6680
  detail: conflict ? `conflict on ${conflict.pointer}` : `applied=${r.applied.length}, skipped=${r.skipped.length}`
5623
6681
  };
5624
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
+ }
5625
6718
  async function checkSetupState(repoRoot, configValue) {
5626
6719
  const { setupComplete: setupComplete2 } = await Promise.resolve().then(() => (init_setup_state(), setup_state_exports));
5627
6720
  const state = await setupComplete2(repoRoot, configValue);
@@ -5682,7 +6775,8 @@ async function runDoctor({ rootPath, profile, json, writeOutput = true }) {
5682
6775
  await checkManagedHashes(repoRoot, validatedLock),
5683
6776
  await checkActiveProfileFootprint(repoRoot, validatedLock, resolved.profile),
5684
6777
  await checkRootConfig(repoRoot),
5685
- await checkSetupState(repoRoot, configValue)
6778
+ await checkSetupState(repoRoot, configValue),
6779
+ await checkWorkflowModelDefaults(repoRoot)
5686
6780
  ];
5687
6781
  const issues = checks.filter((c) => !c.ok).map((c) => `${c.name}: ${c.detail}`);
5688
6782
  const plan = checks.map((c) => ({
@@ -5704,20 +6798,19 @@ async function runDoctor({ rootPath, profile, json, writeOutput = true }) {
5704
6798
 
5705
6799
  // src/installer/commands/init.js
5706
6800
  init_catalog();
5707
- init_config();
5708
6801
 
5709
6802
  // src/installer/setup-pending.js
5710
- 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";
5711
6804
  import { promisify } from "node:util";
5712
- import { resolve as resolve15, dirname as dirname9 } from "node:path";
6805
+ import { resolve as resolve16, dirname as dirname9 } from "node:path";
5713
6806
  var writeFileAsync = promisify(writeFile7);
5714
6807
  var mkdirAsyncAsync = promisify(mkdirAsync);
5715
6808
  var REL_PATH = ".opencode/ship.setup-pending.json";
5716
6809
  function setupPendingPath(repoRoot) {
5717
- return resolve15(repoRoot, REL_PATH);
6810
+ return resolve16(repoRoot, REL_PATH);
5718
6811
  }
5719
6812
  function isSetupPending(repoRoot) {
5720
- return existsSync16(setupPendingPath(repoRoot));
6813
+ return existsSync17(setupPendingPath(repoRoot));
5721
6814
  }
5722
6815
  async function writeSetupPending(repoRoot, payload) {
5723
6816
  const path = setupPendingPath(repoRoot);
@@ -5726,8 +6819,161 @@ async function writeSetupPending(repoRoot, payload) {
5726
6819
  }
5727
6820
  var SETUP_PENDING_REL_PATH2 = REL_PATH;
5728
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
+
5729
6975
  // src/installer/commands/init.js
5730
- var writeFileAsync2 = promisify2(writeFile8);
6976
+ var writeFileAsync2 = promisify2(writeFile12);
5731
6977
  var mkdirAsyncAsync2 = promisify2(mkdirAsync2);
5732
6978
  async function runInit(options) {
5733
6979
  try {
@@ -5771,6 +7017,28 @@ async function runInit(options) {
5771
7017
  if (exitCode === 4) return emitFailure(4, committed?.diagnostics?.[0] ?? "transaction failure", options.json, "init");
5772
7018
  return emitFailure(exitCode, committed?.diagnostics?.[0] ?? "unknown", options.json, "init");
5773
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 };
5774
7042
  const doctor = await runDoctor({
5775
7043
  rootPath: options.rootPath ?? null,
5776
7044
  profile: options.profile ?? null,
@@ -5789,7 +7057,7 @@ async function runInit(options) {
5789
7057
  if (setupPending && preview.repoRoot) {
5790
7058
  await writeSetupPending(preview.repoRoot, {
5791
7059
  profile: preview.profile?.profile ?? "engineering",
5792
- 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",
5793
7061
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
5794
7062
  });
5795
7063
  }
@@ -5814,19 +7082,38 @@ async function runInit(options) {
5814
7082
  prefix: "opencode-ship",
5815
7083
  exitCode,
5816
7084
  doctorIssues: doctor.issues,
5817
- setupPending
7085
+ setupPending,
7086
+ skillsReport
5818
7087
  });
5819
7088
  }
5820
7089
  process.exitCode = exitCode;
5821
- return { ok: exitCode === 0, exitCode, setupPending };
7090
+ return { ok: exitCode === 0, exitCode, setupPending, extra: committed.extra };
5822
7091
  }
5823
- function printHumanResult({ prefix, exitCode, doctorIssues, setupPending }) {
7092
+ function printHumanResult({ prefix, exitCode, doctorIssues, setupPending, skillsReport }) {
5824
7093
  const lines = [];
5825
7094
  if (exitCode === 0) {
5826
7095
  lines.push(`${prefix}: installed; doctor OK`);
5827
7096
  } else {
5828
7097
  lines.push(`${prefix}: installed with warnings`);
5829
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
+ }
5830
7117
  if (Array.isArray(doctorIssues) && doctorIssues.length > 0) {
5831
7118
  lines.push("");
5832
7119
  lines.push("Doctor reported:");
@@ -6000,15 +7287,11 @@ async function runUpdate(options) {
6000
7287
  }
6001
7288
  throw e;
6002
7289
  }
6003
- const hasExplicitModels = options.models && (options.models.planner || options.models.builder || options.models.finalReviewer);
6004
7290
  const preview = await previewInstall({
6005
7291
  rootPath: options.rootPath,
6006
7292
  profile: options.profile ?? null,
6007
7293
  replaceManaged: options.replaceManaged,
6008
- // If the user passes model flags, the run is explicitly
6009
- // populating workflow.models. We must rewrite the config in
6010
- // that case even when the existing config is otherwise valid.
6011
- forceConfig: Boolean(options.forceConfig || hasExplicitModels),
7294
+ forceConfig: Boolean(options.forceConfig),
6012
7295
  forceRootConfig: options.forceRootConfig,
6013
7296
  models: options.models ?? null
6014
7297
  });
@@ -6025,6 +7308,30 @@ async function runUpdate(options) {
6025
7308
  return emitFailure2(3, "modified managed files; rerun with --replace-managed", options.json, "update");
6026
7309
  }
6027
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
+ }
6028
7335
  if (options.json) {
6029
7336
  process.stdout.write(JSON.stringify({
6030
7337
  reportVersion: 1,
@@ -6068,11 +7375,11 @@ function emitFailure2(code, message, json, command) {
6068
7375
  // src/installer/commands/setup-complete.js
6069
7376
  init_config();
6070
7377
  init_setup_state();
6071
- import { existsSync as existsSync17 } from "node:fs";
6072
- import { resolve as resolve16 } from "node:path";
7378
+ import { existsSync as existsSync23 } from "node:fs";
7379
+ import { resolve as resolve23 } from "node:path";
6073
7380
  async function runSetupComplete(options) {
6074
7381
  const repoRoot = options.rootPath ?? process.cwd();
6075
- if (!existsSync17(resolve16(repoRoot, ".git"))) {
7382
+ if (!existsSync23(resolve23(repoRoot, ".git"))) {
6076
7383
  return emitFailure3(2, "not a git repository", options.json);
6077
7384
  }
6078
7385
  const config = await loadConfig(repoRoot);