opencode-ship 0.10.0 → 1.1.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // opencode-ship CLI v0.10.0
2
+ // opencode-ship CLI v1.1.0-rc.1
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __esm = (fn, res) => function __init() {
@@ -516,8 +516,8 @@ var init_root_config = __esm({
516
516
  });
517
517
 
518
518
  // src/profile.js
519
- var PROFILES = Object.freeze(["core", "engineering"]);
520
- var DEFAULT_PROFILE = "core";
519
+ var PROFILES = Object.freeze(["engineering"]);
520
+ var DEFAULT_PROFILE = "engineering";
521
521
  function isValidProfile(name) {
522
522
  return typeof name === "string" && PROFILES.includes(name);
523
523
  }
@@ -526,19 +526,27 @@ function normalizeProfile(name) {
526
526
  if (!isValidProfile(name)) return null;
527
527
  return name;
528
528
  }
529
+ function isLegacyCoreProfile(name) {
530
+ return name === "core";
531
+ }
529
532
  function resolveProfile({ cli = null, config = null, lock = null } = {}) {
530
533
  if (cli !== null && cli !== void 0) {
531
534
  const v = normalizeProfile(cli);
532
535
  if (v === null) {
533
- throw new Error(`unknown CLI profile '${cli}' (expected one of: ${PROFILES.join(", ")})`);
536
+ throw new Error(
537
+ `unknown CLI profile '${cli}' (only 'engineering' is supported in 1.1.0; the 'core' profile was removed)`
538
+ );
534
539
  }
535
540
  return { profile: v, source: "cli" };
536
541
  }
537
542
  if (config && typeof config === "object" && config.profile !== void 0 && config.profile !== null) {
538
543
  const v = normalizeProfile(config.profile);
539
544
  if (v === null) {
545
+ if (isLegacyCoreProfile(config.profile)) {
546
+ return { profile: DEFAULT_PROFILE, source: "default" };
547
+ }
540
548
  throw new Error(
541
- `unknown ship.config.json profile '${config.profile}' (expected one of: ${PROFILES.join(", ")})`
549
+ `unknown ship.config.json profile '${config.profile}' (only 'engineering' is supported in 1.1.0)`
542
550
  );
543
551
  }
544
552
  return { profile: v, source: "config" };
@@ -546,8 +554,11 @@ function resolveProfile({ cli = null, config = null, lock = null } = {}) {
546
554
  if (lock && typeof lock === "object" && lock.manager && lock.manager.profile !== void 0) {
547
555
  const v = normalizeProfile(lock.manager.profile);
548
556
  if (v === null) {
557
+ if (isLegacyCoreProfile(lock.manager.profile)) {
558
+ return { profile: DEFAULT_PROFILE, source: "default" };
559
+ }
549
560
  throw new Error(
550
- `unknown lock manager.profile '${lock.manager.profile}' (expected one of: ${PROFILES.join(", ")})`
561
+ `unknown lock manager.profile '${lock.manager.profile}' (only 'engineering' is supported in 1.1.0)`
551
562
  );
552
563
  }
553
564
  return { profile: v, source: "lock" };
@@ -559,7 +570,7 @@ function resolveProfile({ cli = null, config = null, lock = null } = {}) {
559
570
  var USAGE = `opencode-ship <command> [options]
560
571
 
561
572
  Commands:
562
- init Install or update managed files in this project.
573
+ init Install managed files in this project. One-liner: pnpm dlx opencode-ship@latest init
563
574
  diff Show what would change without writing.
564
575
  update Apply pending updates after recovering the journal.
565
576
  doctor Validate environment, lock, and references.
@@ -569,16 +580,19 @@ Commands:
569
580
 
570
581
  Options:
571
582
  --root <path> Project root (defaults to cwd).
572
- --profile <name> Override active profile: ${PROFILES.join(", ")}.
583
+ --profile engineering Override active profile (engineering only).
573
584
  --force-config Rewrite the user config from detection (init only).
574
585
  --force-root-config Create opencode.json when absent (init only).
575
586
  --strict-doctor Fail init when doctor reports unhealthy checks.
576
587
  --replace-managed Replace locally-modified managed files (update only).
577
588
  --purge-config Remove ship.config.json when uninstalling.
578
- --planner-model <id> Engineering model id for the strong planner.
579
- --builder-model <id> Engineering model id for the cheap builder.
580
- --final-reviewer-model <id> Engineering model id for the Standards + Spec reviewer.
589
+ --planner-model <id> Strong planner model id (init only, optional).
590
+ --builder-model <id> Cheap builder model id (init only, optional).
591
+ --final-reviewer-model <id> Final Standards + Spec reviewer model id (init only, optional).
581
592
  --json Emit a JSON envelope instead of human output.
593
+
594
+ After init succeeds, restart OpenCode and run /setup-ship-workflow to
595
+ fill in the workflow.models fields and the per-repo docs.
582
596
  `;
583
597
  var MODEL_ID_RE = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
584
598
  function parseFlags(argv) {
@@ -609,6 +623,11 @@ function parseFlags(argv) {
609
623
  if (value === void 0) {
610
624
  return { error: "--profile requires a value" };
611
625
  }
626
+ if (value === "core") {
627
+ return {
628
+ error: "the 'core' profile was removed in opencode-ship 1.1.0; only 'engineering' is supported. Run /setup-ship-workflow to migrate."
629
+ };
630
+ }
612
631
  if (!isValidProfile(value)) {
613
632
  return { error: `unknown profile '${value}' (expected one of: ${PROFILES.join(", ")})` };
614
633
  }
@@ -654,6 +673,11 @@ function helpText() {
654
673
  return USAGE;
655
674
  }
656
675
 
676
+ // src/installer/commands/init.js
677
+ import { promisify as promisify2 } from "node:util";
678
+ import { writeFile as writeFile7, mkdir as mkdirAsync2 } from "node:fs/promises";
679
+ import { dirname as dirname9, resolve as resolvePath } from "node:path";
680
+
657
681
  // src/installer/executor.js
658
682
  import { existsSync as existsSync13 } from "node:fs";
659
683
  import { mkdir as mkdir5, readFile as readFile8, rename as rename5, unlink as unlink3, writeFile as writeFile5 } from "node:fs/promises";
@@ -689,7 +713,7 @@ function resolvePackageRoot(startUrl) {
689
713
  import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
690
714
  import { dirname as dirname2, resolve as resolve2 } from "node:path";
691
715
  import { fileURLToPath as fileURLToPath2 } from "node:url";
692
- var PACKAGE_VERSION = "0.10.0";
716
+ var PACKAGE_VERSION = "1.1.0-rc.1";
693
717
  var TEMPLATE_SET = `v${PACKAGE_VERSION}`;
694
718
 
695
719
  // src/installer/catalog.js
@@ -743,7 +767,7 @@ var CATALOG = [
743
767
  path: ".opencode/plugins/opencode-ship.js",
744
768
  source: resolve3(packageRoot, "dist/plugin.js"),
745
769
  mode: 420,
746
- profiles: ["core", "engineering"]
770
+ profiles: ["engineering"]
747
771
  },
748
772
  {
749
773
  id: "agent:delivery-reviewer",
@@ -751,7 +775,7 @@ var CATALOG = [
751
775
  path: ".opencode/agents/delivery-reviewer.md",
752
776
  source: resolve3(packageRoot, "assets/agents/delivery-reviewer.md"),
753
777
  mode: 420,
754
- profiles: ["core", "engineering"]
778
+ profiles: ["engineering"]
755
779
  },
756
780
  {
757
781
  id: "agent:delivery-verifier",
@@ -759,7 +783,7 @@ var CATALOG = [
759
783
  path: ".opencode/agents/delivery-verifier.md",
760
784
  source: resolve3(packageRoot, "assets/agents/delivery-verifier.md"),
761
785
  mode: 420,
762
- profiles: ["core", "engineering"]
786
+ profiles: ["engineering"]
763
787
  },
764
788
  ...ENGINEERING_AGENTS.map((name) => ({
765
789
  id: `agent:${name}`,
@@ -783,7 +807,7 @@ var CATALOG = [
783
807
  path: ".opencode/skills/delivery-workflow/SKILL.md",
784
808
  source: resolve3(packageRoot, "assets/skills/delivery-workflow/SKILL.md"),
785
809
  mode: 420,
786
- profiles: ["core", "engineering"]
810
+ profiles: ["engineering"]
787
811
  },
788
812
  {
789
813
  id: "skill:planning-research-checkpoint",
@@ -791,7 +815,7 @@ var CATALOG = [
791
815
  path: ".opencode/skills/planning-research-checkpoint/SKILL.md",
792
816
  source: resolve3(packageRoot, "assets/skills/planning-research-checkpoint/SKILL.md"),
793
817
  mode: 420,
794
- profiles: ["core", "engineering"]
818
+ profiles: ["engineering"]
795
819
  },
796
820
  ...MATT_SKILLS.map((name) => ({
797
821
  id: `skill:matt:${name}`,
@@ -808,7 +832,31 @@ var CATALOG = [
808
832
  source: resolve3(packageRoot, `assets/skills/${name}/SKILL.md`),
809
833
  mode: 420,
810
834
  profiles: ["engineering"]
811
- }))
835
+ })),
836
+ {
837
+ id: "skill:setup-ship-workflow",
838
+ kind: "skill",
839
+ path: ".opencode/skills/setup-ship-workflow/SKILL.md",
840
+ source: resolve3(packageRoot, "assets/skills/setup-engineering-workflow/SKILL.md"),
841
+ mode: 420,
842
+ profiles: ["engineering"]
843
+ },
844
+ {
845
+ id: "skill:skill-discovery",
846
+ kind: "skill",
847
+ path: ".opencode/skills/skill-discovery/SKILL.md",
848
+ source: resolve3(packageRoot, "assets/skills/skill-discovery/SKILL.md"),
849
+ mode: 420,
850
+ profiles: ["engineering"]
851
+ },
852
+ {
853
+ id: "command:setup-ship-workflow",
854
+ kind: "support",
855
+ path: ".opencode/commands/setup-ship-workflow.md",
856
+ source: resolve3(packageRoot, "assets/commands/setup-ship-workflow.md"),
857
+ mode: 420,
858
+ profiles: ["engineering"]
859
+ }
812
860
  ];
813
861
  function filterCatalogByProfile(catalog, profile) {
814
862
  const effective = profile === void 0 || profile === null ? DEFAULT_PROFILE : profile;
@@ -913,8 +961,8 @@ var ship_config_schema_default = {
913
961
  schemaVersion: { enum: [1, 2] },
914
962
  profile: {
915
963
  type: "string",
916
- enum: ["core", "engineering"],
917
- description: "Active profile (precedence layer 2: ship.config > lock > core default)."
964
+ enum: ["engineering"],
965
+ description: "Active profile. Engineering is the only supported profile in 1.1.0."
918
966
  },
919
967
  owner: {
920
968
  type: "string",
@@ -1043,13 +1091,13 @@ var ship_config_schema_default = {
1043
1091
  },
1044
1092
  workflow: {
1045
1093
  type: "object",
1046
- description: "Required when profile is engineering. Ignored when profile is core.",
1094
+ description: "Workflow configuration. Models are optional at write time; the setup-ship-workflow skill fills them in. Once all three are present, ship-deliver can start.",
1047
1095
  additionalProperties: false,
1048
1096
  properties: {
1049
1097
  models: {
1050
1098
  type: "object",
1051
1099
  additionalProperties: false,
1052
- required: ["planner", "builder", "finalReviewer"],
1100
+ description: "Optional model roles. All three roles must be present before ship-deliver can run.",
1053
1101
  properties: {
1054
1102
  planner: {
1055
1103
  type: "string",
@@ -1078,31 +1126,7 @@ var ship_config_schema_default = {
1078
1126
  }
1079
1127
  }
1080
1128
  }
1081
- },
1082
- allOf: [
1083
- {
1084
- if: {
1085
- required: ["profile"],
1086
- properties: { profile: { const: "engineering" } }
1087
- },
1088
- then: {
1089
- required: ["workflow"],
1090
- properties: {
1091
- workflow: {
1092
- required: ["models", "approval"],
1093
- properties: {
1094
- models: {
1095
- required: ["planner", "builder", "finalReviewer"]
1096
- },
1097
- approval: {
1098
- required: ["mirrorToIssue", "maxFailedRounds"]
1099
- }
1100
- }
1101
- }
1102
- }
1103
- }
1104
- }
1105
- ]
1129
+ }
1106
1130
  };
1107
1131
 
1108
1132
  // src/installer/validation.js
@@ -1241,7 +1265,8 @@ function renderDefaultConfig(detection, overrides = {}) {
1241
1265
  const safeVerification = Array.isArray(detection?.verificationPlan) && detection.verificationPlan.length ? detection.verificationPlan.map((step) => ({ id: step.id, argv: step.argv })) : [{ id: "typecheck", argv: ["npm", "run", "typecheck"] }];
1242
1266
  const repo = detection?.repository ?? overrides.repository ?? "owner/repo";
1243
1267
  return {
1244
- schemaVersion: 1,
1268
+ schemaVersion: 2,
1269
+ profile: "engineering",
1245
1270
  project: {
1246
1271
  remote: detection?.remote ?? "origin",
1247
1272
  repository: repo,
@@ -1270,9 +1295,19 @@ function renderDefaultConfig(detection, overrides = {}) {
1270
1295
  ready: { requires: ["review", "local-verification", "remote-ci"], stopAfterReady: true },
1271
1296
  merge: { strategy: "squash", policy: "explicit-user-request-only", requireFreshGates: true },
1272
1297
  cleanup: { when: "next-task", requireUnpublishedGuard: true }
1298
+ },
1299
+ workflow: {
1300
+ models: {},
1301
+ approval: { mirrorToIssue: true, maxFailedRounds: 3 }
1273
1302
  }
1274
1303
  };
1275
1304
  }
1305
+ function hasCompletedModels(configValue) {
1306
+ const models = configValue?.workflow?.models;
1307
+ if (!models || typeof models !== "object") return false;
1308
+ const idRe = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
1309
+ return typeof models.planner === "string" && idRe.test(models.planner) && typeof models.builder === "string" && idRe.test(models.builder) && typeof models.finalReviewer === "string" && idRe.test(models.finalReviewer);
1310
+ }
1276
1311
 
1277
1312
  // src/installer/planner.js
1278
1313
  init_json_pointer();
@@ -2801,6 +2836,10 @@ function legacyToShipConfig(legacy, detection = null) {
2801
2836
  ready: legacy.ready ?? { requires: ["review", "local-verification", "remote-ci"], stopAfterReady: true },
2802
2837
  merge: legacy.merge ?? { strategy: "squash", policy: "explicit-user-request-only", requireFreshGates: true },
2803
2838
  cleanup: legacy.cleanup && typeof legacy.cleanup === "object" && "when" in legacy.cleanup ? legacy.cleanup : { when: "next-task", requireUnpublishedGuard: true }
2839
+ },
2840
+ workflow: {
2841
+ models: {},
2842
+ approval: { mirrorToIssue: true, maxFailedRounds: 3 }
2804
2843
  }
2805
2844
  };
2806
2845
  }
@@ -2845,14 +2884,7 @@ async function previewInstall({ rootPath, profile = null, replaceManaged, forceC
2845
2884
  });
2846
2885
  if (candidate.kind === "create" || candidate.kind === "update") {
2847
2886
  const configValue2 = candidate.configValue;
2848
- if (!configValue2.workflow || !configValue2.workflow.models) {
2849
- return { ok: false, error: { kind: "engineering-models-required", message: "engineering profile requires workflow.models.{planner,builder,finalReviewer}" } };
2850
- }
2851
- const { planner, builder, finalReviewer } = configValue2.workflow.models;
2852
- if (!planner || !builder || !finalReviewer) {
2853
- return { ok: false, error: { kind: "engineering-models-required", message: "all three workflow.models.* are required" } };
2854
- }
2855
- if (!configValue2.workflow.approval || !configValue2.workflow.approval.mirrorToIssue || configValue2.workflow.approval.maxFailedRounds !== 3) {
2887
+ if (!configValue2.workflow || !configValue2.workflow.approval) {
2856
2888
  return { ok: false, error: { kind: "engineering-approval-required", message: "engineering profile requires workflow.approval.{mirrorToIssue:true, maxFailedRounds:3}" } };
2857
2889
  }
2858
2890
  }
@@ -2866,7 +2898,8 @@ async function previewInstall({ rootPath, profile = null, replaceManaged, forceC
2866
2898
  detection,
2867
2899
  lock,
2868
2900
  forceOverwrite: Boolean(forceConfig),
2869
- migrationSeed: migrationReport?.proposedConfigSeed ?? null
2901
+ migrationSeed: migrationReport?.proposedConfigSeed ?? null,
2902
+ models
2870
2903
  });
2871
2904
  const filePlan = await planFileInstall({ repoRoot, lock, allowUnowned: Boolean(replaceManaged), catalog: activeCatalog });
2872
2905
  const staleFilePlan = await planStaleFileRemoval({ repoRoot, lock, staleCatalog });
@@ -2878,6 +2911,7 @@ async function previewInstall({ rootPath, profile = null, replaceManaged, forceC
2878
2911
  });
2879
2912
  const planMode = resolved.profile === "engineering" ? { id: "/agent/plan/permission", block: planModePermissions().build, scope: "engineering" } : null;
2880
2913
  const rootPlan = await planRootConfigApply({ repoRoot, lock, forceRepair: Boolean(forceRootConfig), planMode });
2914
+ const setupPending = resolved.profile === "engineering" && !lock?.manager?.setupComplete && !hasCompletedModels(configValue?.workflow?.models ?? {}) && !models?.planner;
2881
2915
  const plan = [...filePlan ?? [], ...staleFilePlan, ...migrationPlan, configPlan, rootPlan];
2882
2916
  const conflicts = plan.filter((p) => p && p.kind === "conflict");
2883
2917
  const summary = summarise(plan);
@@ -2892,7 +2926,8 @@ async function previewInstall({ rootPath, profile = null, replaceManaged, forceC
2892
2926
  plan,
2893
2927
  conflicts,
2894
2928
  summary,
2895
- migrationReport
2929
+ migrationReport,
2930
+ setupPending
2896
2931
  };
2897
2932
  }
2898
2933
  async function previewUninstall({ rootPath }) {
@@ -2927,7 +2962,7 @@ function summarise(plan) {
2927
2962
  }
2928
2963
  return counts;
2929
2964
  }
2930
- async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profile = null }) {
2965
+ async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profile = null, models = null }) {
2931
2966
  const files = [];
2932
2967
  const remain = lock?.files?.filter((f) => !plan.some((op) => op?.relPath === f.path)) ?? [];
2933
2968
  for (const op of plan) {
@@ -2955,18 +2990,18 @@ async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profil
2955
2990
  const rootPointers = rootPlan?.pointerRecords ?? lock?.manager?.rootDocuments?.[0]?.pointers ?? [];
2956
2991
  const hasRootPlan = Boolean(rootPlan?.target || rootPlan?.pointerRecords && rootPlan.pointerRecords.length > 0);
2957
2992
  const hasRootDocuments = rootPlan?.pointerRecords && rootPlan.pointerRecords.length > 0 || lock?.manager?.rootDocuments && lock.manager.rootDocuments.length > 0;
2993
+ const configModels = configPlan?.configValue?.workflow?.models ?? lock?.manager?.config?.models ?? {};
2994
+ const completedModels = hasCompletedModels({ workflow: { models: configModels } });
2958
2995
  return {
2959
2996
  contractVersion: CURRENT_LOCK_SCHEMA,
2960
2997
  manager: {
2961
2998
  schemaVersion: CURRENT_LOCK_SCHEMA,
2962
2999
  name: "opencode-ship",
2963
- version: "0.10.0",
3000
+ version: "1.1.0-rc.1",
2964
3001
  templateSet: TEMPLATE_SET_ID,
2965
- // Newly written locks always carry the resolved profile so
2966
- // future CLI invocations without --profile still resolve to
2967
- // the same choice through the lock-precedence layer.
2968
3002
  profile: profile ?? lock?.manager?.profile,
2969
3003
  appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
3004
+ setupComplete: completedModels,
2970
3005
  config: {
2971
3006
  path: ".opencode/ship.config.json",
2972
3007
  sha256: configSha ?? lock?.manager?.config?.sha256 ?? "",
@@ -3383,7 +3418,35 @@ async function runDoctor({ rootPath, profile, json, writeOutput = true }) {
3383
3418
  return { issues, exitCode, plan, checks, profile: resolved };
3384
3419
  }
3385
3420
 
3421
+ // src/installer/setup-pending.js
3422
+ import { existsSync as existsSync15, readFileSync as readFileSync6, unlinkSync, writeFile as writeFile6, mkdir as mkdirAsync } from "node:fs";
3423
+ import { promisify } from "node:util";
3424
+ import { resolve as resolve14, dirname as dirname8 } from "node:path";
3425
+ var writeFileAsync = promisify(writeFile6);
3426
+ var mkdirAsyncAsync = promisify(mkdirAsync);
3427
+ var REL_PATH = ".opencode/ship.setup-pending.json";
3428
+ function setupPendingPath(repoRoot) {
3429
+ return resolve14(repoRoot, REL_PATH);
3430
+ }
3431
+ async function writeSetupPending(repoRoot, payload) {
3432
+ const path = setupPendingPath(repoRoot);
3433
+ await mkdirAsyncAsync(dirname8(path), { recursive: true });
3434
+ await writeFileAsync(path, JSON.stringify(payload, null, 2) + "\n", "utf8");
3435
+ }
3436
+ function clearSetupPending(repoRoot) {
3437
+ const path = setupPendingPath(repoRoot);
3438
+ if (!existsSync15(path)) return false;
3439
+ try {
3440
+ unlinkSync(path);
3441
+ return true;
3442
+ } catch {
3443
+ return false;
3444
+ }
3445
+ }
3446
+
3386
3447
  // src/installer/commands/init.js
3448
+ var writeFileAsync2 = promisify2(writeFile7);
3449
+ var mkdirAsyncAsync2 = promisify2(mkdirAsync2);
3387
3450
  async function runInit(options) {
3388
3451
  try {
3389
3452
  validateCatalog();
@@ -3393,21 +3456,23 @@ async function runInit(options) {
3393
3456
  }
3394
3457
  throw e;
3395
3458
  }
3396
- const preview = await previewInstall({
3397
- rootPath: options.rootPath ?? null,
3398
- profile: options.profile ?? null,
3399
- replaceManaged: false,
3400
- forceConfig: Boolean(options.forceConfig),
3401
- forceRootConfig: Boolean(options.forceRootConfig),
3402
- models: options.models ?? null
3403
- });
3459
+ let preview;
3460
+ try {
3461
+ preview = await previewInstall({
3462
+ rootPath: options.rootPath ?? null,
3463
+ profile: options.profile ?? null,
3464
+ replaceManaged: false,
3465
+ forceConfig: Boolean(options.forceConfig),
3466
+ forceRootConfig: Boolean(options.forceRootConfig),
3467
+ models: options.models ?? null
3468
+ });
3469
+ } catch (e) {
3470
+ return emitFailure(2, e?.message ?? "invalid input", options.json, "init");
3471
+ }
3404
3472
  if (!preview.ok) {
3405
3473
  if (preview.error?.kind === "unsupported-lock-schema") {
3406
3474
  return emitFailure(5, `unsupported lock schema: ${(preview.error.issues ?? []).join("; ")}`, options.json, "init");
3407
3475
  }
3408
- if (preview.error?.kind === "engineering-models-required") {
3409
- return emitFailure(2, preview.error.message, options.json, "init");
3410
- }
3411
3476
  if (preview.error?.kind === "engineering-approval-required") {
3412
3477
  return emitFailure(2, preview.error.message, options.json, "init");
3413
3478
  }
@@ -3438,6 +3503,14 @@ async function runInit(options) {
3438
3503
  exitCode = 1;
3439
3504
  }
3440
3505
  }
3506
+ const setupPending = Boolean(preview.setupPending);
3507
+ if (setupPending && preview.repoRoot) {
3508
+ await writeSetupPending(preview.repoRoot, {
3509
+ profile: preview.profile?.profile ?? "engineering",
3510
+ reason: "workflow.models is empty; run /setup-ship-workflow to fill in model roles",
3511
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
3512
+ });
3513
+ }
3441
3514
  if (options.json) {
3442
3515
  const envelope = {
3443
3516
  reportVersion: 1,
@@ -3449,19 +3522,56 @@ async function runInit(options) {
3449
3522
  diagnostics: committed.diagnostics ?? [],
3450
3523
  doctor: doctor.issues ?? [],
3451
3524
  doctorChecks: doctor.checks ?? [],
3525
+ setupPending,
3452
3526
  exitCode
3453
3527
  };
3454
3528
  Object.assign(envelope, committed.extra ?? {}, { doctor: doctor.issues ?? [] });
3455
3529
  process.stdout.write(JSON.stringify(envelope, null, 2) + "\n");
3456
- } else if (exitCode !== 0) {
3457
- process.stdout.write(`opencode-ship: doctor reported ${doctor.issues.length} unhealthy check(s)
3458
- `);
3459
3530
  } else {
3460
- process.stdout.write(`opencode-ship: installed; doctor OK
3461
- `);
3531
+ printHumanResult({
3532
+ prefix: "opencode-ship",
3533
+ exitCode,
3534
+ doctorIssues: doctor.issues,
3535
+ setupPending
3536
+ });
3462
3537
  }
3463
3538
  process.exitCode = exitCode;
3464
- return { ok: exitCode === 0, exitCode };
3539
+ return { ok: exitCode === 0, exitCode, setupPending };
3540
+ }
3541
+ function printHumanResult({ prefix, exitCode, doctorIssues, setupPending }) {
3542
+ const lines = [];
3543
+ if (exitCode === 0) {
3544
+ lines.push(`${prefix}: installed; doctor OK`);
3545
+ } else {
3546
+ lines.push(`${prefix}: installed with warnings`);
3547
+ }
3548
+ if (Array.isArray(doctorIssues) && doctorIssues.length > 0) {
3549
+ lines.push("");
3550
+ lines.push("Doctor reported:");
3551
+ for (const issue of doctorIssues) lines.push(` - ${issue}`);
3552
+ }
3553
+ if (setupPending) {
3554
+ lines.push("");
3555
+ lines.push("NEXT:");
3556
+ lines.push(" 1. Restart OpenCode in this repo (if you haven't already).");
3557
+ lines.push(" 2. In chat, run: /setup-ship-workflow");
3558
+ lines.push(" (or type: continue ship setup)");
3559
+ lines.push(" 3. The skill will ask for:");
3560
+ lines.push(" - issue tracker (GitHub / GitLab / local / other)");
3561
+ lines.push(" - triage labels (defaults are fine)");
3562
+ lines.push(" - domain docs (single-context default)");
3563
+ lines.push(" - AI model roles (planner / builder / finalReviewer)");
3564
+ lines.push(" 4. After setup, try: Ship issue <number>");
3565
+ lines.push("");
3566
+ lines.push("The controller will refuse to dispatch until setup is complete.");
3567
+ } else {
3568
+ lines.push("");
3569
+ lines.push("NEXT:");
3570
+ lines.push(" 1. Restart OpenCode in this repo.");
3571
+ lines.push(" 2. Run: opencode-ship doctor && to confirm everything is clean.");
3572
+ lines.push(" 3. Run: Ship issue <number> (or /setup-ship-workflow to customise first)");
3573
+ }
3574
+ process.stdout.write(lines.join("\n") + "\n");
3465
3575
  }
3466
3576
  function emitFailure(code, message, json, command) {
3467
3577
  if (json) {
@@ -3606,11 +3716,15 @@ async function runUpdate(options) {
3606
3716
  }
3607
3717
  throw e;
3608
3718
  }
3719
+ const hasExplicitModels = options.models && (options.models.planner || options.models.builder || options.models.finalReviewer);
3609
3720
  const preview = await previewInstall({
3610
3721
  rootPath: options.rootPath,
3611
3722
  profile: options.profile ?? null,
3612
3723
  replaceManaged: options.replaceManaged,
3613
- forceConfig: options.forceConfig,
3724
+ // If the user passes model flags, the run is explicitly
3725
+ // populating workflow.models. We must rewrite the config in
3726
+ // that case even when the existing config is otherwise valid.
3727
+ forceConfig: Boolean(options.forceConfig || hasExplicitModels),
3614
3728
  forceRootConfig: options.forceRootConfig,
3615
3729
  models: options.models ?? null
3616
3730
  });
@@ -3627,6 +3741,9 @@ async function runUpdate(options) {
3627
3741
  return emitFailure2(3, "modified managed files; rerun with --replace-managed", options.json, "update");
3628
3742
  }
3629
3743
  const committed = await commitInstall(preview, { json: options.json, command: "update" });
3744
+ if (committed.extra?.exitCode === 0 && preview.repoRoot && !preview.setupPending) {
3745
+ clearSetupPending(preview.repoRoot);
3746
+ }
3630
3747
  if (options.json) {
3631
3748
  process.stdout.write(JSON.stringify({
3632
3749
  reportVersion: 1,
@@ -3637,6 +3754,7 @@ async function runUpdate(options) {
3637
3754
  summary: committed.summary ?? {},
3638
3755
  diagnostics: committed.diagnostics ?? [],
3639
3756
  exitCode: committed.extra?.exitCode ?? 0,
3757
+ setupPending: Boolean(preview.setupPending),
3640
3758
  ...committed.extra ?? {}
3641
3759
  }, null, 2) + "\n");
3642
3760
  } else if (committed.extra?.exitCode === 0) {
package/dist/core.js CHANGED
@@ -1,4 +1,4 @@
1
- // opencode-ship/core v0.10.0
1
+ // opencode-ship/core v1.1.0-rc.1
2
2
 
3
3
  // src/adapter.js
4
4
  import { readFile, writeFile, mkdir, rename } from "node:fs/promises";