githolon 0.100.6 → 0.101.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.mjs CHANGED
@@ -329,6 +329,131 @@ var init_custody_transport = __esm({
329
329
  }
330
330
  });
331
331
 
332
+ // vendor/engine/managed-promotion.ts
333
+ function prepareManagedPromotion(report, candidateHash, domain, opts = {}) {
334
+ if (!report.sourceVerified || !report.complete || !report.evolve?.ok) {
335
+ const reason = !report.sourceVerified ? "the source ledger did not verify" : !report.complete ? "some historical premises could not be re-derived" : "the candidate fails deterministic schema evolution";
336
+ return { ok: false, stage: "preflight", report, error: `promotion refused: ${reason}` };
337
+ }
338
+ const acceptanceReason = opts.acceptanceReason?.trim();
339
+ if (!report.promotable && !acceptanceReason) {
340
+ return {
341
+ ok: false,
342
+ stage: "acceptance",
343
+ report,
344
+ error: "exact history changes under the candidate; provide an acceptanceReason to make that business decision accountable"
345
+ };
346
+ }
347
+ if (!report.atStateRoot || !report.historyHash || !report.reportHash || !report.projection) {
348
+ return {
349
+ ok: false,
350
+ stage: "preflight",
351
+ report,
352
+ error: "the complete pre-flight report is missing its state, projection, or content proof"
353
+ };
354
+ }
355
+ const provedDomains = report.domains?.length ? report.domains : [report.domain];
356
+ if (!provedDomains.includes(domain)) {
357
+ return {
358
+ ok: false,
359
+ stage: "preflight",
360
+ report,
361
+ error: `the package-wide report does not cover the generated business area '${domain}'`
362
+ };
363
+ }
364
+ const reportEvidence = JSON.parse(JSON.stringify(report));
365
+ if (Array.isArray(reportEvidence.divergences)) {
366
+ for (const divergence of reportEvidence.divergences) {
367
+ if (divergence && typeof divergence === "object") {
368
+ delete divergence.atCommit;
369
+ }
370
+ }
371
+ }
372
+ const preflight = {
373
+ candidateHash,
374
+ // A package may be reached through any one of its generated domain clients. The
375
+ // kernel chooses one canonical domain for the private receipt so the exact same
376
+ // package/history proof has one identity regardless of that entry point.
377
+ domain: report.domain,
378
+ atHash: report.atHash,
379
+ atStateRoot: report.atStateRoot,
380
+ historyHash: report.historyHash,
381
+ reportHash: report.reportHash,
382
+ reportJson: JSON.stringify(reportEvidence),
383
+ complete: true,
384
+ evolveOk: true,
385
+ rawPromotable: report.promotable,
386
+ projectionEqual: report.projection.equal,
387
+ replayedIntents: report.replayedIntents,
388
+ candidateIntents: report.candidateIntents,
389
+ divergenceCount: report.divergences.length,
390
+ baselineRoot: report.projection.baselineRoot,
391
+ candidateRoot: report.projection.candidateRoot,
392
+ ...!report.promotable && acceptanceReason ? { acceptanceReason } : {}
393
+ };
394
+ return {
395
+ ok: true,
396
+ stage: "ready",
397
+ report,
398
+ receiptId: `${RELEASE_RECEIPT_PREFIX}${report.reportHash}`,
399
+ payload: {
400
+ domainHash: candidateHash,
401
+ ...opts.replaces ? { replaces: opts.replaces } : {},
402
+ retireReplaced: opts.retireReplaced ?? false,
403
+ ...opts.dispositions ? { dispositions: opts.dispositions } : {},
404
+ preflight
405
+ }
406
+ };
407
+ }
408
+ function prepareManagedRestoration(report, currentHash, priorHash, domain, opts = {}) {
409
+ if (!report.dispositions || typeof report.dispositions !== "object") {
410
+ return {
411
+ ok: false,
412
+ stage: "preflight",
413
+ report,
414
+ error: "restoration refused: the exact-history proof did not derive its predecessor dispositions"
415
+ };
416
+ }
417
+ const acceptanceReason = opts.acceptanceReason?.trim();
418
+ const prepared = prepareManagedPromotion(report, priorHash, domain, {
419
+ dispositions: report.dispositions,
420
+ ...acceptanceReason ? { acceptanceReason } : {}
421
+ });
422
+ if (!prepared.ok) {
423
+ return {
424
+ ...prepared,
425
+ stage: prepared.stage === "acceptance" ? "acceptance" : "preflight"
426
+ };
427
+ }
428
+ if (!acceptanceReason) {
429
+ return {
430
+ ok: false,
431
+ stage: "acceptance",
432
+ report,
433
+ error: "restoring previous law needs a business reason even when historical outcomes are unchanged"
434
+ };
435
+ }
436
+ return {
437
+ ok: true,
438
+ stage: "ready",
439
+ report,
440
+ receiptId: prepared.receiptId,
441
+ payload: {
442
+ candidateHash: currentHash,
443
+ priorHash,
444
+ dispositions: report.dispositions,
445
+ preflight: prepared.payload.preflight
446
+ }
447
+ };
448
+ }
449
+ var RELEASE_RECEIPT_PREFIX;
450
+ var init_managed_promotion = __esm({
451
+ "vendor/engine/managed-promotion.ts"() {
452
+ "use strict";
453
+ RELEASE_RECEIPT_PREFIX = "nomos-release-receipt:";
454
+ }
455
+ });
456
+
332
457
  // vendor/engine/engine.ts
333
458
  var engine_exports = {};
