opencode-ship 1.1.1 → 1.1.2-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // opencode-ship CLI v1.1.1
2
+ // opencode-ship CLI v1.1.2-rc.1
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __esm = (fn, res) => function __init() {
@@ -110,7 +110,7 @@ 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.1";
113
+ PACKAGE_VERSION = "1.1.2-rc.1";
114
114
  TEMPLATE_SET = `v${PACKAGE_VERSION}`;
115
115
  }
116
116
  });
@@ -1145,6 +1145,34 @@ var init_root_config = __esm({
1145
1145
  pointer: "/agent/build/permission/task/delivery-verifier",
1146
1146
  strategy: "value",
1147
1147
  value: "allow"
1148
+ },
1149
+ // Build -> ship-controller delegation so the deep plan/build/review
1150
+ // chain works with subagent_depth=2.
1151
+ {
1152
+ pointer: "/agent/build/permission/task/ship-controller",
1153
+ strategy: "value",
1154
+ value: "allow"
1155
+ },
1156
+ {
1157
+ pointer: "/subagent_depth",
1158
+ strategy: "value",
1159
+ value: 2
1160
+ },
1161
+ // Build-tool permission ask/allow/deny surface.
1162
+ {
1163
+ pointer: "/agent/build/permission/ship_plan_approve",
1164
+ strategy: "value",
1165
+ value: "ask"
1166
+ },
1167
+ {
1168
+ pointer: "/agent/build/permission/ship_resume",
1169
+ strategy: "value",
1170
+ value: "allow"
1171
+ },
1172
+ {
1173
+ pointer: "/agent/build/permission/ship_status",
1174
+ strategy: "value",
1175
+ value: "allow"
1148
1176
  }
1149
1177
  ];
1150
1178
  ROOT_PATH_CANDIDATES = ["opencode.json", "opencode.jsonc"];
@@ -1278,6 +1306,114 @@ var init_root_config = __esm({
1278
1306
  }
1279
1307
  });
1280
1308
 
1309
+ // src/installer/lock.js
1310
+ import { readFile as readFile4, writeFile as writeFile2, rename as rename2, mkdir as mkdir2 } from "node:fs/promises";
1311
+ import { existsSync as existsSync8 } from "node:fs";
1312
+ import { dirname as dirname4, resolve as resolve6 } from "node:path";
1313
+ function lockPath(repoRoot) {
1314
+ return resolve6(repoRoot, ".opencode", "ship.lock.json");
1315
+ }
1316
+ function computeIntegrity(lock) {
1317
+ const { integrity: _ignored, ...without } = lock ?? {};
1318
+ void _ignored;
1319
+ return {
1320
+ lockSha256: bytesHashString(stableStringify(without))
1321
+ };
1322
+ }
1323
+ function normalizeLegacyLock(lock) {
1324
+ if (!lock || typeof lock !== "object") return lock;
1325
+ const { cleanupPending: _drop, ...rest } = lock;
1326
+ void _drop;
1327
+ return rest;
1328
+ }
1329
+ function validateLock(rawLock) {
1330
+ if (rawLock === null || rawLock === void 0) {
1331
+ return { ok: true, kind: "missing", issues: [] };
1332
+ }
1333
+ if (typeof rawLock !== "object" || Array.isArray(rawLock)) {
1334
+ return { ok: false, kind: "shape", issues: ["lock root must be an object"] };
1335
+ }
1336
+ const issues = [];
1337
+ let kind = "ok";
1338
+ if (rawLock.contractVersion !== CURRENT_LOCK_SCHEMA && rawLock.contractVersion !== 3 && rawLock.contractVersion !== 2 && rawLock.contractVersion !== 1) {
1339
+ issues.push(`unsupported contractVersion: ${JSON.stringify(rawLock.contractVersion)} (expected ${CURRENT_LOCK_SCHEMA}, 3, 2, or 1)`);
1340
+ kind = "schema";
1341
+ }
1342
+ const manager = rawLock.manager;
1343
+ if (manager === void 0) {
1344
+ issues.push("manager section missing");
1345
+ kind = kind === "ok" ? "shape" : kind;
1346
+ } else if (typeof manager !== "object" || manager === null) {
1347
+ issues.push("manager section must be an object");
1348
+ kind = kind === "ok" ? "shape" : kind;
1349
+ } else if (manager.schemaVersion !== CURRENT_LOCK_SCHEMA && manager.schemaVersion !== 3 && manager.schemaVersion !== 2 && manager.schemaVersion !== 1) {
1350
+ issues.push(`unsupported manager.schemaVersion: ${JSON.stringify(manager.schemaVersion)} (expected ${CURRENT_LOCK_SCHEMA}, 3, 2, or 1)`);
1351
+ kind = "schema";
1352
+ } else if (manager.name !== "opencode-ship") {
1353
+ issues.push(`unknown manager.name: ${JSON.stringify(manager.name)}`);
1354
+ kind = "shape";
1355
+ } else if (rawLock.contractVersion >= 2 && manager.schemaVersion >= 2 && manager.profile !== void 0 && manager.profile !== "core" && manager.profile !== "engineering") {
1356
+ issues.push(`invalid manager.profile: ${JSON.stringify(manager.profile)} (expected one of: engineering, core [legacy])`);
1357
+ kind = "shape";
1358
+ }
1359
+ if (!rawLock.files || !Array.isArray(rawLock.files)) {
1360
+ issues.push("files must be an array");
1361
+ kind = kind === "ok" ? "shape" : kind;
1362
+ }
1363
+ if (!rawLock.integrity || typeof rawLock.integrity !== "object") {
1364
+ issues.push("integrity section missing");
1365
+ kind = kind === "ok" ? "shape" : kind;
1366
+ } else {
1367
+ const expected = computeIntegrity(rawLock).lockSha256;
1368
+ if (expected !== rawLock.integrity.lockSha256) {
1369
+ issues.push(`integrity mismatch: stored ${rawLock.integrity.lockSha256} != computed ${expected}`);
1370
+ kind = "integrity";
1371
+ }
1372
+ }
1373
+ return { ok: issues.length === 0, kind, issues };
1374
+ }
1375
+ async function readValidatedLock(repoRoot) {
1376
+ const path = lockPath(repoRoot);
1377
+ if (!existsSync8(path)) {
1378
+ return { kind: "missing", lock: null, issues: [] };
1379
+ }
1380
+ let raw;
1381
+ try {
1382
+ const text = await readFile4(path, "utf8");
1383
+ raw = JSON.parse(text);
1384
+ } catch (e) {
1385
+ return {
1386
+ kind: "integrity",
1387
+ lock: null,
1388
+ issues: [`unable to parse lock JSON: ${e?.message ?? String(e)}`]
1389
+ };
1390
+ }
1391
+ const validation = validateLock(raw);
1392
+ if (validation.ok) {
1393
+ return {
1394
+ kind: validation.kind,
1395
+ lock: normalizeLegacyLock(raw),
1396
+ issues: []
1397
+ };
1398
+ }
1399
+ return { kind: validation.kind, lock: null, issues: validation.issues };
1400
+ }
1401
+ function isSetupComplete(lock) {
1402
+ if (!lock || typeof lock !== "object") return false;
1403
+ const manager = lock.manager;
1404
+ if (!manager || typeof manager !== "object") return false;
1405
+ return manager.setupComplete === true;
1406
+ }
1407
+ var CURRENT_LOCK_SCHEMA;
1408
+ var init_lock = __esm({
1409
+ "src/installer/lock.js"() {
1410
+ init_hash();
1411
+ init_json_pointer();
1412
+ init_profile();
1413
+ CURRENT_LOCK_SCHEMA = 4;
1414
+ }
1415
+ });
1416
+
1281
1417
  // src/installer/agent-renderer.js