334
459
  __export(engine_exports, {
@@ -360,6 +485,10 @@ __export(engine_exports, {
360
485
  countTenantRowsByHome: () => countTenantRowsByHome,
361
486
  createEngine: () => createEngine,
362
487
  cryptoKeygen: () => cryptoKeygen,
488
+ cryptoOwnedDeviceRequest: () => cryptoOwnedDeviceRequest,
489
+ cryptoOwnedPrepareApproval: () => cryptoOwnedPrepareApproval,
490
+ cryptoOwnedPrepareRecovery: () => cryptoOwnedPrepareRecovery,
491
+ cryptoOwnedScopeKey: () => cryptoOwnedScopeKey,
363
492
  cryptoSealField: () => cryptoSealField,
364
493
  cryptoUnwrapKey: () => cryptoUnwrapKey,
365
494
  cryptoWrapKey: () => cryptoWrapKey,
@@ -383,6 +512,8 @@ __export(engine_exports, {
383
512
  flushDeferredProjections: () => flushDeferredProjections,
384
513
  gitdirOf: () => gitdirOf,
385
514
  globalAggregates: () => globalAggregates,
515
+ historyPreflight: () => historyPreflight,
516
+ historyRestorePreflight: () => historyRestorePreflight,
386
517
  importCheckpoint: () => importCheckpoint,
387
518
  ingestPackAsCustody: () => ingestPackAsCustody,
388
519
  installPayload: () => installPayload,
@@ -405,9 +536,11 @@ __export(engine_exports, {
405
536
  packBornChildren: () => packBornChildren,
406
537
  packDelta: () => packDelta,
407
538
  parseAttestedReadNeed: () => parseAttestedReadNeed,
539
+ pendingConflictSummary: () => pendingConflictSummary,
408
540
  pendingConflicts: () => pendingConflicts,
409
541
  placementDirectives: () => placementDirectives,
410
542
  projectionStats: () => projectionStats,
543
+ promotePreflighted: () => promotePreflighted,
411
544
  pushCheckpoint: () => pushCheckpoint,
412
545
  pushShape: () => pushShape,
413
546
  pushStagedCheckpoint: () => pushStagedCheckpoint,
@@ -425,6 +558,7 @@ __export(engine_exports, {
425
558
  replicateIntents: () => replicateIntents,
426
559
  repoArgOf: () => repoArgOf,
427
560
  restoreCheckpointFromCustody: () => restoreCheckpointFromCustody,
561
+ restorePreviousPreflighted: () => restorePreviousPreflighted,
428
562
  scopedReads: () => scopedReads,
429
563
  sealAttestationBatches: () => sealAttestationBatches,
430
564
  sealHomes: () => sealHomes,
@@ -435,6 +569,7 @@ __export(engine_exports, {
435
569
  spatialBboxParams: () => spatialBboxParams,
436
570
  spatialWithin: () => spatialWithin,
437
571
  stageCheckpoint: () => stageCheckpoint,
572
+ stagePayload: () => stagePayload,
438
573
  stats: () => stats,
439
574
  sum: () => sum,
440
575
  summaryCloseAgainstHeld: () => summaryCloseAgainstHeld,
@@ -1139,7 +1274,7 @@ async function replicateIntents(eng, ws, ledger, intents, { push = true } = {})
1139
1274
  log("replicate-globals", { ws, replicated: replicated.length, skipped: skipped.length, refused: refused.length });
1140
1275
  return { replicated, skipped, refused };
1141
1276
  }
1142
- async function admitIntentOffers(eng, ws, ledger, offers, { receiptFence = null, shardRouting = null, clientBase = null, defer = false } = {}) {
1277
+ async function admitIntentOffers(eng, ws, ledger, offers, { receiptFence = null, shardRouting = null, clientBase = null } = {}) {
1143
1278
  const main2 = await mainIntentIds(eng, ws);
1144
1279
  const sessions = /* @__PURE__ */ new Map();
1145
1280
  const deadLetters = [];
@@ -1206,8 +1341,8 @@ async function admitIntentOffers(eng, ws, ledger, offers, { receiptFence = null,
1206
1341
  }
1207
1342
  const cap2 = summaryCtx ? beginSummaryCapture(eng, ws, summaryCtx, blob) : null;
1208
1343
  const _tOffer = Date.now();
1209
- const res = offerAdmit(eng, ws, blob, { defer: !summaryCtx });
1210
- log("offer-admit-ms", { ws, ms: Date.now() - _tOffer, deferred: !summaryCtx, ok: res.ok });
1344
+ const res = offerAdmit(eng, ws, blob, { defer: false });
1345
+ log("offer-admit-ms", { ws, ms: Date.now() - _tOffer, deferred: false, ok: res.ok });
1211
1346
  if (res.ok) {
1212
1347
  if (intentId) main2.ids.add(intentId);
1213
1348
  main2.oids.add(res.head);
@@ -1230,7 +1365,6 @@ async function admitIntentOffers(eng, ws, ledger, offers, { receiptFence = null,
1230
1365
  }
1231
1366
  timings.sessions = sessions.size;
1232
1367
  timings.intents = admittedTotal;
1233
- if (admittedTotal && !summaryCtx) (eng.pendingProjection ??= /* @__PURE__ */ new Set()).add(ws);
1234
1368
  if (deadLetters.length) {
1235
1369
  try {
1236
1370
  await materializeRefused(eng, ws, ledger, deadLetters);
@@ -1238,8 +1372,7 @@ async function admitIntentOffers(eng, ws, ledger, offers, { receiptFence = null,
1238
1372
  log("refused-materialize-failed", { ws, error: String(e).slice(0, 200) });
1239
1373
  }
1240
1374
  }
1241
- const deferDurability = defer && !summaryCtx;
1242
- if (admittedTotal && !deferDurability) {
1375
+ if (admittedTotal) {
1243
1376
  const sealDone = span("seal offered intents", "cseal", 1, { admitted: admittedTotal });
1244
1377
  await commitAndPush(eng, ws, ledger);
1245
1378
  sealDone();
@@ -1257,7 +1390,7 @@ async function admitIntentOffers(eng, ws, ledger, offers, { receiptFence = null,
1257
1390
  }
1258
1391
  log("admit-intent-offers", { ws, sessions: sessions.size, admitted: admittedTotal, rejected: [...sessions.values()].reduce((n, s) => n + s.rejected.length, 0), resultPack: resultPackB64 ? "inline" : "none" });
1259
1392
  const bornPacked = born.length ? packBornChildren(eng, born) : born;
1260
- return { sessions: [...sessions.values()].map((s) => ({ ...s, ...s.skipped.length ? {} : { skipped: void 0 } })), main: head, deadLetters, timings, summary: summary ?? null, born: bornPacked, birthOutcomes, ...resultPackB64 ? { resultPackB64, resultBase: clientBase } : {}, ...deferDurability && admittedTotal && head ? { deferred: true, head } : {} };
1393
+ return { sessions: [...sessions.values()].map((s) => ({ ...s, ...s.skipped.length ? {} : { skipped: void 0 } })), main: head, deadLetters, timings, summary: summary ?? null, born: bornPacked, birthOutcomes, ...resultPackB64 ? { resultPackB64, resultBase: clientBase } : {} };
1261
1394
  }
1262
1395
  async function checkpointStateHash(eng, ws) {
1263
1396
  const rows = query(eng, ws, "aggregatesByType", JSON.stringify({ __type: "NomosSummarySubtotal" }));
@@ -1415,6 +1548,9 @@ function mintId(eng, typeTag) {
1415
1548
  function author(eng, ws, domain, directiveId, payload, controllerHash, opts = {}) {
1416
1549
  const seq = eng.seq++;
1417
1550
  const capPorts = { clock: { physical: Date.now(), logical: 0, replica: eng.replica }, rng: osEntropyBuffer(opts.rngDraws ?? 64) };
1551
+ if (opts.__nomosConflictSnapshots !== void 0 && opts.__nomosConflictSnapshots.length > 0) {
1552
+ capPorts.conflict_snapshots = opts.__nomosConflictSnapshots;
1553
+ }
1418
1554
  if (opts.validFromEpoch !== void 0) capPorts.valid_from_epoch = opts.validFromEpoch;
1419
1555
  if (opts.principalVerified !== void 0) {
1420
1556
  capPorts.principal = opts.actor ?? "";
@@ -2214,6 +2350,120 @@ async function verifyChain(eng, ws, ledger) {
2214
2350
  function verifyChainLocal(eng, ws) {
2215
2351
  return JSON.parse(call(eng.ex, "query", { repoArg: repoArgOf(ws), workspace: ws, queryBytes: b64Json({ op: "verifyChain" }), branch: BRANCH }, eng.STDERR));
2216
2352
  }
2353
+ function historyPreflight(eng, ws, domain, candidateHash, atHash, dispositions) {
2354
+ return JSON.parse(call(eng.ex, "query", {
2355
+ repoArg: repoArgOf(ws),
2356
+ workspace: ws,
2357
+ branch: BRANCH,
2358
+ queryBytes: b64Json({
2359
+ op: "historyPreflight",
2360
+ domain,
2361
+ candidateHash,
2362
+ atHash,
2363
+ ...dispositions ? { dispositions } : {}
2364
+ })
2365
+ }, eng.STDERR));
2366
+ }
2367
+ function historyRestorePreflight(eng, ws, domain, currentHash, priorHash, atHash, actor) {
2368
+ return JSON.parse(call(eng.ex, "query", {
2369
+ repoArg: repoArgOf(ws),
2370
+ workspace: ws,
2371
+ branch: BRANCH,
2372
+ queryBytes: b64Json({
2373
+ op: "historyPreflight",
2374
+ domain,
2375
+ candidateHash: priorHash,
2376
+ atHash,
2377
+ restorePreviousOf: currentHash,
2378
+ ackBy: actor
2379
+ })
2380
+ }, eng.STDERR));
2381
+ }
2382
+ function promotePreflighted(eng, ws, domain, candidateHash, opts = {}) {
2383
+ const actingPrincipal = opts.actor?.trim() || String(eng.installedBy || "").trim();
2384
+ if (!actingPrincipal && !opts.authToken) {
2385
+ return {
2386
+ ok: false,
2387
+ stage: "identity",
2388
+ error: "promotion needs the stateful client's current principal so its business decision is attributable"
2389
+ };
2390
+ }
2391
+ const atHash = kgit.resolveRef(eng, gitdirOf(ws), BRANCH);
2392
+ if (!atHash) {
2393
+ return { ok: false, stage: "preflight", error: "workspace has no history to prove" };
2394
+ }
2395
+ let report;
2396
+ try {
2397
+ report = historyPreflight(eng, ws, domain, candidateHash, atHash, opts.dispositions);
2398
+ } catch (error) {
2399
+ return { ok: false, stage: "preflight", error: String(error?.message || error) };
2400
+ }
2401
+ const prepared = prepareManagedPromotion(report, candidateHash, domain, opts);
2402
+ if (!prepared.ok) return prepared;
2403
+ const {
2404
+ replaces: _replaces,
2405
+ retireReplaced: _retireReplaced,
2406
+ dispositions: _dispositions,
2407
+ acceptanceReason: _acceptance,
2408
+ ...authorOpts
2409
+ } = opts;
2410
+ const result = author(eng, ws, "nomos", "promoteDomain", prepared.payload, eng.hashes.nomos, {
2411
+ ...authorOpts,
2412
+ ...actingPrincipal ? { actor: actingPrincipal } : {}
2413
+ });
2414
+ return result.ok ? { ...result, stage: "promoted", report, receiptId: prepared.receiptId } : { ...result, stage: "admission", report, receiptId: prepared.receiptId };
2415
+ }
2416
+ function restorePreviousPreflighted(eng, ws, domain, currentHash, opts = {}) {
2417
+ const actingPrincipal = opts.actor?.trim() || String(eng.installedBy || "").trim();
2418
+ if (!actingPrincipal) {
2419
+ return {
2420
+ ok: false,
2421
+ stage: "identity",
2422
+ error: "model restoration needs the stateful client's current principal so its business decision is attributable"
2423
+ };
2424
+ }
2425
+ const resolved = currentLaw(eng, ws)[domain];
2426
+ if (resolved !== currentHash) {
2427
+ return {
2428
+ ok: false,
2429
+ stage: "preflight",
2430
+ error: "the generated model is no longer current; nothing changed [nomos.rollout.restore-current-changed]"
2431
+ };
2432
+ }
2433
+ const current = qById(eng, ws, `domain-installation:${currentHash}`)[0]?.data;
2434
+ const priorHash = typeof current?.["spec.supersedes"] === "string" ? current["spec.supersedes"] : "";
2435
+ if (!priorHash) {
2436
+ return {
2437
+ ok: false,
2438
+ stage: "preflight",
2439
+ error: "this model has no previous model to restore [nomos.rollout.restore-no-previous]"
2440
+ };
2441
+ }
2442
+ const atHash = kgit.resolveRef(eng, gitdirOf(ws), BRANCH);
2443
+ if (!atHash) return { ok: false, stage: "preflight", error: "workspace has no history to prove" };
2444
+ let report;
2445
+ try {
2446
+ report = historyRestorePreflight(
2447
+ eng,
2448
+ ws,
2449
+ domain,
2450
+ currentHash,
2451
+ priorHash,
2452
+ atHash,
2453
+ actingPrincipal
2454
+ );
2455
+ } catch (error) {
2456
+ return { ok: false, stage: "preflight", error: String(error?.message || error) };
2457
+ }
2458
+ const prepared = prepareManagedRestoration(report, currentHash, priorHash, domain, opts);
2459
+ if (!prepared.ok) return prepared;
2460
+ const { acceptanceReason: _acceptance, ...authorOpts } = opts;
2461
+ const result = author(eng, ws, "nomos", "rollbackDomain", prepared.payload, eng.hashes.nomos, {
2462
+ ...authorOpts,
2463
+ ...actingPrincipal ? { actor: actingPrincipal } : {}
2464
+ });
2465
+ return result.ok ? { ...result, stage: "restored", report, receiptId: prepared.receiptId } : { ...result, stage: "admission", report, receiptId: prepared.receiptId };
2466
+ }
2217
2467
  async function rebirthCompact(eng, ws, ledger, { reason = "", supersededAt = 0, parent = "" } = {}) {
2218
2468
  const incWs = `${ws}__rebirth`;
2219
2469
  try {
@@ -2526,26 +2776,89 @@ async function boot(eng, ws, ledger) {
2526
2776
  );
2527
2777
  }
2528
2778
  async function deploy(eng, ws, ledger, usda, domainHash, dispositions, opts = {}) {
2529
- const result = author(eng, ws, "nomos", "installDomain", installPayload(domainHash, usda, eng.installedBy, dispositions), eng.hashes.nomos, { actor: opts.actor ?? "", ...opts.authToken ? { authToken: opts.authToken } : {} });
2530
- if (result.ok === false) {
2779
+ const existing = qById(eng, ws, `domain-installation:${domainHash}`);
2780
+ if (existing.length > 0) {
2781
+ const phase = String(existing[0]?.data?.["status.phase"] ?? "");
2782
+ return {
2783
+ head: kgit.resolveRef(eng, gitdirOf(ws), BRANCH),
2784
+ installation: existing,
2785
+ staged: phase === "Pending",
2786
+ alreadyInstalled: true
2787
+ };
2788
+ }
2789
+ const before = currentLaw(eng, ws);
2790
+ const tenantHashes = [...new Set(Object.values(before).filter(
2791
+ (hash) => typeof hash === "string" && hash !== eng.hashes.bootstrap && hash !== eng.hashes.nomos
2792
+ ))];
2793
+ const upgrade = tenantHashes.length > 0;
2794
+ let supersedes = opts.supersedes?.trim() || void 0;
2795
+ if (supersedes !== void 0 && !tenantHashes.includes(supersedes)) {
2531
2796
  throw new Error(
2532
- `deploy-refused: installing ${domainHash.slice(0, 12)}\u2026 was REFUSED by the one gate (custody unchanged): ${result.error ?? "unknown refusal"}`
2797
+ `deploy-predecessor-changed: the client named ${supersedes.slice(0, 12)}\u2026 as the current model, but this workspace no longer serves it. Refresh and deploy again; nothing was committed.`
2533
2798
  );
2534
2799
  }
2535
- const installation = qById(eng, ws, `domain-installation:${domainHash}`);
2536
- const keys = installation[0]?.data?.domainKeys ?? installation[0]?.domainKeys ?? [];
2537
- if (keys.length > 0) {
2538
- const law = installedLaw(eng, ws);
2539
- const currentLaw2 = law?.currentLaw ?? {};
2540
- const stale = keys.filter((k) => currentLaw2[k] !== void 0 && currentLaw2[k] !== domainHash);
2541
- if (stale.length > 0) {
2800
+ if (upgrade && supersedes === void 0) {
2801
+ if (tenantHashes.length === 1) supersedes = tenantHashes[0];
2802
+ else {
2542
2803
  throw new Error(
2543
- `deploy-did-not-advance: the install of ${domainHash.slice(0, 12)}\u2026 ADMITTED but the workspace's current law still serves ${stale.map((k) => `'${k}' from ${String(currentLaw2[k]).slice(0, 12)}\u2026`).join(", ")} \u2014 refusing (custody unchanged) instead of a silent no-op. This usually means the existing installation's era/lineage prevents supersession: run \`githolon deploy --dry-run=evolve\` for the gate's verdict, supply #58 dispositions for a true removal/retype, or re-forge the workspace onto the current lineage.`
2804
+ `deploy-predecessor-ambiguous: this workspace currently serves ${tenantHashes.length} business-model packages. Nomos will not guess which one this candidate follows; use the generated deploy client, which binds the candidate to the current model before staging. Nothing was committed.`
2544
2805
  );
2545
2806
  }
2546
2807
  }
2547
- await commitAndPush(eng, ws, ledger);
2548
- return { head: result.head, installation };
2808
+ if (upgrade && dispositions && Object.keys(dispositions).length > 0) {
2809
+ throw new Error(
2810
+ "deploy-manual-evolution-refused: successor deployment no longer accepts caller-chosen schema dispositions. Schema evolution belongs to the candidate model's adoption proof; nothing was committed."
2811
+ );
2812
+ }
2813
+ const directiveId = upgrade ? "stageDomain" : "installDomain";
2814
+ const payload = upgrade ? stagePayload(domainHash, usda, eng.installedBy, supersedes) : installPayload(domainHash, usda, eng.installedBy, dispositions);
2815
+ const result = author(eng, ws, "nomos", directiveId, payload, eng.hashes.nomos, {
2816
+ actor: opts.actor ?? "",
2817
+ ...opts.authToken ? { authToken: opts.authToken } : {}
2818
+ });
2819
+ if (result.ok === false) {
2820
+ unmount(eng, ws);
2821
+ throw new Error(
2822
+ `deploy-refused: ${upgrade ? "staging" : "installing"} ${domainHash.slice(0, 12)}\u2026 was REFUSED by the one gate (custody unchanged): ${result.error ?? "unknown refusal"}`
2823
+ );
2824
+ }
2825
+ try {
2826
+ const installation = qById(eng, ws, `domain-installation:${domainHash}`);
2827
+ if (upgrade) {
2828
+ const phase = String(installation[0]?.data?.["status.phase"] ?? "");
2829
+ if (phase !== "Pending") {
2830
+ throw new Error(
2831
+ `deploy-did-not-stage: candidate ${domainHash.slice(0, 12)}\u2026 admitted but its lifecycle phase is '${phase || "missing"}', not Pending. Custody was not pushed.`
2832
+ );
2833
+ }
2834
+ const after = currentLaw(eng, ws);
2835
+ const moved = Object.entries(before).filter(([key, hash]) => after[key] !== hash);
2836
+ if (moved.length > 0 || Object.values(after).includes(domainHash)) {
2837
+ throw new Error(
2838
+ `deploy-activated-candidate: staging ${domainHash.slice(0, 12)}\u2026 changed served law (${moved.map(([key]) => key).join(", ") || "candidate became current"}). Nomos refuses to persist an upgrade that bypassed model.adopt().`
2839
+ );
2840
+ }
2841
+ await commitAndPush(eng, ws, ledger);
2842
+ return { head: result.head, installation, staged: true, supersedes };
2843
+ }
2844
+ const law = installedLaw(eng, ws);
2845
+ const installed = (law?.domains ?? []).find((row) => row?.domainHash === domainHash);
2846
+ const keys = installed?.domainKeys ?? installation[0]?.data?.domainKeys ?? installation[0]?.domainKeys ?? [];
2847
+ if (keys.length > 0) {
2848
+ const currentLaw2 = law?.currentLaw ?? {};
2849
+ const stale = keys.filter((k) => currentLaw2[k] !== domainHash);
2850
+ if (stale.length > 0) {
2851
+ throw new Error(
2852
+ `deploy-did-not-advance: the install of ${domainHash.slice(0, 12)}\u2026 ADMITTED but the workspace's current law still serves ${stale.map((k) => `'${k}' from ${String(currentLaw2[k]).slice(0, 12)}\u2026`).join(", ")} \u2014 refusing (custody unchanged) instead of a silent no-op. This usually means the existing installation's era/lineage prevents supersession: run \`githolon deploy --dry-run=evolve\` for the gate's verdict, supply #58 dispositions for a true removal/retype, or re-forge the workspace onto the current lineage.`
2853
+ );
2854
+ }
2855
+ }
2856
+ await commitAndPush(eng, ws, ledger);
2857
+ return { head: result.head, installation, staged: false };
2858
+ } catch (error) {
2859
+ unmount(eng, ws);
2860
+ throw error;
2861
+ }
2549
2862
  }
2550
2863
  function snapshot(eng, ws) {
2551
2864
  const wsDir = dirOf(eng.preopen.dir.contents.get("ws")).contents.get(ws);
@@ -2645,13 +2958,14 @@ function sealAttestationBatches(eng, ws, domain, controllerHash, opts = {}) {
2645
2958
  }
2646
2959
  return outcomes;
2647
2960
  }
2648
- var enc3, dec3, BRANCH, RESULT_PACK_INLINE_MAX, dirOf, repoArgOf, gitdirOf, unpack, log, repoArgOfGitdir, bytesFromB64, kgit, installPayload, routeTagHexOfHomeKey, SUMMARY_SEP, summaryKey, splitSummaryKey, NOMOS_SHARD_ROWS_READ, groupOf, tallyOf, isExtremumRead, extremumMonotone, CKPT_REF, pushVia, fullRef, applyPackInto, custodyLsRefs, qById, query, check, expand, cryptoSealField, cryptoKeygen, authorKeygen, eraCertSign, delegationCertSign, attestationSign, attestedRead, certSign, cryptoWrapKey, cryptoUnwrapKey, readPosture, count, sum, pendingConflicts, spatialBboxParams, spatialWithin, extremum, nomosActive, refusedRefName, outboxRefName, GENESIS_WARRANT_ID, isWarranted, projectionStats;
2961
+ var enc3, dec3, BRANCH, RESULT_PACK_INLINE_MAX, dirOf, repoArgOf, gitdirOf, unpack, log, repoArgOfGitdir, bytesFromB64, kgit, installPayload, stagePayload, routeTagHexOfHomeKey, SUMMARY_SEP, summaryKey, splitSummaryKey, NOMOS_SHARD_ROWS_READ, groupOf, tallyOf, isExtremumRead, extremumMonotone, CKPT_REF, pushVia, fullRef, applyPackInto, custodyLsRefs, qById, query, check, expand, cryptoSealField, cryptoOwnedScopeKey, cryptoOwnedDeviceRequest, cryptoOwnedPrepareApproval, cryptoOwnedPrepareRecovery, cryptoKeygen, authorKeygen, eraCertSign, delegationCertSign, attestationSign, attestedRead, certSign, cryptoWrapKey, cryptoUnwrapKey, readPosture, count, sum, pendingConflicts, pendingConflictSummary, spatialBboxParams, spatialWithin, extremum, nomosActive, refusedRefName, outboxRefName, GENESIS_WARRANT_ID, isWarranted, projectionStats;
2649
2962
  var init_engine = __esm({
2650
2963
  "vendor/engine/engine.ts"() {
2651
2964
  "use strict";
2652
2965
  init_git_fs();
2653
2966
  init_tree();
2654
2967
  init_custody_transport();
2968
+ init_managed_promotion();
2655
2969
  enc3 = new TextEncoder();
2656
2970
  dec3 = new TextDecoder();
2657
2971
  BRANCH = "main";
@@ -2697,6 +3011,14 @@ var init_engine = __esm({
2697
3011
  }, eng.STDERR)).oid
2698
3012
  };
2699
3013
  installPayload = (hash, usda, installedBy, dispositions) => ({ domainHash: hash, packageUsda: usda, installedBy, dependencies: [], finalizers: [], ...dispositions ? { dispositions } : {} });
3014
+ stagePayload = (hash, usda, installedBy, supersedes) => ({
3015
+ domainHash: hash,
3016
+ packageUsda: usda,
3017
+ installedBy,
3018
+ dependencies: [],
3019
+ ...supersedes ? { supersedes } : {},
3020
+ finalizers: []
3021
+ });
2700
3022
  routeTagHexOfHomeKey = async (homeKey) => (await sha256hex(`nomos-route:${homeKey}`)).slice(0, 12);
2701
3023
  SUMMARY_SEP = "";
2702
3024
  summaryKey = (readId, group) => `${readId}${SUMMARY_SEP}${group}`;
@@ -2727,6 +3049,105 @@ var init_engine = __esm({
2727
3049
  check = (eng, ws, object, relation, subject) => JSON.parse(call(eng.ex, "query", { repoArg: repoArgOf(ws), workspace: ws, queryBytes: b64Json({ op: "check", object, relation, subject }), branch: BRANCH }, eng.STDERR));
2728
3050
  expand = (eng, ws, object, relation) => JSON.parse(call(eng.ex, "query", { repoArg: repoArgOf(ws), workspace: ws, queryBytes: b64Json({ op: "expand", object, relation }), branch: BRANCH }, eng.STDERR));
2729
3051
  cryptoSealField = (eng, { scopeKey, scope, epoch, aggSid, fieldSid, plaintext }) => JSON.parse(call(eng.ex, "query", { queryBytes: b64Json({ op: "cryptoSealField", scopeKey, scope, epoch, aggSid, fieldSid, plaintext }) }, eng.STDERR)).envelope;
3052
+ cryptoOwnedScopeKey = (eng, workspace, authorSecret, scope, principal = "", requireSeat = true, record, recordEpoch = 0, recordWrap) => {
3053
+ const recordObject = record === void 0 ? void 0 : `${record.type}:${record.id}`;
3054
+ const derived = JSON.parse(call(eng.ex, "query", {
3055
+ queryBytes: b64Json({
3056
+ op: "cryptoOwnedScopeKey",
3057
+ workspace,
3058
+ principal,
3059
+ authorSecret,
3060
+ scope,
3061
+ ...recordObject === void 0 ? {} : { recordObject, recordEpoch },
3062
+ ...recordWrap === void 0 ? {} : { recordWrap }
3063
+ })
3064
+ }, eng.STDERR));
3065
+ const ownedKeySubject = String(derived.ownedKeySubject || "");
3066
+ if (!requireSeat) {
3067
+ return {
3068
+ scope: String(derived.scope || scope),
3069
+ epoch: Number(derived.epoch) || 0,
3070
+ key: String(derived.scopeKey || ""),
3071
+ ownedKeySubject,
3072
+ ownedCustody: typeof derived.ownedCustody === "string" ? derived.ownedCustody : void 0,
3073
+ keyCommitment: typeof derived.keyCommitment === "string" ? derived.keyCommitment : void 0,
3074
+ principalWrappingPublic: typeof derived.principalWrappingPublic === "string" ? derived.principalWrappingPublic : void 0
3075
+ };
3076
+ }
3077
+ if (principal) {
3078
+ let hex = "";
3079
+ for (const byte of enc3.encode(principal)) hex += byte.toString(16).padStart(2, "0");
3080
+ const object = `nomos-owned-principal:${hex || "00"}`;
3081
+ const rows = qById(eng, workspace, `reltuple-${object}-nomos_internal_owned_device_key_7c1f5e`);
3082
+ const row = Array.isArray(rows) ? rows[0] : rows?.rows?.[0];
3083
+ const leaves = row?.data?.subjects ?? row?.subjects ?? {};
3084
+ const leaf = (raw) => {
3085
+ if (raw && typeof raw === "object") return raw;
3086
+ try {
3087
+ return typeof raw === "string" ? JSON.parse(raw) : {};
3088
+ } catch {
3089
+ return {};
3090
+ }
3091
+ };
3092
+ const active = Object.entries(leaves).filter(([, raw]) => leaf(raw).status === "active").map(([subject]) => subject);
3093
+ if (active.length === 0 && Object.keys(leaves).length === 0) {
3094
+ return {
3095
+ scope: String(derived.scope || scope),
3096
+ epoch: Number(derived.epoch) || 0,
3097
+ key: String(derived.scopeKey || ""),
3098
+ ownedKeySubject,
3099
+ ownedCustody: typeof derived.ownedCustody === "string" ? derived.ownedCustody : void 0,
3100
+ keyCommitment: typeof derived.keyCommitment === "string" ? derived.keyCommitment : void 0,
3101
+ principalWrappingPublic: typeof derived.principalWrappingPublic === "string" ? derived.principalWrappingPublic : void 0
3102
+ };
3103
+ }
3104
+ if (!active.includes(ownedKeySubject)) {
3105
+ throw new Error(
3106
+ "this device is waiting for approval \u2014 approve its opaque request on an already-working device"
3107
+ );
3108
+ }
3109
+ const custody = String(leaf(leaves[ownedKeySubject]).ownedCustody || "");
3110
+ if (!custody) throw new Error("the approved device has no owned-data handoff");
3111
+ const opened = JSON.parse(call(eng.ex, "query", {
3112
+ queryBytes: b64Json({
3113
+ op: "cryptoOwnedScopeKey",
3114
+ workspace,
3115
+ principal,
3116
+ authorSecret,
3117
+ scope,
3118
+ ownedCustody: custody,
3119
+ ...recordObject === void 0 ? {} : { recordObject, recordEpoch },
3120
+ ...recordWrap === void 0 ? {} : { recordWrap }
3121
+ })
3122
+ }, eng.STDERR));
3123
+ return {
3124
+ scope: String(opened.scope || scope),
3125
+ epoch: Number(opened.epoch) || 0,
3126
+ key: String(opened.scopeKey || ""),
3127
+ ownedKeySubject,
3128
+ keyCommitment: typeof opened.keyCommitment === "string" ? opened.keyCommitment : void 0,
3129
+ principalWrappingPublic: typeof opened.principalWrappingPublic === "string" ? opened.principalWrappingPublic : void 0
3130
+ };
3131
+ }
3132
+ return {
3133
+ scope: String(derived.scope || scope),
3134
+ epoch: Number(derived.epoch) || 0,
3135
+ key: String(derived.scopeKey || ""),
3136
+ ownedKeySubject,
3137
+ ownedCustody: typeof derived.ownedCustody === "string" ? derived.ownedCustody : void 0,
3138
+ keyCommitment: typeof derived.keyCommitment === "string" ? derived.keyCommitment : void 0,
3139
+ principalWrappingPublic: typeof derived.principalWrappingPublic === "string" ? derived.principalWrappingPublic : void 0
3140
+ };
3141
+ };
3142
+ cryptoOwnedDeviceRequest = (eng, workspace, principal, authorSecret, scope) => String(JSON.parse(call(eng.ex, "query", {
3143
+ queryBytes: b64Json({ op: "cryptoOwnedDeviceRequest", workspace, principal, authorSecret, scope })
3144
+ }, eng.STDERR)).request || "");
3145
+ cryptoOwnedPrepareApproval = (eng, args) => JSON.parse(call(eng.ex, "query", {
3146
+ queryBytes: b64Json({ op: "cryptoOwnedPrepareApproval", ...args })
3147
+ }, eng.STDERR));
3148
+ cryptoOwnedPrepareRecovery = (eng, args) => JSON.parse(call(eng.ex, "query", {
3149
+ queryBytes: b64Json({ op: "cryptoOwnedPrepareRecovery", ...args })
3150
+ }, eng.STDERR));
2730
3151
  cryptoKeygen = (eng) => JSON.parse(call(eng.ex, "query", { queryBytes: b64Json({ op: "cryptoKeygen" }) }, eng.STDERR));
2731
3152
  authorKeygen = (eng) => JSON.parse(call(eng.ex, "query", { queryBytes: b64Json({ op: "authorKeygen" }) }, eng.STDERR));
2732
3153
  eraCertSign = (eng, args) => JSON.parse(call(eng.ex, "query", { queryBytes: b64Json({ op: "eraCertSign", ...args }) }, eng.STDERR));
@@ -2774,7 +3195,8 @@ var init_engine = __esm({
2774
3195
  readPosture = (eng, ws, principal = "", authToken = "", nowUnix = 0) => JSON.parse(call(eng.ex, "query", { repoArg: repoArgOf(ws), workspace: ws, queryBytes: b64Json({ op: "readPosture", authToken, nowUnix }), principal, branch: BRANCH }, eng.STDERR));
2775
3196
  count = (eng, ws, countId, groupKey, principal = "") => JSON.parse(call(eng.ex, "query", { repoArg: repoArgOf(ws), workspace: ws, queryBytes: b64Json({ op: "count", countId, groupKey }), principal, branch: BRANCH }, eng.STDERR));
2776
3197
  sum = (eng, ws, sumId, groupKey, principal = "") => JSON.parse(call(eng.ex, "query", { repoArg: repoArgOf(ws), workspace: ws, queryBytes: b64Json({ op: "sum", sumId, groupKey }), principal, branch: BRANCH }, eng.STDERR));
2777
- pendingConflicts = (eng, ws, principal = "") => JSON.parse(call(eng.ex, "query", { repoArg: repoArgOf(ws), workspace: ws, queryBytes: b64Json({ op: "pendingConflicts" }), principal, branch: BRANCH }, eng.STDERR));
3198
+ pendingConflicts = (eng, ws, principal = "", aggregateType = "") => JSON.parse(call(eng.ex, "query", { repoArg: repoArgOf(ws), workspace: ws, queryBytes: b64Json({ op: "pendingConflicts", aggregateType }), principal, branch: BRANCH }, eng.STDERR));
3199
+ pendingConflictSummary = (eng, ws, principal = "") => JSON.parse(call(eng.ex, "query", { repoArg: repoArgOf(ws), workspace: ws, queryBytes: b64Json({ op: "pendingConflictSummary" }), principal, branch: BRANCH }, eng.STDERR));
2778
3200
  spatialBboxParams = (bbox) => {
2779
3201
  const minX = bbox.minX ?? bbox.minLng, minY = bbox.minY ?? bbox.minLat, maxX = bbox.maxX ?? bbox.maxLng, maxY = bbox.maxY ?? bbox.maxLat;
2780
3202
  return { minX, minY, maxX, maxY, minLng: minX, minLat: minY, maxLng: maxX, maxLat: maxY };
@@ -2820,15 +3242,28 @@ async function fetchJsonCached(url, cacheFile) {
2820
3242
  }
2821
3243
  }
2822
3244
  async function fetchRuntime(cloud, kind = "production") {
2823
- const cache = join2(configDir(), "runtime");
3245
+ const cache = process.env["NOMOS_OFFLINE_RUNTIME"] || join2(configDir(), "runtime");
2824
3246
  let wasmBytes;
2825
3247
  const wasmName = kind === "test-mint" ? "holon-testmint.wasm" : "holon.wasm";
2826
3248
  const wasmUrl = kind === "test-mint" ? "holon-testmint.wasm" : "holon.wasm";
2827
3249
  const wasmCache = join2(cache, wasmName);
3250
+ const packagesCache = join2(cache, "packages.json");
3251
+ const refresh = String(process.env["NOMOS_RUNTIME_REFRESH"] || "auto").toLowerCase();
3252
+ if (refresh === "never") {
3253
+ if (!existsSync2(wasmCache) || !existsSync2(packagesCache)) {
3254
+ throw new Error(
3255
+ `offline runtime cache missing at ${cache} (${wasmName} + packages.json); build or warm that exact runtime before running with NOMOS_RUNTIME_REFRESH=never`
3256
+ );
3257
+ }
3258
+ return {
3259
+ wasmBytes: readFileSync(wasmCache),
3260
+ packages: JSON.parse(readFileSync(packagesCache, "utf8"))
3261
+ };
3262
+ }
2828
3263
  const localWasm = process.env["NOMOS_LOCAL_WASM"];
2829
3264
  if (localWasm) {
2830
3265
  const bytes = readFileSync(localWasm);
2831
- const packages2 = await fetchJsonCached(`${cloud}/v1/runtime/packages`, join2(cache, "packages.json"));
3266
+ const packages2 = await fetchJsonCached(`${cloud}/v1/runtime/packages`, packagesCache);
2832
3267
  return { wasmBytes: bytes, packages: packages2 };
2833
3268
  }
2834
3269
  try {
@@ -2841,7 +3276,7 @@ async function fetchRuntime(cloud, kind = "production") {
2841
3276
  if (!existsSync2(wasmCache)) throw e;
2842
3277
  wasmBytes = readFileSync(wasmCache);
2843
3278
  }
2844
- const packages = await fetchJsonCached(`${cloud}/v1/runtime/packages`, join2(cache, "packages.json"));
3279
+ const packages = await fetchJsonCached(`${cloud}/v1/runtime/packages`, packagesCache);
2845
3280
  return { wasmBytes, packages };
2846
3281
  }
2847
3282
  function atomicWriteRuntime(target, data) {
@@ -4980,14 +5415,14 @@ function negotiateDeployRoute(caps, opts) {
4980
5415
  if (opts.signed) {
4981
5416
  if (caps.offerLane === false) {
4982
5417
  return {
4983
- fail: `${caps.cloud} does not serve the SIGNED OFFER lane \u2014 POST /v2/workspaces/${opts.ws}/${opts.route.domain}/${opts.route.directiveId} is not routed (the workspace serves no self-describing OpenAPI, so this cloud has no gateway to route a signed offer). This is often a lightweight cloud (e.g. \`githolon cloud dev\`) without the offer gateway.
4984
- \u2192 deploy the OPEN (unsigned) lane instead (drop --as/--principal) if this workspace is unwarranted, or target a cloud that serves the offer gateway.`
5418
+ fail: `${caps.cloud} cannot accept an authenticated deployment for this workspace \u2014 POST /v2/workspaces/${opts.ws}/${opts.route.domain}/${opts.route.directiveId} is not routed because the workspace serves no self-describing gateway.
5419
+ \u2192 target a current Nomos cloud that supports attributed deployment.`
4985
5420
  };
4986
5421
  }
4987
5422
  if (caps.offerLane === true && caps.routesTarget === false) {
4988
5423
  return {
4989
- fail: `${caps.cloud} serves the offer gateway for ${opts.ws}, but its OpenAPI does not name the route ${opts.route.domain}/${opts.route.directiveId} \u2014 the ${opts.route.domain} controller is not installed here, so a signed ${opts.route.directiveId} would 404.
4990
- \u2192 confirm the workspace is born and the controller is deployed, or deploy the OPEN lane (drop --as).`
5424
+ fail: `${caps.cloud} serves the offer gateway for ${opts.ws}, but its OpenAPI does not name the route ${opts.route.domain}/${opts.route.directiveId} \u2014 the ${opts.route.domain} controller is not installed here, so the lifecycle request would 404.
5425
+ \u2192 confirm the workspace is born and its current Nomos controller is installed.`
4991
5426
  };
4992
5427
  }
4993
5428
  if (opts.sealedSizeChars > INLINE_OFFER_CAP_CHARS && caps.stage === false) {
@@ -5065,7 +5500,7 @@ function domainKeysOf(body) {
5065
5500
  function packageHashOf(packageUsda) {
5066
5501
  return createHash2("sha256").update(Buffer.from(packageUsda, "utf8")).digest("hex");
5067
5502
  }
5068
- function resolveDeployPlan(domainKeys, currentLaw2, targetHash, opts = {}) {
5503
+ function resolveDeployPlan(domainKeys, currentLaw2, targetHash) {
5069
5504
  const decisions = [];
5070
5505
  const priors = /* @__PURE__ */ new Set();
5071
5506
  for (const key of domainKeys) {
@@ -5074,21 +5509,18 @@ function resolveDeployPlan(domainKeys, currentLaw2, targetHash, opts = {}) {
5074
5509
  decisions.push({ key, action: "first" });
5075
5510
  } else if (currentHash === targetHash) {
5076
5511
  decisions.push({ key, currentHash, action: "idempotent" });
5077
- } else if (opts.keepBeside === true) {
5078
- decisions.push({ key, currentHash, action: "keep-beside" });
5079
5512
  } else {
5080
- decisions.push({ key, currentHash, action: "supersedes" });
5513
+ decisions.push({ key, currentHash, action: "successor" });
5081
5514
  priors.add(currentHash);
5082
5515
  }
5083
5516
  }
5084
- const replaceCandidates = [...priors];
5517
+ const predecessorCandidates = [...priors];
5085
5518
  const idempotent = domainKeys.length > 0 && decisions.every((d) => d.action === "idempotent");
5086
5519
  return {
5087
5520
  decisions,
5088
- ...replaceCandidates.length > 0 ? { replaces: replaceCandidates[0] } : {},
5089
- retireReplaced: opts.retireReplaced === true,
5521
+ ...predecessorCandidates.length > 0 ? { supersedes: predecessorCandidates[0] } : {},
5090
5522
  idempotent,
5091
- replaceCandidates
5523
+ predecessorCandidates
5092
5524
  };
5093
5525
  }
5094
5526
  function describeDeployPlan(plan, targetHash) {
@@ -5097,10 +5529,8 @@ function describeDeployPlan(plan, targetHash) {
5097
5529
  switch (d.action) {
5098
5530
  case "first":
5099
5531
  return ` ${d.key}: ${t} installed (first)`;
5100
- case "supersedes":
5101
- return ` ${d.key}: ${t} supersedes ${short(d.currentHash)}${plan.retireReplaced ? " (retired)" : ""}`;
5102
- case "keep-beside":
5103
- return ` ${d.key}: ${t} kept beside ${short(d.currentHash)}`;
5532
+ case "successor":
5533
+ return ` ${d.key}: ${t} is the candidate after ${short(d.currentHash)}`;
5104
5534
  case "idempotent":
5105
5535
  return ` ${d.key}: unchanged \u2014 ${t} is already active`;
5106
5536
  }
@@ -5571,7 +6001,7 @@ async function wsCreate(name, opts) {
5571
6001
  return 1;
5572
6002
  }
5573
6003
  out4(`\u2713 createWorkspace '${name}' requested via signed governance offer to ${opts.via} on ${cloud}`);
5574
- out4(` the worker provisions it on first touch; deploy is open (kernel-gated)`);
6004
+ out4(` the worker provisions it on first touch; then run githolon deploy ${name}`);
5575
6005
  out4(` (want the direct birth instead? drop --via: githolon ws create ${name})`);
5576
6006
  return 0;
5577
6007
  }
@@ -5614,7 +6044,7 @@ async function wsCreate(name, opts) {
5614
6044
  }
5615
6045
  if (opts.principal !== void 0) setPrincipal(opts.principal);
5616
6046
  out4(`\u2713 workspace ${name} created on ${cloud}`);
5617
- out4(` deploy is open (kernel-gated): githolon deploy ${name}`);
6047
+ out4(` deploy its first model: githolon deploy ${name}`);
5618
6048
  return 0;
5619
6049
  }
5620
6050
  async function wsStatus(name, opts) {
@@ -5816,14 +6246,18 @@ async function deploy2(ws, opts) {
5816
6246
  try {
5817
6247
  const body = readDeployBody3(file);
5818
6248
  targetHash = packageHashOf2(body.packageUsda);
5819
- plan = resolveDeployPlan2(domainKeysOf2(body), currentLaw2, targetHash, {
5820
- keepBeside: opts.keepBeside === true,
5821
- retireReplaced: opts.retireReplaced === true
5822
- });
5823
- } catch {
5824
- plan = void 0;
5825
- targetHash = void 0;
6249
+ plan = resolveDeployPlan2(domainKeysOf2(body), currentLaw2, targetHash);
6250
+ } catch (error) {
6251
+ err4(`deploy '${ws}' needs the current generated deploy body: ${error.message}`);
6252
+ return 1;
6253
+ }
6254
+ if ((plan?.predecessorCandidates.length ?? 0) > 1) {
6255
+ err4(
6256
+ `deploy '${ws}' refused: this candidate spans ${plan.predecessorCandidates.length} different current models; Nomos will not guess one predecessor. Compile one coherent workspace model and try again.`
6257
+ );
6258
+ return 1;
5826
6259
  }
6260
+ let expectsPending = plan !== void 0 && plan.predecessorCandidates.length === 1 && !plan.idempotent;
5827
6261
  const principal = opts.principal ?? activePrincipal();
5828
6262
  const signed = opts.principal !== void 0 || principal !== void 0 && getDeviceKey(principal) !== void 0;
5829
6263
  let phaseSuffix = "";
@@ -5842,8 +6276,14 @@ async function deploy2(ws, opts) {
5842
6276
  return 1;
5843
6277
  }
5844
6278
  const targetHash2 = createHash3("sha256").update(Buffer.from(packageUsda, "utf8")).digest("hex");
6279
+ if (expectsPending && body.dispositions !== void 0 && body.dispositions !== null) {
6280
+ err4(
6281
+ `deploy '${ws}' refused: successor models cannot carry caller-chosen schema dispositions; evolution must be proved by model.adopt(), not smuggled into deployment`
6282
+ );
6283
+ return 1;
6284
+ }
5845
6285
  const { probeCloudCapabilities: probeCloudCapabilities2, negotiateDeployRoute: negotiateDeployRoute2 } = await Promise.resolve().then(() => (init_capabilities(), capabilities_exports));
5846
- const route = { domain: "nomos", directiveId: "installDomain" };
6286
+ const route = { domain: "nomos", directiveId: expectsPending ? "stageDomain" : "installDomain" };
5847
6287
  const caps = await probeCloudCapabilities2(cloud, ws, route);
5848
6288
  const refusal = negotiateDeployRoute2(caps, { signed: true, ws, sealedSizeChars: packageUsda.length, route });
5849
6289
  if (refusal !== void 0) {
@@ -5855,17 +6295,16 @@ async function deploy2(ws, opts) {
5855
6295
  let v;
5856
6296
  try {
5857
6297
  const who = resolveGovernancePrincipal2(opts);
5858
- const installPayload2 = {
6298
+ const lifecyclePayload = {
5859
6299
  domainHash: targetHash2,
5860
6300
  packageUsda,
5861
6301
  installedBy: `user:${who}`,
5862
6302
  dependencies: [],
5863
6303
  finalizers: [],
5864
- // REPLACE-CURRENT: supersede the prior install per key (unless --keep-beside / first-install / idempotent).
5865
- ...plan?.replaces !== void 0 ? { replaces: plan.replaces, retireReplaced: plan.retireReplaced } : {},
5866
- ...body.dispositions !== void 0 && body.dispositions !== null ? { dispositions: body.dispositions } : {}
6304
+ ...expectsPending && plan?.supersedes !== void 0 ? { supersedes: plan.supersedes } : {},
6305
+ ...!expectsPending && body.dispositions !== void 0 && body.dispositions !== null ? { dispositions: body.dispositions } : {}
5867
6306
  };
5868
- v = await signAndPostOffer2(ws, "nomos", "installDomain", installPayload2, opts);
6307
+ v = await signAndPostOffer2(ws, "nomos", route.directiveId, lifecyclePayload, opts);
5869
6308
  } catch (e) {
5870
6309
  err4(e.message);
5871
6310
  return 1;
@@ -5887,7 +6326,7 @@ async function deploy2(ws, opts) {
5887
6326
  const txt = d ? JSON.stringify(d) : "no body";
5888
6327
  err4(`deploy '${ws}' failed (${r.status}): ${txt}`);
5889
6328
  if (r.status === 401 || r.status === 403 || r.status === 422 || /warrant|signed|signer|unsigned|authorship|forged|relation|installDomain/i.test(txt)) {
5890
- err4(` this workspace looks WARRANTED \u2014 re-run with --as <principal> to author a SIGNED install offer`);
6329
+ err4(` this workspace looks WARRANTED \u2014 re-run with --as <principal> to author a signed lifecycle offer`);
5891
6330
  err4(` (import the admin signing key first if needed: githolon key import --principal <uid> --secret <key>)`);
5892
6331
  }
5893
6332
  return 1;
@@ -5895,6 +6334,7 @@ async function deploy2(ws, opts) {
5895
6334
  const phase = d.installation?.[0]?.data?.["status.phase"];
5896
6335
  phaseSuffix = phase === "Active" ? " (active)" : typeof phase === "string" ? ` (${phase.toLowerCase()})` : "";
5897
6336
  deployedHash = typeof d.domainHash === "string" ? d.domainHash : void 0;
6337
+ expectsPending = d.staged === true || expectsPending;
5898
6338
  }
5899
6339
  const after = await fetch(`${cloud}/v2/workspaces/${ws}/domains`).then((ar) => ar.json()).then((ad) => ad.ok === true ? ad : {}).catch(() => ({}));
5900
6340
  const currentHashes = new Set(Object.values(after.currentLaw ?? {}));
@@ -5903,13 +6343,28 @@ async function deploy2(ws, opts) {
5903
6343
  const emitPlan = () => {
5904
6344
  if (plan === void 0 || targetHash === void 0 || plan.decisions.length === 0) return;
5905
6345
  for (const line of describeDeployPlan2(plan, targetHash)) out4(line);
5906
- if (plan.replaceCandidates.length > 1) {
5907
- out4(` \u26A0 the package's keys were active on ${plan.replaceCandidates.length} different installs \u2014 installDomain supersedes one (${plan.replaces.slice(0, 16)}\u2026); this deploy is active for every key.`);
5908
- }
5909
- if (!signed && plan.replaces !== void 0 && !opts.keepBeside) {
5910
- out4(` note: the OPEN (unsigned) lane installs beside \u2014 the new law is active, and the prior install is effectively superseded but not marked Superseded. Sign with --as <uid> to record the supersession.`);
5911
- }
5912
6346
  };
6347
+ if (expectsPending && deployedHash !== void 0) {
6348
+ const row = (after.domains ?? []).find((x) => x.domainHash === deployedHash);
6349
+ const unchanged = Object.entries(currentLaw2).every(([key, hash]) => after.currentLaw?.[key] === hash);
6350
+ if (row?.phase !== "Pending" || !unchanged || isCurrent) {
6351
+ err4(`deploy '${ws}' refused its own outcome: a successor must be Pending while the current model keeps serving`);
6352
+ err4(` observed phase=${row?.phase ?? "missing"}; current law ${unchanged && !isCurrent ? "unchanged" : "changed"}`);
6353
+ return 2;
6354
+ }
6355
+ if (beforeDurableHead !== void 0) {
6356
+ const durableHead = await waitForDurableMainAdvance(cloud, ws, beforeDurableHead);
6357
+ if (durableHead === void 0) {
6358
+ err4(`deploy '${ws}' staged the candidate, but canonical Git main did not advance within 30000ms`);
6359
+ return 2;
6360
+ }
6361
+ }
6362
+ out4(`\u2713 staged ${fileName} \u2192 ${ws} (pending)`);
6363
+ if (plan?.supersedes) out4(` current model ${plan.supersedes.slice(0, 16)}\u2026 keeps serving`);
6364
+ out4(` candidate ${deployedHash.slice(0, 16)}\u2026 is in custody but cannot serve yet`);
6365
+ out4(` next: use the candidate generated client and call model.adopt()`);
6366
+ return 0;
6367
+ }
5913
6368
  if (deployedHash !== void 0 && isCurrent) {
5914
6369
  const wasCurrentBefore = Object.values(currentLaw2).includes(deployedHash);
5915
6370
  if (!wasCurrentBefore && beforeDurableHead !== void 0) {
@@ -6316,6 +6771,39 @@ async function runOfflineLegs(eng, ws, lawHash, legs, frameworkHash, deployUsda)
6316
6771
  }
6317
6772
  return done(true, lines);
6318
6773
  }
6774
+ function sealOwnedBusinessBirth(eng, payload, encryption, authorSecret, principal) {
6775
+ const key = payload[encryption.keyField];
6776
+ const digest = createHash4("sha256").update(`${encryption.namespace}${String(key)}`).digest("hex");
6777
+ const childWorkspace = `${encryption.namespace}-${digest.slice(0, 40)}`;
6778
+ const scopeKey = cryptoOwnedScopeKey(
6779
+ eng,
6780
+ childWorkspace,
6781
+ authorSecret,
6782
+ encryption.scope,
6783
+ principal,
6784
+ false
6785
+ );
6786
+ const sealed = {
6787
+ [OWNED_KEY_PAYLOAD_FIELD]: scopeKey.ownedKeySubject,
6788
+ ...scopeKey.ownedCustody ? { [OWNED_CUSTODY_PAYLOAD_FIELD]: scopeKey.ownedCustody } : {}
6789
+ };
6790
+ for (const [field, slot] of Object.entries(encryption.fields)) {
6791
+ const value = payload[field];
6792
+ if (value === void 0 || value === null) continue;
6793
+ sealed[field] = cryptoSealField(eng, {
6794
+ scopeKey: scopeKey.key,
6795
+ scope: encryption.scope,
6796
+ epoch: scopeKey.epoch,
6797
+ aggSid: slot.aggSid,
6798
+ fieldSid: slot.fieldSid,
6799
+ plaintext: String(value)
6800
+ });
6801
+ }
6802
+ payload[BUSINESS_BIRTH_SEED_PAYLOAD_FIELD] = JSON.stringify(sealed);
6803
+ payload[OWNED_KEY_PAYLOAD_FIELD] = scopeKey.ownedKeySubject;
6804
+ if (scopeKey.ownedCustody) payload[OWNED_CUSTODY_PAYLOAD_FIELD] = scopeKey.ownedCustody;
6805
+ return { childWorkspace, scopeKey };
6806
+ }
6319
6807
  function warrantParentForBirth(eng, parentWs, parentDomain, lawHash, genesisOwner, birthActor, dev2, stamp) {
6320
6808
  const authorFrameworkAs = (directiveId, payload, actor) => {
6321
6809
  for (const dom of [parentDomain, "nomos"]) {
@@ -6382,7 +6870,10 @@ function warrantParentForBirth(eng, parentWs, parentDomain, lawHash, genesisOwne
6382
6870
  return { ok: true, attestationVerified: hasDelegation && hasParentAttestation };
6383
6871
  }
6384
6872
  function attestedBirthOffer(eng, parentWs, parentDomain, directiveId, payloadNoCert, lawHash, founder, dev2, verified = true) {
6385
- const probe = author(eng, parentWs, parentDomain, directiveId, payloadNoCert, lawHash, { actor: founder });
6873
+ const probe = author(eng, parentWs, parentDomain, directiveId, payloadNoCert, lawHash, {
6874
+ actor: founder,
6875
+ authorSecret: dev2.secret
6876
+ });
6386
6877
  if (probe.ok !== false) {
6387
6878
  return { res: probe };
6388
6879
  }
@@ -6489,6 +6980,7 @@ async function runChildBirthLeg(eng, ws, lawHash, cb, frameworkHash) {
6489
6980
  return done(true);
6490
6981
  }
6491
6982
  }
6983
+ const ownedBirth = cb.businessEncryption !== void 0 ? sealOwnedBusinessBirth(eng, payload, cb.businessEncryption, dev2.secret, founder) : void 0;
6492
6984
  const { res } = attestedBirthOffer(
6493
6985
  eng,
6494
6986
  parentWs,
@@ -6603,10 +7095,29 @@ async function runChildBirthLeg(eng, ws, lawHash, cb, frameworkHash) {
6603
7095
  return done(false, `the recovered child ${childWs} FAILED verify_chain from its own custody: ${recoveredVerify.error ?? "invalid"}`);
6604
7096
  }
6605
7097
  if (cb.childSeedInstanceId !== void 0) {
6606
- const recoveredRows = qById(eng, childWs, cb.childSeedInstanceId);
7098
+ const recoveredRows = qById(
7099
+ eng,
7100
+ childWs,
7101
+ cb.childSeedInstanceId,
7102
+ ownedBirth === void 0 ? "" : founder,
7103
+ ownedBirth === void 0 ? [] : [ownedBirth.scopeKey]
7104
+ );
6607
7105
  if (recoveredRows.length < 1) {
6608
7106
  return done(false, `the recovered child ${childWs} verified but its business root ${cb.childSeedInstanceId} was missing`);
6609
7107
  }
7108
+ if (ownedBirth !== void 0 && cb.businessEncryption !== void 0) {
7109
+ if (childWs !== ownedBirth.childWorkspace) {
7110
+ return done(false, `the born child '${childWs}' differs from the deterministic owned-custody address '${ownedBirth.childWorkspace}'`);
7111
+ }
7112
+ const data = recoveredRows[0]?.data ?? {};
7113
+ for (const field of Object.keys(cb.businessEncryption.fields)) {
7114
+ const expected = cb.birthPayload[field];
7115
+ if (expected !== void 0 && data[field] !== expected) {
7116
+ return done(false, `the recovered owned ${cb.businessAggregateId ?? "aggregate"} did not decrypt '${field}' back to its business fact`);
7117
+ }
7118
+ }
7119
+ lines.push(`\u2713 owned custody \u2014 recovered business facts decrypted under the creator's framework-managed device key`);
7120
+ }
6610
7121
  }
6611
7122
  if (cb.businessAggregateId !== void 0) {
6612
7123
  lines.push(`\u2713 persistence + recovery \u2014 evicted, restored from durable custody, and ${cb.businessAggregateId} read back`);
@@ -6870,7 +7381,7 @@ OFFLINE PROOF GREEN (${Math.round(v.ms)}ms) \u2014 the same legs rerun on every
6870
7381
  console.log(`Run the same generated business proof against Nomos Cloud when ready: githolon proof --live`);
6871
7382
  return 0;
6872
7383
  }
6873
- var keyHashOf, OFFLINE_PROOF_VERIFIER;
7384
+ var keyHashOf, OFFLINE_PROOF_VERIFIER, OWNED_KEY_PAYLOAD_FIELD, OWNED_CUSTODY_PAYLOAD_FIELD, BUSINESS_BIRTH_SEED_PAYLOAD_FIELD;
6874
7385
  var init_proof_offline = __esm({
6875
7386
  "src/proof_offline.ts"() {
6876
7387
  "use strict";
@@ -6878,6 +7389,9 @@ var init_proof_offline = __esm({
6878
7389
  init_local_holon();
6879
7390
  keyHashOf = (pubB64) => createHash4("sha256").update(Buffer.from(pubB64, "base64")).digest("hex");
6880
7391
  OFFLINE_PROOF_VERIFIER = "nomos:test-mint";
7392
+ OWNED_KEY_PAYLOAD_FIELD = "__nomos_owned_device_key_subject_7c1f5e";
7393
+ OWNED_CUSTODY_PAYLOAD_FIELD = "__nomos_owned_device_custody_7c1f5e";
7394
+ BUSINESS_BIRTH_SEED_PAYLOAD_FIELD = "__nomos_birth_seed_payload_7c1f5e";
6881
7395
  }
6882
7396
  });
6883
7397
 
@@ -8694,7 +9208,7 @@ export const ${n.pascal} = aggregate("${n.pascal}", {
8694
9208
  status: t.enum(${n.statusesConst}).merge(Lww),
8695
9209
  tags: t.set(t.string()).merge(AddWins),
8696
9210
  customFields: t.map(t.string()).merge(MapOf(Lww)),
8697
- });`;
9211
+ }).public();`;
8698
9212
  }
8699
9213
  function domainTemplate(raw) {
8700
9214
  const n = namesFor(raw);
@@ -8766,7 +9280,7 @@ import { z, aggregate, directive, t, Conflict, set } from "@githolon/dsl";
8766
9280
  // Stub target \u2014 in a real domain, import the existing aggregate handle instead.
8767
9281
  export const ${n.pascal} = aggregate("${n.pascal}", {
8768
9282
  label: t.string().merge(Conflict),
8769
- });
9283
+ }).public();
8770
9284
 
8771
9285
  /** ${n.camel} \u2014 .mutates the target aggregate. */
8772
9286
  export const ${n.camel} = directive("${n.camel}")
@@ -9481,21 +9995,19 @@ var HELP = {
9481
9995
  examples: ["githolon revoke creation 32c7100e-\u2026", "githolon revoke admin 32c7100e-\u2026"]
9482
9996
  },
9483
9997
  deploy: {
9484
- usage: "githolon deploy [<ws>] [--as <uid>] [--keep-beside] [--retire-replaced] [--file <deploy.json>] [--cloud <url>] [--target <name>]",
9485
- what: "Deploy build/*.deploy.json \u2014 kernel-gated, no host secret. DEFAULT = REPLACE-CURRENT: for each domain\nkey in the package it resolves the workspace's active install (the holon's existing `currentLaw` map)\nand passes it as installDomain's `replaces`, so the old install becomes superseded for that key.\nRe-deploying the SAME hash stays idempotent (never self-superseded).\nTWO lanes, picked automatically:\n\u2022 SIGNED (a WARRANTED ws, or when --as / the active `githolon use` principal has a signing key on\n file): signs a `nomos/installDomain` offer (carrying `replaces`) with that key and relays it; the\n kernel re-verifies the signature + judges the installDomain relation. Required for warranted platforms.\n\u2022 OPEN (unwarranted ws / no key): the open POST /domains installs beside. The new law becomes active,\n but the prior install is only effectively superseded unless you sign with --as to record Superseded.\nAfter the POST it CONFIRMS the deployed hash is active (exit 2 if committed-but-not-active) and prints\nwhat moved, per key. With no <ws>, the project's `workspace` binding resolves it.\nLifecycle vocabulary: active = served now; superseded = replayable, not served; retired = explicitly withdrawn, replayable, not served.",
9998
+ usage: "githolon deploy [<ws>] [--as <uid>] [--file <deploy.json>] [--cloud <url>] [--target <name>]",
9999
+ what: "Put the compiled model in the workspace's verified custody. There is one lifecycle: the first\nmodel becomes active so a blank application can start; every successor stays Pending while the current\nmodel keeps serving. The candidate generated client then runs the exact-history proof through\n`model.adopt()` and either adopts immediately or returns a business-readable decision requiring\n`decision.accept(reason)`. Deployment cannot choose a replacement mode or bypass that proof.\nRe-deploying the same content hash is idempotent. With no <ws>, the project's workspace binding resolves it.",
9486
10000
  flags: [
9487
- ["--as <uid>", "deploy as this principal via the SIGNED lane (needs its key \u2014 githolon key import; else githolon use)"],
9488
- ["--keep-beside", "install beside the prior law (the old multi-version default; prior becomes effectively superseded by recency)"],
9489
- ["--retire-replaced", "supersede via Retired \u2014 the prior install is explicitly withdrawn"],
10001
+ ["--as <uid>", "attribute the deployment to this principal (needs its enrolled signing key)"],
9490
10002
  ["--file <path>", "an explicit deploy body (default: the one build/*.deploy.json)"],
9491
10003
  ["--target <name>", "pick among named targets (workspace: { dev: \u2026, prod: \u2026 })"],
9492
10004
  ["--cloud <url>", "target cloud"]
9493
10005
  ],
9494
- examples: ["githolon deploy", "githolon deploy my-guestbook", "githolon deploy co2-platform --as <admin-uid>", "githolon deploy my-ws --keep-beside", "githolon deploy my-ws --retire-replaced"]
10006
+ examples: ["githolon deploy", "githolon deploy my-guestbook", "githolon deploy co2-platform --as <admin-uid>"]
9495
10007
  },
9496
10008
  domains: {
9497
10009
  usage: "githolon domains <status <ws> [--explain-current-interface] | retire <ws> <hashOrKey> [--with <hashOrKey>]>",
9498
- what: "The INSTALL-LIFECYCLE lane over a workspace's law.\n\u2022 status: per domain KEY, the active hash, then the superseded/retired lineage behind it.\n Lifecycle vocabulary: active = served now; superseded = replaced by a newer install for the same domain\n key, replayable, not served; retired = explicitly withdrawn, replayable, not served.\n --explain-current-interface spells out why active reads/dispatch use only that active map.\n\u2022 retire: mark an install Retired via a signed installDomain offer. The framework retires only as the\n consequence of installing a SUCCESSOR \u2014 by default the local compiled package (build/*.deploy.json),\n or --with <hashOrKey> names an already-installed successor. A key the successor does not cover is left\n with NO active law \u2014 WARNED loudly, never blocked. A key resolves to its active hash; a 64-hex arg is\n taken as the install hash.",
10010
+ what: "Framework lifecycle inspection and explicit withdrawal.\n\u2022 status: per domain KEY, the active hash, then the superseded/retired lineage behind it.\n Lifecycle vocabulary: active = served now; superseded = replaced by a newer install for the same domain\n key, replayable, not served; retired = explicitly withdrawn, replayable, not served.\n --explain-current-interface spells out why active reads/dispatch use only that active map.\n\u2022 retire: mark an install Retired via a signed installDomain offer. The framework retires only as the\n consequence of installing a SUCCESSOR \u2014 by default the local compiled package (build/*.deploy.json),\n or --with <hashOrKey> names an already-installed successor. A key the successor does not cover is left\n with NO active law \u2014 WARNED loudly, never blocked. A key resolves to its active hash; a 64-hex arg is\n taken as the install hash.",
9499
10011
  flags: [
9500
10012
  ["--explain-current-interface", "status: also explain the frontier resolution (why the interface is what it is)"],
9501
10013
  ["--with <hashOrKey>", "retire: the retiring successor package \u2014 an already-installed hash or served key"],
@@ -9506,7 +10018,7 @@ var HELP = {
9506
10018
  },
9507
10019
  secret: {
9508
10020
  usage: "githolon secret <set <ws> <push-password> | rotate <ws>> [--cloud <url>]",
9509
- what: "The git push-password store (~/.holon/credentials.json, 0600) for authenticated git sync to the\ncloud host. The open deploy lane uses no secret. `set` imports a push password you already hold;\n`rotate` is RETIRED (the host holds no ownership credential to rotate \u2014 410). (There is no\npush-to-create birth: a workspace is born by its parent authoring its birth.)",
10021
+ what: "The git push-password store (~/.holon/credentials.json, 0600) for authenticated git sync to the\ncloud host. Deployment has no host secret. `set` imports a push password you already hold;\n`rotate` is RETIRED (the host holds no ownership credential to rotate \u2014 410). (There is no\npush-to-create birth: a workspace is born by its parent authoring its birth.)",
9510
10022
  examples: ["githolon secret set my-ws holon_v1_\u2026"]
9511
10023
  },
9512
10024
  git: {
@@ -10052,15 +10564,13 @@ function cloudArgs(rest) {
10052
10564
  const pos = [];
10053
10565
  const opts = {};
10054
10566
  const VALUE_FLAGS = ["--cloud", "--principal", "--as", "--file", "--target", "--parent", "--via", "--max", "--label", "--secret", "--payload", "--payload-file", "--save", "--format", "--with", "--out", "--reason", "--oid", "--seed-secret", "--gcp-secret", "--controller-hash", "--plan", "--depth", "--domain", "--directive"];
10055
- const BOOL_FLAGS = ["--sha256", "--stamp", "--yes", "--clear", "--keep-beside", "--retire-replaced", "--explain-current-interface", "--dry-run", "--json", "--full"];
10567
+ const BOOL_FLAGS = ["--sha256", "--stamp", "--yes", "--clear", "--explain-current-interface", "--dry-run", "--json", "--full"];
10056
10568
  for (let i = 0; i < rest.length; i++) {
10057
10569
  const a = rest[i];
10058
10570
  if (BOOL_FLAGS.includes(a)) {
10059
10571
  if (a === "--sha256") opts.sha256 = true;
10060
10572
  else if (a === "--stamp") opts.stamp = true;
10061
10573
  else if (a === "--clear") opts.clear = true;
10062
- else if (a === "--keep-beside") opts.keepBeside = true;
10063
- else if (a === "--retire-replaced") opts.retireReplaced = true;
10064
10574
  else if (a === "--explain-current-interface") opts.explain = true;
10065
10575
  else if (a === "--dry-run") opts.dryRun = true;
10066
10576
  else if (a === "--json") opts.json = true;