1282
1418
  var agent_renderer_exports = {};
1283
1419
  __export(agent_renderer_exports, {
@@ -1346,18 +1482,116 @@ var init_agent_renderer = __esm({
1346
1482
  }
1347
1483
  });
1348
1484
 
1485
+ // src/installer/setup-pending.js
1486
+ import { existsSync as existsSync14, readFileSync as readFileSync5, unlinkSync, writeFile as writeFile7, mkdir as mkdirAsync } from "node:fs";
1487
+ import { promisify } from "node:util";
1488
+ import { resolve as resolve13, dirname as dirname9 } from "node:path";
1489
+ function setupPendingPath(repoRoot) {
1490
+ return resolve13(repoRoot, REL_PATH);
1491
+ }
1492
+ async function writeSetupPending(repoRoot, payload) {
1493
+ const path = setupPendingPath(repoRoot);
1494
+ await mkdirAsyncAsync(dirname9(path), { recursive: true });
1495
+ await writeFileAsync(path, JSON.stringify(payload, null, 2) + "\n", "utf8");
1496
+ }
1497
+ function clearSetupPending(repoRoot) {
1498
+ const path = setupPendingPath(repoRoot);
1499
+ if (!existsSync14(path)) return false;
1500
+ try {
1501
+ unlinkSync(path);
1502
+ return true;
1503
+ } catch {
1504
+ return false;
1505
+ }
1506
+ }
1507
+ var writeFileAsync, mkdirAsyncAsync, REL_PATH;
1508
+ var init_setup_pending = __esm({
1509
+ "src/installer/setup-pending.js"() {
1510
+ writeFileAsync = promisify(writeFile7);
1511
+ mkdirAsyncAsync = promisify(mkdirAsync);
1512
+ REL_PATH = ".opencode/ship.setup-pending.json";
1513
+ }
1514
+ });
1515
+
1516
+ // src/installer/setup-state.js
1517
+ var setup_state_exports = {};
1518
+ __export(setup_state_exports, {
1519
+ SETUP_PENDING_REL_PATH: () => SETUP_PENDING_REL_PATH,
1520
+ SETUP_REQUIREMENTS: () => SETUP_REQUIREMENTS,
1521
+ modelsComplete: () => modelsComplete,
1522
+ setupComplete: () => setupComplete
1523
+ });
1524
+ import { existsSync as existsSync15 } from "node:fs";
1525
+ import { readFile as readFile10 } from "node:fs/promises";
1526
+ import { resolve as resolve14, dirname as dirname10 } from "node:path";
1527
+ function modelsComplete(repoRoot, configValue) {
1528
+ if (configValue === void 0) return false;
1529
+ return hasCompletedModels(configValue);
1530
+ }
1531
+ async function setupComplete(repoRoot, configValue) {
1532
+ const missing = [];
1533
+ for (const rel of REQUIRED_DOCS) {
1534
+ const path = resolve14(repoRoot, rel);
1535
+ if (!existsSync15(path)) missing.push(rel);
1536
+ }
1537
+ const agentsPath = resolve14(repoRoot, "AGENTS.md");
1538
+ let agentsOk = false;
1539
+ if (existsSync15(agentsPath)) {
1540
+ try {
1541
+ const raw = await readFile10(agentsPath, "utf8");
1542
+ agentsOk = /##\s+Ship workflow\b/.test(raw);
1543
+ } catch {
1544
+ agentsOk = false;
1545
+ }
1546
+ if (!agentsOk) missing.push("AGENTS.md Ship workflow block");
1547
+ } else {
1548
+ missing.push("AGENTS.md");
1549
+ }
1550
+ const lockResult = await readValidatedLock(repoRoot);
1551
+ const cfgOk = modelsComplete(repoRoot, configValue);
1552
+ const markerPath = setupPendingPath(repoRoot);
1553
+ if (existsSync15(markerPath)) missing.push("setup-pending marker");
1554
+ const lockOk = lockResult.kind === "ok" || lockResult.kind === "missing";
1555
+ const ok = cfgOk && lockOk && missing.length === 0;
1556
+ return {
1557
+ ok,
1558
+ missing,
1559
+ config: { ok: cfgOk, lock: { ok: lockOk, setupComplete: isSetupComplete(lockResult.lock) } }
1560
+ };
1561
+ }
1562
+ var REQUIRED_DOCS, SETUP_PENDING_REL_PATH, SETUP_REQUIREMENTS;
1563
+ var init_setup_state = __esm({
1564
+ "src/installer/setup-state.js"() {
1565
+ init_config();
1566
+ init_lock();
1567
+ init_setup_pending();
1568
+ REQUIRED_DOCS = [
1569
+ "docs/agents/issue-tracker.md",
1570
+ "docs/agents/domain.md",
1571
+ "docs/agents/triage-labels.md"
1572
+ ];
1573
+ SETUP_PENDING_REL_PATH = ".opencode/ship.setup-pending.json";
1574
+ SETUP_REQUIREMENTS = Object.freeze({
1575
+ docs: REQUIRED_DOCS,
1576
+ agentFile: "AGENTS.md",
1577
+ markerPath: SETUP_PENDING_REL_PATH
1578
+ });
1579
+ }
1580
+ });
1581
+
1349
1582
  // src/installer/cli-args.js
1350
1583
  init_profile();
1351
1584
  var USAGE = `opencode-ship <command> [options]
1352
1585
 
1353
1586
  Commands:
1354
- init Install managed files in this project. One-liner: pnpm dlx opencode-ship@latest init
1355
- diff Show what would change without writing.
1356
- update Apply pending updates after recovering the journal.
1357
- doctor Validate environment, lock, and references.
1358
- uninstall Remove managed files that still match the lock.
1359
- --version Print the version and exit.
1360
- --help Show this usage and exit.
1587
+ init Install managed files in this project. One-liner: pnpm dlx opencode-ship@latest init
1588
+ diff Show what would change without writing.
1589
+ update Apply pending updates after recovering the journal.
1590
+ doctor Validate environment, lock, and references.
1591
+ setup-complete Sole writer of lock.manager.setupComplete = true. Validates models + docs + AGENTS.md.
1592
+ uninstall Remove managed files that still match the lock.
1593
+ --version Print the version and exit.
1594
+ --help Show this usage and exit.
1361
1595
 
1362
1596
  Options:
1363
1597
  --root <path> Project root (defaults to cwd).
@@ -1372,8 +1606,10 @@ Options:
1372
1606
  --final-reviewer-model <id> Final Standards + Spec reviewer model id (init only, optional).
1373
1607
  --json Emit a JSON envelope instead of human output.
1374
1608
 
1375
- After init succeeds, restart OpenCode and run /setup-ship-workflow to
1376
- fill in the workflow.models fields and the per-repo docs.
1609
+ After init succeeds, restart OpenCode and run /setup-ship-workflow.
1610
+ The setup skill calls 'opencode-ship setup-complete' at the end so
1611
+ the lock.manager.setupComplete flag flips to true and the ship
1612
+ controller can dispatch.
1377
1613
  `;
1378
1614
  var MODEL_ID_RE = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
1379
1615
  function parseFlags(argv) {
@@ -1444,6 +1680,7 @@ function parseCommand(argv) {
1444
1680
  case "diff":
1445
1681
  case "update":
1446
1682
  case "doctor":
1683
+ case "setup-complete":
1447
1684
  case "uninstall":
1448
1685
  return { command: cmd, options: flags };
1449
1686
  default:
@@ -1457,7 +1694,7 @@ function helpText() {
1457
1694
  // src/installer/commands/init.js
1458
1695
  import { promisify as promisify2 } from "node:util";
1459
1696
  import { writeFile as writeFile8, mkdir as mkdirAsync2 } from "node:fs/promises";
1460
- import { dirname as dirname10, resolve as resolvePath } from "node:path";
1697
+ import { dirname as dirname11, resolve as resolvePath } from "node:path";
1461
1698
 
1462
1699
  // src/installer/executor.js
1463
1700
  init_catalog();
@@ -2212,104 +2449,8 @@ async function planRootConfigApply({ repoRoot, lock, forceRepair, planMode = nul
2212
2449
  });
2213
2450
  }
2214
2451
 
2215
- // src/installer/lock.js
2216
- init_hash();
2217
- init_json_pointer();
2218
- init_profile();
2219
- import { readFile as readFile4, writeFile as writeFile2, rename as rename2, mkdir as mkdir2 } from "node:fs/promises";
2220
- import { existsSync as existsSync8 } from "node:fs";
2221
- import { dirname as dirname4, resolve as resolve6 } from "node:path";
2222
- var CURRENT_LOCK_SCHEMA = 4;
2223
- function lockPath(repoRoot) {
2224
- return resolve6(repoRoot, ".opencode", "ship.lock.json");
2225
- }
2226
- function computeIntegrity(lock) {
2227
- const { integrity: _ignored, ...without } = lock ?? {};
2228
- void _ignored;
2229
- return {
2230
- lockSha256: bytesHashString(stableStringify(without))
2231
- };
2232
- }
2233
- function normalizeLegacyLock(lock) {
2234
- if (!lock || typeof lock !== "object") return lock;
2235
- const { cleanupPending: _drop, ...rest } = lock;
2236
- void _drop;
2237
- return rest;
2238
- }
2239
- function validateLock(rawLock) {
2240
- if (rawLock === null || rawLock === void 0) {
2241
- return { ok: true, kind: "missing", issues: [] };
2242
- }
2243
- if (typeof rawLock !== "object" || Array.isArray(rawLock)) {
2244
- return { ok: false, kind: "shape", issues: ["lock root must be an object"] };
2245
- }
2246
- const issues = [];
2247
- let kind = "ok";
2248
- if (rawLock.contractVersion !== CURRENT_LOCK_SCHEMA && rawLock.contractVersion !== 3 && rawLock.contractVersion !== 2 && rawLock.contractVersion !== 1) {
2249
- issues.push(`unsupported contractVersion: ${JSON.stringify(rawLock.contractVersion)} (expected ${CURRENT_LOCK_SCHEMA}, 3, 2, or 1)`);
2250
- kind = "schema";
2251
- }
2252
- const manager = rawLock.manager;
2253
- if (manager === void 0) {
2254
- issues.push("manager section missing");
2255
- kind = kind === "ok" ? "shape" : kind;
2256
- } else if (typeof manager !== "object" || manager === null) {
2257
- issues.push("manager section must be an object");
2258
- kind = kind === "ok" ? "shape" : kind;
2259
- } else if (manager.schemaVersion !== CURRENT_LOCK_SCHEMA && manager.schemaVersion !== 3 && manager.schemaVersion !== 2 && manager.schemaVersion !== 1) {
2260
- issues.push(`unsupported manager.schemaVersion: ${JSON.stringify(manager.schemaVersion)} (expected ${CURRENT_LOCK_SCHEMA}, 3, 2, or 1)`);
2261
- kind = "schema";
2262
- } else if (manager.name !== "opencode-ship") {
2263
- issues.push(`unknown manager.name: ${JSON.stringify(manager.name)}`);
2264
- kind = "shape";
2265
- } else if (rawLock.contractVersion >= 2 && manager.schemaVersion >= 2 && manager.profile !== void 0 && manager.profile !== "core" && manager.profile !== "engineering") {
2266
- issues.push(`invalid manager.profile: ${JSON.stringify(manager.profile)} (expected one of: engineering, core [legacy])`);
2267
- kind = "shape";
2268
- }
2269
- if (!rawLock.files || !Array.isArray(rawLock.files)) {
2270
- issues.push("files must be an array");
2271
- kind = kind === "ok" ? "shape" : kind;
2272
- }
2273
- if (!rawLock.integrity || typeof rawLock.integrity !== "object") {
2274
- issues.push("integrity section missing");
2275
- kind = kind === "ok" ? "shape" : kind;
2276
- } else {
2277
- const expected = computeIntegrity(rawLock).lockSha256;
2278
- if (expected !== rawLock.integrity.lockSha256) {
2279
- issues.push(`integrity mismatch: stored ${rawLock.integrity.lockSha256} != computed ${expected}`);
2280
- kind = "integrity";
2281
- }
2282
- }
2283
- return { ok: issues.length === 0, kind, issues };
2284
- }
2285
- async function readValidatedLock(repoRoot) {
2286
- const path = lockPath(repoRoot);
2287
- if (!existsSync8(path)) {
2288
- return { kind: "missing", lock: null, issues: [] };
2289
- }
2290
- let raw;
2291
- try {
2292
- const text = await readFile4(path, "utf8");
2293
- raw = JSON.parse(text);
2294
- } catch (e) {
2295
- return {
2296
- kind: "integrity",
2297
- lock: null,
2298
- issues: [`unable to parse lock JSON: ${e?.message ?? String(e)}`]
2299
- };
2300
- }
2301
- const validation = validateLock(raw);
2302
- if (validation.ok) {
2303
- return {
2304
- kind: validation.kind,
2305
- lock: normalizeLegacyLock(raw),
2306
- issues: []
2307
- };
2308
- }
2309
- return { kind: validation.kind, lock: null, issues: validation.issues };
2310
- }
2311
-
2312
2452
  // src/installer/executor.js
2453
+ init_lock();
2313
2454
  init_config();
2314
2455
  init_hash();
2315
2456
  init_json_pointer();
@@ -2459,8 +2600,11 @@ function detectProject(repoRoot = process.cwd()) {
2459
2600
 
2460
2601
  // src/installer/executor.js
2461
2602
  init_root_config();
2603
+ init_lock();
2462
2604
 
2463
2605
  // src/installer/transaction.js
2606
+ init_lock();
2607
+ init_hash();
2464
2608
  import {
2465
2609
  writeFile as writeFile4,
2466
2610
  rename as rename4,
@@ -2472,7 +2616,6 @@ import {
2472
2616
  } from "node:fs/promises";
2473
2617
  import { existsSync as existsSync11 } from "node:fs";
2474
2618
  import { dirname as dirname6, resolve as resolve10, join as join5 } from "node:path";
2475
- init_hash();
2476
2619
 
2477
2620
  // src/state/git-common-dir.js
2478
2621
  import { spawn } from "node:child_process";
@@ -2897,10 +3040,11 @@ async function rollback(lockDir, journal) {
2897
3040
  }
2898
3041
 
2899
3042
  // src/installer/migration.js
3043
+ init_lock();
3044
+ init_config();
2900
3045
  import { readFile as readFile7 } from "node:fs/promises";
2901
3046
  import { existsSync as existsSync12 } from "node:fs";
2902
3047
  import { resolve as resolve11 } from "node:path";
2903
- init_config();
2904
3048
  function legacyAdapterPath(repoRoot) {
2905
3049
  return resolve11(repoRoot, ".opencode", "delivery.json");
2906
3050
  }
@@ -3095,7 +3239,7 @@ async function previewInstall({ rootPath, profile = null, replaceManaged, forceC
3095
3239
  });
3096
3240
  const planMode = null;
3097
3241
  const rootPlan = await planRootConfigApply({ repoRoot, lock, forceRepair: Boolean(forceRootConfig), planMode });
3098
- const setupPending = resolved.profile === "engineering" && !lock?.manager?.setupComplete && !hasCompletedModels(configValue) && !models?.planner;
3242
+ const setupPending = resolved.profile === "engineering" && !hasCompletedModels(configValue);
3099
3243
  const plan = [...filePlan ?? [], ...staleFilePlan, ...migrationPlan, configPlan, rootPlan];
3100
3244
  const conflicts = plan.filter((p) => p && p.kind === "conflict");
3101
3245
  const summary = summarise(plan);
@@ -3146,7 +3290,7 @@ function summarise(plan) {
3146
3290
  }
3147
3291
  return counts;
3148
3292
  }
3149
- async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profile = null, models = null }) {
3293
+ async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profile = null, models = null, fullSetupComplete = false }) {
3150
3294
  const files = [];
3151
3295
  const remain = lock?.files?.filter((f) => !plan.some((op) => op?.relPath === f.path)) ?? [];
3152
3296
  for (const op of plan) {
@@ -3182,11 +3326,11 @@ async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profil
3182
3326
  manager: {
3183
3327
  schemaVersion: CURRENT_LOCK_SCHEMA,
3184
3328
  name: "opencode-ship",
3185
- version: "1.1.1",
3329
+ version: "1.1.2-rc.1",
3186
3330
  templateSet: TEMPLATE_SET_ID,
3187
3331
  profile: resolvedProfile,
3188
3332
  appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
3189
- setupComplete: completedModels,
3333
+ setupComplete: Boolean(fullSetupComplete) && completedModels,
3190
3334
  config: {
3191
3335
  path: ".opencode/ship.config.json",
3192
3336
  sha256: configSha ?? lock?.manager?.config?.sha256 ?? "",
@@ -3201,7 +3345,7 @@ async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profil
3201
3345
  files
3202
3346
  };
3203
3347
  }
3204
- async function commitInstall(preview, { json, command }) {
3348
+ async function commitInstall(preview, { json, command, fullSetupComplete = false }) {
3205
3349
  if (!preview.ok) {
3206
3350
  return {
3207
3351
  ok: false,
@@ -3237,7 +3381,8 @@ async function commitInstall(preview, { json, command }) {
3237
3381
  lock: preview.lock,
3238
3382
  configPlan,
3239
3383
  rootPlan,
3240
- profile: preview.profile?.profile
3384
+ profile: preview.profile?.profile,
3385
+ fullSetupComplete
3241
3386
  });
3242
3387
  const txPlan = await stageFiles(fileOnly, repoRoot);
3243
3388
  if (configPlan && (configPlan.kind === "create" || configPlan.kind === "update")) {
@@ -3327,13 +3472,14 @@ function relativeTemplate(source) {
3327
3472
  }
3328
3473
 
3329
3474
  // src/installer/commands/doctor.js
3330
- import { existsSync as existsSync14, readFileSync as readFileSync5 } from "node:fs";
3331
- import { spawnSync as spawnSync2 } from "node:child_process";
3475
+ init_lock();
3332
3476
  init_config();
3477
+ import { existsSync as existsSync16, readFileSync as readFileSync6 } from "node:fs";
3478
+ import { spawnSync as spawnSync2 } from "node:child_process";
3333
3479
  init_hash();
3334
3480
  init_catalog();
3335
3481
  init_root_config();
3336
- import { resolve as resolve13 } from "node:path";
3482
+ import { resolve as resolve15 } from "node:path";
3337
3483
 
3338
3484
  // src/installer/report.js
3339
3485
  var REPORT_VERSION = 1;
@@ -3422,9 +3568,9 @@ function checkPackageIntegrity() {
3422
3568
  function buildSourceHashIndex() {
3423
3569
  const idx = /* @__PURE__ */ new Map();
3424
3570
  for (const entry of CATALOG) {
3425
- if (!existsSync14(entry.source)) continue;
3571
+ if (!existsSync16(entry.source)) continue;
3426
3572
  try {
3427
- const buf = readFileSync5(entry.source, "utf8");
3573
+ const buf = readFileSync6(entry.source, "utf8");
3428
3574
  idx.set(entry.source, bytesHashString(buf));
3429
3575
  } catch {
3430
3576
  }
@@ -3435,13 +3581,13 @@ async function checkCatalogInstall(repoRoot, sourceHashes, profile, renderedAgen
3435
3581
  const rows = [];
3436
3582
  const scoped = profile ? filterCatalogByProfile(CATALOG, profile) : CATALOG;
3437
3583
  for (const entry of scoped) {
3438
- const target = resolve13(repoRoot, entry.path);
3439
- if (!existsSync14(target)) {
3584
+ const target = resolve15(repoRoot, entry.path);
3585
+ if (!existsSync16(target)) {
3440
3586
  rows.push(`${entry.id}: missing`);
3441
3587
  continue;
3442
3588
  }
3443
3589
  try {
3444
- const buf = readFileSync5(target, "utf8");
3590
+ const buf = readFileSync6(target, "utf8");
3445
3591
  const actual = bytesHashString(buf);
3446
3592
  const rendered = renderedAgentMap.get(entry.path);
3447
3593
  const expected = rendered ? rendered.sha256 : sourceHashes.get(entry.source);
@@ -3497,12 +3643,12 @@ async function checkManagedHashes(repoRoot, validatedLock) {
3497
3643
  const drift = [];
3498
3644
  const renderedAgents = await loadRenderedAgentOverrides(repoRoot);
3499
3645
  for (const entry of validatedLock.lock.files ?? []) {
3500
- const p = resolve13(repoRoot, entry.path);
3501
- if (!existsSync14(p)) {
3646
+ const p = resolve15(repoRoot, entry.path);
3647
+ if (!existsSync16(p)) {
3502
3648
  drift.push(`missing:${entry.path}`);
3503
3649
  continue;
3504
3650
  }
3505
- const buf = readFileSync5(p, "utf8");
3651
+ const buf = readFileSync6(p, "utf8");
3506
3652
  const actual = bytesHashString(buf);
3507
3653
  if (actual !== entry.sha256) drift.push(`drift:${entry.path}`);
3508
3654
  }
@@ -3552,6 +3698,18 @@ async function checkRootConfig(repoRoot) {
3552
3698
  detail: conflict ? `conflict on ${conflict.pointer}` : `applied=${r.applied.length}, skipped=${r.skipped.length}`
3553
3699
  };
3554
3700
  }
3701
+ async function checkSetupState(repoRoot, configValue) {
3702
+ const { setupComplete: setupComplete2 } = await Promise.resolve().then(() => (init_setup_state(), setup_state_exports));
3703
+ const state = await setupComplete2(repoRoot, configValue);
3704
+ if (state.ok) {
3705
+ return { name: "setup-complete", ok: true, detail: "models + docs + AGENTS.md all present" };
3706
+ }
3707
+ return {
3708
+ name: "setup-complete",
3709
+ ok: true,
3710
+ detail: `pending: ${state.missing.join(", ") || "(none)"}`
3711
+ };
3712
+ }
3555
3713
  function writeEnvelope({ command, plan, summary, diagnostics, json, exitCode }) {
3556
3714
  const conflicts = plan.filter((p) => p.kind === "conflict");
3557
3715
  if (json) {
@@ -3599,7 +3757,8 @@ async function runDoctor({ rootPath, profile, json, writeOutput = true }) {
3599
3757
  await checkConfig(repoRoot),
3600
3758
  await checkManagedHashes(repoRoot, validatedLock),
3601
3759
  await checkActiveProfileFootprint(repoRoot, validatedLock, resolved.profile),
3602
- await checkRootConfig(repoRoot)
3760
+ await checkRootConfig(repoRoot),
3761
+ await checkSetupState(repoRoot, configValue)
3603
3762
  ];
3604
3763
  const issues = checks.filter((c) => !c.ok).map((c) => `${c.name}: ${c.detail}`);
3605
3764
  const plan = checks.map((c) => ({
@@ -3622,34 +3781,7 @@ async function runDoctor({ rootPath, profile, json, writeOutput = true }) {
3622
3781
  // src/installer/commands/init.js
3623
3782
  init_catalog();
3624
3783
  init_config();
3625
-
3626
- // src/installer/setup-pending.js
3627
- import { existsSync as existsSync15, readFileSync as readFileSync6, unlinkSync, writeFile as writeFile7, mkdir as mkdirAsync } from "node:fs";
3628
- import { promisify } from "node:util";
3629
- import { resolve as resolve14, dirname as dirname9 } from "node:path";
3630
- var writeFileAsync = promisify(writeFile7);
3631
- var mkdirAsyncAsync = promisify(mkdirAsync);
3632
- var REL_PATH = ".opencode/ship.setup-pending.json";
3633
- function setupPendingPath(repoRoot) {
3634
- return resolve14(repoRoot, REL_PATH);
3635
- }
3636
- async function writeSetupPending(repoRoot, payload) {
3637
- const path = setupPendingPath(repoRoot);
3638
- await mkdirAsyncAsync(dirname9(path), { recursive: true });
3639
- await writeFileAsync(path, JSON.stringify(payload, null, 2) + "\n", "utf8");
3640
- }
3641
- function clearSetupPending(repoRoot) {
3642
- const path = setupPendingPath(repoRoot);
3643
- if (!existsSync15(path)) return false;
3644
- try {
3645
- unlinkSync(path);
3646
- return true;
3647
- } catch {
3648
- return false;
3649
- }
3650
- }
3651
-
3652
- // src/installer/commands/init.js
3784
+ init_setup_pending();
3653
3785
  var writeFileAsync2 = promisify2(writeFile8);
3654
3786
  var mkdirAsyncAsync2 = promisify2(mkdirAsync2);
3655
3787
  async function runInit(options) {
@@ -3914,6 +4046,7 @@ async function runDiff(options) {
3914
4046
 
3915
4047
  // src/installer/commands/update.js
3916
4048
  init_catalog();
4049
+ init_setup_pending();
3917
4050
  async function runUpdate(options) {
3918
4051
  try {
3919
4052
  validateCatalog();
@@ -3948,9 +4081,6 @@ async function runUpdate(options) {
3948
4081
  return emitFailure2(3, "modified managed files; rerun with --replace-managed", options.json, "update");
3949
4082
  }
3950
4083
  const committed = await commitInstall(preview, { json: options.json, command: "update" });
3951
- if (committed.extra?.exitCode === 0 && preview.repoRoot && !preview.setupPending) {
3952
- clearSetupPending(preview.repoRoot);
3953
- }
3954
4084
  if (options.json) {
3955
4085
  process.stdout.write(JSON.stringify({
3956
4086
  reportVersion: 1,
@@ -3991,6 +4121,87 @@ function emitFailure2(code, message, json, command) {
3991
4121
  return { ok: false, exitCode: code };
3992
4122
  }
3993
4123
 
4124
+ // src/installer/commands/setup-complete.js
4125
+ init_config();
4126
+ init_setup_state();
4127
+ init_setup_pending();
4128
+ import { existsSync as existsSync17 } from "node:fs";
4129
+ import { resolve as resolve16 } from "node:path";
4130
+ async function runSetupComplete(options) {
4131
+ const repoRoot = options.rootPath ?? process.cwd();
4132
+ if (!existsSync17(resolve16(repoRoot, ".git"))) {
4133
+ return emitFailure3(2, "not a git repository", options.json);
4134
+ }
4135
+ clearSetupPending(repoRoot);
4136
+ const config = await loadConfig(repoRoot);
4137
+ const configValue = config?.ok ? config.value : null;
4138
+ const state = await setupComplete(repoRoot, configValue);
4139
+ const diagnostics = [];
4140
+ if (!state.config.ok) diagnostics.push("workflow.models incomplete");
4141
+ if (!state.config.lock.ok) diagnostics.push("lock is not v4 or fails integrity");
4142
+ if (state.missing.length > 0) {
4143
+ diagnostics.push(`missing: ${state.missing.join(", ")}`);
4144
+ }
4145
+ if (!state.ok) {
4146
+ return emitFailure3(6, `setup incomplete: ${diagnostics.join("; ")}`, options.json, { state });
4147
+ }
4148
+ const preview = await previewInstall({
4149
+ rootPath: repoRoot,
4150
+ profile: "engineering",
4151
+ replaceManaged: false,
4152
+ forceConfig: false,
4153
+ forceRootConfig: false
4154
+ });
4155
+ if (!preview.ok) {
4156
+ return emitFailure3(2, `preview failed: ${preview.error?.kind ?? "unknown"}`, options.json);
4157
+ }
4158
+ if (preview.conflicts.length > 0) {
4159
+ return emitFailure3(3, "managed files conflict; resolve with `opencode-ship update --replace-managed` first", options.json);
4160
+ }
4161
+ const committed = await commitInstall(preview, {
4162
+ json: options.json,
4163
+ command: "setup-complete",
4164
+ fullSetupComplete: true
4165
+ });
4166
+ if (committed.extra?.exitCode !== 0) {
4167
+ return emitFailure3(committed.extra?.exitCode ?? 1, "commit failed", options.json, { committed });
4168
+ }
4169
+ if (options.json) {
4170
+ process.stdout.write(JSON.stringify({
4171
+ reportVersion: 1,
4172
+ command: "setup-complete",
4173
+ status: "ok",
4174
+ setupComplete: true,
4175
+ requirements: SETUP_REQUIREMENTS,
4176
+ plan: serializePlan(committed.plan ?? []),
4177
+ summary: committed.summary ?? {},
4178
+ exitCode: 0
4179
+ }, null, 2) + "\n");
4180
+ } else {
4181
+ process.stdout.write("opencode-ship: setup complete\n");
4182
+ }
4183
+ process.exitCode = 0;
4184
+ return committed;
4185
+ }
4186
+ function emitFailure3(code, message, json, extra) {
4187
+ if (json) {
4188
+ process.stdout.write(JSON.stringify({
4189
+ reportVersion: 1,
4190
+ command: "setup-complete",
4191
+ status: "error",
4192
+ setupComplete: false,
4193
+ diagnostics: [message],
4194
+ ...extra ?? {},
4195
+ exitCode: code
4196
+ }, null, 2) + "\n");
4197
+ } else {
4198
+ process.stdout.write(`opencode-ship: ${message}
4199
+ `);
4200
+ }
4201
+ process.exitCode = code;
4202
+ return { ok: false, exitCode: code };
4203
+ }
4204
+
3994
4205
  // src/installer/commands/uninstall.js
3995
4206
  init_config();
3996
4207
  import { unlink as unlink4 } from "node:fs/promises";
@@ -3998,12 +4209,12 @@ async function runUninstall(options) {
3998
4209
  const preview = await previewUninstall({ rootPath: options.rootPath });
3999
4210
  if (!preview.ok) {
4000
4211
  if (preview.error?.kind === "unsupported-lock-schema") {
4001
- return emitFailure3(5, `unsupported lock schema: ${(preview.error.issues ?? []).join("; ")}`, options.json);
4212
+ return emitFailure4(5, `unsupported lock schema: ${(preview.error.issues ?? []).join("; ")}`, options.json);
4002
4213
  }
4003
4214
  if (preview.error?.kind === "lock-invalid") {
4004
- return emitFailure3(3, `lock invalid: ${(preview.error.issues ?? []).join("; ")}`, options.json);
4215
+ return emitFailure4(3, `lock invalid: ${(preview.error.issues ?? []).join("; ")}`, options.json);
4005
4216
  }
4006
- return emitFailure3(2, preview.error?.kind ?? "invalid-project", options.json);
4217
+ return emitFailure4(2, preview.error?.kind ?? "invalid-project", options.json);
4007
4218
  }
4008
4219
  const { repoRoot, plan, conflicts, summary } = preview;
4009
4220
  if (options.purgeConfig) {
@@ -4021,11 +4232,11 @@ async function runUninstall(options) {
4021
4232
  }
4022
4233
  const tx = await executePlan({ repoRoot, plan, newLockBuilder: null });
4023
4234
  if (!tx.ok) {
4024
- return emitFailure3(4, tx.error?.message ?? "transaction failure", options.json);
4235
+ return emitFailure4(4, tx.error?.message ?? "transaction failure", options.json);
4025
4236
  }
4026
4237
  return emitReport(plan, [], summary, options.json, 0, [tx.recovered ? "journal recovered before uninstall" : ""].filter(Boolean));
4027
4238
  }
4028
- function emitFailure3(code, message, json) {
4239
+ function emitFailure4(code, message, json) {
4029
4240
  if (json) {
4030
4241
  process.stdout.write(JSON.stringify({
4031
4242
  reportVersion: 1,
@@ -4136,6 +4347,9 @@ ${helpText()}`);
4136
4347
  case "doctor":
4137
4348
  await runDoctor({ json: !!opts.json, rootPath: opts.rootPath, profile });
4138
4349
  return;
4350
+ case "setup-complete":
4351
+ await runSetupComplete({ json: !!opts.json, rootPath: opts.rootPath });
4352
+ return;
4139
4353
  case "uninstall":
4140
4354
  await runUninstall({ json: !!opts.json, rootPath: opts.rootPath, profile, purgeConfig: !!opts.purgeConfig });
4141
4355
  return;