@manifest-network/manifest-agent-core 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +1 -1
  2. package/dist/close-lease.d.ts +3 -2
  3. package/dist/close-lease.d.ts.map +1 -1
  4. package/dist/close-lease.js +4 -3
  5. package/dist/close-lease.js.map +1 -1
  6. package/dist/deploy-app.d.ts +3 -2
  7. package/dist/deploy-app.d.ts.map +1 -1
  8. package/dist/deploy-app.js +245 -77
  9. package/dist/deploy-app.js.map +1 -1
  10. package/dist/internals/build-fred-input.d.ts +38 -0
  11. package/dist/internals/build-fred-input.d.ts.map +1 -0
  12. package/dist/internals/build-fred-input.js +147 -0
  13. package/dist/internals/build-fred-input.js.map +1 -0
  14. package/dist/internals/evaluate-readiness-from-fred.d.ts +28 -0
  15. package/dist/internals/evaluate-readiness-from-fred.d.ts.map +1 -0
  16. package/dist/internals/evaluate-readiness-from-fred.js +94 -0
  17. package/dist/internals/evaluate-readiness-from-fred.js.map +1 -0
  18. package/dist/internals/format-success.js.map +1 -1
  19. package/dist/internals/guarded-fetch.d.ts +2 -138
  20. package/dist/internals/guarded-fetch.js +1 -241
  21. package/dist/internals/humanize-denom.js.map +1 -1
  22. package/dist/internals/inspect-image.js.map +1 -1
  23. package/dist/internals/lease-items.js +1 -4
  24. package/dist/internals/lease-items.js.map +1 -1
  25. package/dist/internals/render-deployment-plan.js.map +1 -1
  26. package/dist/internals/verify-recover.js.map +1 -1
  27. package/dist/manage-domain.d.ts +3 -2
  28. package/dist/manage-domain.d.ts.map +1 -1
  29. package/dist/manage-domain.js +4 -3
  30. package/dist/manage-domain.js.map +1 -1
  31. package/dist/troubleshoot.js.map +1 -1
  32. package/dist/types.d.ts +19 -0
  33. package/dist/types.d.ts.map +1 -1
  34. package/package.json +3 -5
  35. package/dist/internals/guarded-fetch.d.ts.map +0 -1
  36. package/dist/internals/guarded-fetch.js.map +0 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  TypeScript orchestration surface for Manifest agent flows. This package owns the deploy / manage-domain / troubleshoot / close-lease orchestration that host surfaces consume in lockstep — a bug fix in the core's recovery branch fixes every host surface simultaneously.
4
4
 
5
- > **Status.** All four orchestration functions have real implementations as of ENG-129 (PRs 1–4). The package remains `private: true` pending a publish decision; do not depend on it from external repos yet.
5
+ > **Status.** Publicly published on npm as `@manifest-network/manifest-agent-core` (ENG-129). Consume via `npm install @manifest-network/manifest-agent-core`. The MCP-server adapter that wraps this orchestration surface via elicitation lives at [`@manifest-network/manifest-mcp-agent`](../agent/README.md).
6
6
 
7
7
  See [ENG-127](https://linear.app/liftedinit/issue/ENG-127) for the broader initiative and [ENG-128](https://linear.app/liftedinit/issue/ENG-128) for the bootstrap PR.
8
8
 
@@ -4,8 +4,9 @@ import { CloseLeaseArgs, CloseLeaseCallbacks, CloseLeaseOptions, CloseLeaseResul
4
4
  /**
5
5
  * Close a lease and verify it reached a terminal on-chain state.
6
6
  *
7
- * @throws `ManifestMCPError(INVALID_CONFIG)` for args validation or when
8
- * `onConfirm` returns `'no'`.
7
+ * @throws `ManifestMCPError(INVALID_CONFIG)` for args validation.
8
+ * @throws `ManifestMCPError(OPERATION_CANCELLED)` when `onConfirm` returns
9
+ * `'no'` (deliberate user cancellation — ENG-272).
9
10
  * @throws `ManifestMCPError` (typically `TX_FAILED`) propagated as-is
10
11
  * from the `stopApp()` broadcast step. Broadcast errors do NOT invoke
11
12
  * `onFailure` — that callback is reserved for post-broadcast
@@ -1 +1 @@
1
- {"version":3,"file":"close-lease.d.ts","names":[],"sources":["../src/close-lease.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+EsB,UAAA,CACpB,IAAA,EAAM,cAAA,EACN,SAAA,EAAW,mBAAA,EACX,IAAA,EAAM,iBAAA,GACL,OAAA,CAAQ,gBAAA"}
1
+ {"version":3,"file":"close-lease.d.ts","names":[],"sources":["../src/close-lease.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAgFsB,UAAA,CACpB,IAAA,EAAM,cAAA,EACN,SAAA,EAAW,mBAAA,EACX,IAAA,EAAM,iBAAA,GACL,OAAA,CAAQ,gBAAA"}
@@ -26,8 +26,9 @@ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
26
26
  /**
27
27
  * Close a lease and verify it reached a terminal on-chain state.
28
28
  *
29
- * @throws `ManifestMCPError(INVALID_CONFIG)` for args validation or when
30
- * `onConfirm` returns `'no'`.
29
+ * @throws `ManifestMCPError(INVALID_CONFIG)` for args validation.
30
+ * @throws `ManifestMCPError(OPERATION_CANCELLED)` when `onConfirm` returns
31
+ * `'no'` (deliberate user cancellation — ENG-272).
31
32
  * @throws `ManifestMCPError` (typically `TX_FAILED`) propagated as-is
32
33
  * from the `stopApp()` broadcast step. Broadcast errors do NOT invoke
33
34
  * `onFailure` — that callback is reserved for post-broadcast
@@ -53,7 +54,7 @@ async function closeLease(args, callbacks, opts) {
53
54
  validateArgs(args);
54
55
  const block = renderConfirmationBlock(args);
55
56
  if (callbacks.onConfirm) {
56
- if (await callbacks.onConfirm(block) !== "yes") throw new ManifestMCPError(ManifestMCPErrorCode.INVALID_CONFIG, "User declined to proceed with close-lease.");
57
+ if (await callbacks.onConfirm(block) !== "yes") throw new ManifestMCPError(ManifestMCPErrorCode.OPERATION_CANCELLED, "User declined to proceed with close-lease.");
57
58
  }
58
59
  callbacks.onProgress?.({ kind: "user_confirmed" });
59
60
  await stopApp(opts.clientManager, args.leaseUuid);
@@ -1 +1 @@
1
- {"version":3,"file":"close-lease.js","names":["decodeLeaseState"],"sources":["../src/close-lease.ts"],"sourcesContent":["/**\n * Public entry point: orchestrate closing an existing lease via the\n * `close-lease` billing tx.\n *\n * Composition (mirrors `deploy-app.ts` / `manage-domain.ts`):\n *\n * 1. Validate args.\n * 2. Render a confirmation block + optionally consult `onConfirm`.\n * 3. Broadcast `stopApp` (which submits `MsgCloseLease`).\n * 4. Verify the post-broadcast on-chain state via `verifyAndRecover`\n * driving a direct `billing.v1.lease({ leaseUuid })` query +\n * `lease-state.decode` + `isTerminal`. Terminal states (CLOSED /\n * REJECTED / EXPIRED / INSUFFICIENT_FUNDS) count as success;\n * PENDING / ACTIVE map to the `pending_drift` branch; a chain\n * response with no lease (`{ lease: null }`) maps to the catch-all\n * `unclassified` branch.\n * 5. On verify-failure, invoke the simple-form `onFailure({ reason })`\n * then throw `ManifestMCPError(TX_FAILED)`. On success, emit\n * `onComplete` with the typed `CloseLeaseResult`.\n */\n\nimport {\n ManifestMCPError,\n ManifestMCPErrorCode,\n stopApp,\n} from '@manifest-network/manifest-mcp-core';\nimport {\n decode as decodeLeaseState,\n isTerminal,\n} from './internals/lease-state.js';\nimport {\n type VerificationSpec,\n verifyAndRecover,\n} from './internals/verify-recover.js';\nimport type {\n CloseLeaseArgs,\n CloseLeaseCallbacks,\n CloseLeaseOptions,\n CloseLeaseResult,\n DeploymentPlanBlock,\n LeaseStateName,\n} from './types.js';\n\nconst UUID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\ntype CloseOutcome = 'terminal' | 'pending' | 'not_found';\n\ninterface CloseDiag {\n stateName?: LeaseStateName;\n reason?: string;\n}\n\n/**\n * Close a lease and verify it reached a terminal on-chain state.\n *\n * @throws `ManifestMCPError(INVALID_CONFIG)` for args validation or when\n * `onConfirm` returns `'no'`.\n * @throws `ManifestMCPError` (typically `TX_FAILED`) propagated as-is\n * from the `stopApp()` broadcast step. Broadcast errors do NOT invoke\n * `onFailure` — that callback is reserved for post-broadcast\n * verification failures. `stopApp` already raises a structured\n * `ManifestMCPError` from the core package; wrapping it again at this\n * layer would be redundant. Callers wanting to react to broadcast\n * errors should catch them at the call site.\n * @throws `ManifestMCPError(TX_FAILED)` when post-broadcast verification\n * reaches one of two failure modes (both with `onFailure({ reason })`\n * invoked first):\n * - the lease is still non-terminal (`pending_drift` branch — state\n * decoded as PENDING / ACTIVE / similar non-terminal); or\n * - the chain returns `{ lease: null }` post-close, so the lease is\n * not visible on-chain (`unclassified` branch).\n * @throws `ManifestMCPError(QUERY_FAILED)` when the post-broadcast verify\n * chain query (`billing.v1.lease`) raises a non-NotFound error\n * (RPC / transport / decoding failure). Wrapped inside the verifier\n * closure so the failure flows through `onFailure({ reason })` before\n * the throw. Structured `ManifestMCPError`s raised by the chain client\n * are re-thrown as-is (with `onFailure` invoked first).\n */\nexport async function closeLease(\n args: CloseLeaseArgs,\n callbacks: CloseLeaseCallbacks,\n opts: CloseLeaseOptions,\n): Promise<CloseLeaseResult> {\n validateArgs(args);\n\n const block = renderConfirmationBlock(args);\n if (callbacks.onConfirm) {\n const yesNo = await callbacks.onConfirm(block);\n if (yesNo !== 'yes') {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n 'User declined to proceed with close-lease.',\n );\n }\n }\n callbacks.onProgress?.({ kind: 'user_confirmed' });\n\n await stopApp(opts.clientManager, args.leaseUuid);\n\n // Direct single-lease query (Copilot review PR #60, comment 3275999624):\n // the previous `leasesByTenant` + page-1-only pagination would\n // false-`not_found` for tenants with >100 leases. `billing.v1.lease`\n // is the same query shape `troubleshoot.ts` already uses; it's\n // tenant-agnostic and bounded to a single lease.\n const spec: VerificationSpec<unknown, CloseOutcome, CloseDiag> = {\n verifier: async () => {\n // Wrap the chain call in try/catch (Copilot review PR #60,\n // comment 3276419264): if `billing.v1.lease` rejects (RPC down,\n // transport, structured `ManifestMCPError`), the error would\n // otherwise propagate OUT of `verifyAndRecover` and bypass the\n // post-verify `onFailure({ reason })` callback below. Mirror\n // the disambiguation pattern from `lookupDomain` (commit aaa5cc5)\n // and `troubleshootDeployment` (commit f1a4737): invoke\n // `onFailure` first, then re-throw `ManifestMCPError` as-is or\n // wrap plain errors as `QUERY_FAILED`.\n let result: unknown;\n try {\n const queryClient = await opts.clientManager.getQueryClient();\n result = await queryClient.liftedinit.billing.v1.lease({\n leaseUuid: args.leaseUuid,\n });\n } catch (err) {\n const reason = `Failed to query lease ${args.leaseUuid} during close-verify: ${\n err instanceof Error ? err.message : String(err)\n }`;\n if (callbacks.onFailure) {\n await callbacks.onFailure({ reason });\n }\n if (err instanceof ManifestMCPError) {\n throw err;\n }\n throw new ManifestMCPError(ManifestMCPErrorCode.QUERY_FAILED, reason);\n }\n const lease = (result as { lease?: unknown })?.lease;\n if (lease === null || lease === undefined) {\n return {\n outcome: 'not_found' as const,\n diagnostic: {\n reason: `lease ${args.leaseUuid} not visible on chain after close`,\n },\n };\n }\n const rawState = (lease as { state?: unknown }).state;\n const stateName = decodeLeaseState(\n typeof rawState === 'number' || typeof rawState === 'string'\n ? rawState\n : undefined,\n );\n if (stateName === undefined) {\n return {\n outcome: 'pending' as const,\n diagnostic: {\n reason: `lease ${args.leaseUuid} state could not be decoded (raw=${String(rawState)})`,\n },\n };\n }\n return {\n outcome: (isTerminal(stateName) ? 'terminal' : 'pending') as\n | 'terminal'\n | 'pending',\n diagnostic: { stateName },\n };\n },\n successValues: ['terminal'],\n branches: {\n pending: {\n branchId: 'pending_drift',\n journalActionTags: ['close-lease-verify-pending'],\n buildFailureEnvelope: (d) => ({\n outcome: 'failed',\n reason:\n d.reason ??\n `close_lease tx accepted but state is still ${d.stateName ?? 'unknown'}.`,\n }),\n buildRecoveryOptions: () => [],\n },\n not_found: {\n branchId: 'unclassified',\n journalActionTags: ['close-lease-verify-not-found'],\n buildFailureEnvelope: (d) => ({\n outcome: 'failed',\n reason:\n d.reason ??\n `Lease ${args.leaseUuid} not visible on chain after close.`,\n }),\n buildRecoveryOptions: () => [],\n },\n },\n };\n\n const verifyResult = await verifyAndRecover(spec, undefined);\n\n if (verifyResult.result !== 'success') {\n const reason =\n verifyResult.failure?.reason ?? 'close-lease verification failed.';\n if (callbacks.onFailure) {\n await callbacks.onFailure({ reason });\n }\n throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, reason);\n }\n\n // Invariant: when `verifyAndRecover` returns success, the matched\n // outcome was `'terminal'`, and the verifier's `terminal` branch\n // ALWAYS sets `diagnostic.stateName` (see the spec above). A missing\n // `stateName` on the success path means the verifier invariant is\n // broken — likely a future refactor regression. The previous\n // implementation fell back to `'LEASE_STATE_CLOSED'` silently, which\n // would lie to the caller (Copilot review PR #60, comment 3276719603).\n // Fail loudly with a typed error instead. `TX_FAILED` is the closest\n // available code in `ManifestMCPErrorCode` (no `INTERNAL_ERROR`\n // variant); the message names the invariant explicitly.\n if (!verifyResult.diagnostic.stateName) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.TX_FAILED,\n `close-lease verifier invariant violated: success outcome reached without diagnostic.stateName for lease ${args.leaseUuid}`,\n );\n }\n const finalState: LeaseStateName = verifyResult.diagnostic.stateName;\n const result: CloseLeaseResult = {\n leaseUuid: args.leaseUuid,\n finalState,\n };\n callbacks.onComplete?.(result);\n return result;\n}\n\n// --- Helpers --------------------------------------------------------\n\nfunction validateArgs(args: CloseLeaseArgs): void {\n if (typeof args.leaseUuid !== 'string' || !args.leaseUuid.match(UUID_RE)) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `closeLease: leaseUuid must be a UUID; got \"${args.leaseUuid}\".`,\n );\n }\n}\n\nfunction renderConfirmationBlock(args: CloseLeaseArgs): DeploymentPlanBlock {\n // Image is not tracked in `CloseLeaseArgs` and `stopApp` doesn't return it;\n // surface the gap explicitly so reviewers/users see the missing context\n // rather than silently omitting an image field they'd expect.\n const text = [\n `Close lease ${args.leaseUuid}.`,\n ' Image: (image not recorded)',\n ' This is permanent — the lease cannot be reopened.',\n '',\n 'Proceed?',\n ].join('\\n');\n return { text };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA2CA,MAAM,UACJ;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCF,eAAsB,WACpB,MACA,WACA,MAC2B;AAC3B,cAAa,KAAK;CAElB,MAAM,QAAQ,wBAAwB,KAAK;AAC3C,KAAI,UAAU;MACE,MAAM,UAAU,UAAU,MAAM,KAChC,MACZ,OAAM,IAAI,iBACR,qBAAqB,gBACrB,6CACD;;AAGL,WAAU,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAM,QAAQ,KAAK,eAAe,KAAK,UAAU;CA6FjD,MAAM,eAAe,MAAM,iBAtFsC;EAC/D,UAAU,YAAY;GAUpB,IAAI;AACJ,OAAI;AAEF,aAAS,OADW,MAAM,KAAK,cAAc,gBAAgB,EAClC,WAAW,QAAQ,GAAG,MAAM,EACrD,WAAW,KAAK,WACjB,CAAC;YACK,KAAK;IACZ,MAAM,SAAS,yBAAyB,KAAK,UAAU,wBACrD,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAElD,QAAI,UAAU,UACZ,OAAM,UAAU,UAAU,EAAE,QAAQ,CAAC;AAEvC,QAAI,eAAe,iBACjB,OAAM;AAER,UAAM,IAAI,iBAAiB,qBAAqB,cAAc,OAAO;;GAEvE,MAAM,QAAS,QAAgC;AAC/C,OAAI,UAAU,QAAQ,UAAU,KAAA,EAC9B,QAAO;IACL,SAAS;IACT,YAAY,EACV,QAAQ,SAAS,KAAK,UAAU,oCACjC;IACF;GAEH,MAAM,WAAY,MAA8B;GAChD,MAAM,YAAYA,OAChB,OAAO,aAAa,YAAY,OAAO,aAAa,WAChD,WACA,KAAA,EACL;AACD,OAAI,cAAc,KAAA,EAChB,QAAO;IACL,SAAS;IACT,YAAY,EACV,QAAQ,SAAS,KAAK,UAAU,mCAAmC,OAAO,SAAS,CAAC,IACrF;IACF;AAEH,UAAO;IACL,SAAU,WAAW,UAAU,GAAG,aAAa;IAG/C,YAAY,EAAE,WAAW;IAC1B;;EAEH,eAAe,CAAC,WAAW;EAC3B,UAAU;GACR,SAAS;IACP,UAAU;IACV,mBAAmB,CAAC,6BAA6B;IACjD,uBAAuB,OAAO;KAC5B,SAAS;KACT,QACE,EAAE,UACF,8CAA8C,EAAE,aAAa,UAAU;KAC1E;IACD,4BAA4B,EAAE;IAC/B;GACD,WAAW;IACT,UAAU;IACV,mBAAmB,CAAC,+BAA+B;IACnD,uBAAuB,OAAO;KAC5B,SAAS;KACT,QACE,EAAE,UACF,SAAS,KAAK,UAAU;KAC3B;IACD,4BAA4B,EAAE;IAC/B;GACF;EACF,EAEiD,KAAA,EAAU;AAE5D,KAAI,aAAa,WAAW,WAAW;EACrC,MAAM,SACJ,aAAa,SAAS,UAAU;AAClC,MAAI,UAAU,UACZ,OAAM,UAAU,UAAU,EAAE,QAAQ,CAAC;AAEvC,QAAM,IAAI,iBAAiB,qBAAqB,WAAW,OAAO;;AAapE,KAAI,CAAC,aAAa,WAAW,UAC3B,OAAM,IAAI,iBACR,qBAAqB,WACrB,2GAA2G,KAAK,YACjH;CAEH,MAAM,aAA6B,aAAa,WAAW;CAC3D,MAAM,SAA2B;EAC/B,WAAW,KAAK;EAChB;EACD;AACD,WAAU,aAAa,OAAO;AAC9B,QAAO;;AAKT,SAAS,aAAa,MAA4B;AAChD,KAAI,OAAO,KAAK,cAAc,YAAY,CAAC,KAAK,UAAU,MAAM,QAAQ,CACtE,OAAM,IAAI,iBACR,qBAAqB,gBACrB,8CAA8C,KAAK,UAAU,IAC9D;;AAIL,SAAS,wBAAwB,MAA2C;AAW1E,QAAO,EAAE,MAPI;EACX,eAAe,KAAK,UAAU;EAC9B;EACA;EACA;EACA;EACD,CAAC,KAAK,KAAK,EACG"}
1
+ {"version":3,"file":"close-lease.js","names":["decodeLeaseState"],"sources":["../src/close-lease.ts"],"sourcesContent":["/**\n * Public entry point: orchestrate closing an existing lease via the\n * `close-lease` billing tx.\n *\n * Composition (mirrors `deploy-app.ts` / `manage-domain.ts`):\n *\n * 1. Validate args.\n * 2. Render a confirmation block + optionally consult `onConfirm`.\n * 3. Broadcast `stopApp` (which submits `MsgCloseLease`).\n * 4. Verify the post-broadcast on-chain state via `verifyAndRecover`\n * driving a direct `billing.v1.lease({ leaseUuid })` query +\n * `lease-state.decode` + `isTerminal`. Terminal states (CLOSED /\n * REJECTED / EXPIRED / INSUFFICIENT_FUNDS) count as success;\n * PENDING / ACTIVE map to the `pending_drift` branch; a chain\n * response with no lease (`{ lease: null }`) maps to the catch-all\n * `unclassified` branch.\n * 5. On verify-failure, invoke the simple-form `onFailure({ reason })`\n * then throw `ManifestMCPError(TX_FAILED)`. On success, emit\n * `onComplete` with the typed `CloseLeaseResult`.\n */\n\nimport {\n ManifestMCPError,\n ManifestMCPErrorCode,\n stopApp,\n} from '@manifest-network/manifest-mcp-core';\nimport {\n decode as decodeLeaseState,\n isTerminal,\n} from './internals/lease-state.js';\nimport {\n type VerificationSpec,\n verifyAndRecover,\n} from './internals/verify-recover.js';\nimport type {\n CloseLeaseArgs,\n CloseLeaseCallbacks,\n CloseLeaseOptions,\n CloseLeaseResult,\n DeploymentPlanBlock,\n LeaseStateName,\n} from './types.js';\n\nconst UUID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\ntype CloseOutcome = 'terminal' | 'pending' | 'not_found';\n\ninterface CloseDiag {\n stateName?: LeaseStateName;\n reason?: string;\n}\n\n/**\n * Close a lease and verify it reached a terminal on-chain state.\n *\n * @throws `ManifestMCPError(INVALID_CONFIG)` for args validation.\n * @throws `ManifestMCPError(OPERATION_CANCELLED)` when `onConfirm` returns\n * `'no'` (deliberate user cancellation — ENG-272).\n * @throws `ManifestMCPError` (typically `TX_FAILED`) propagated as-is\n * from the `stopApp()` broadcast step. Broadcast errors do NOT invoke\n * `onFailure` — that callback is reserved for post-broadcast\n * verification failures. `stopApp` already raises a structured\n * `ManifestMCPError` from the core package; wrapping it again at this\n * layer would be redundant. Callers wanting to react to broadcast\n * errors should catch them at the call site.\n * @throws `ManifestMCPError(TX_FAILED)` when post-broadcast verification\n * reaches one of two failure modes (both with `onFailure({ reason })`\n * invoked first):\n * - the lease is still non-terminal (`pending_drift` branch — state\n * decoded as PENDING / ACTIVE / similar non-terminal); or\n * - the chain returns `{ lease: null }` post-close, so the lease is\n * not visible on-chain (`unclassified` branch).\n * @throws `ManifestMCPError(QUERY_FAILED)` when the post-broadcast verify\n * chain query (`billing.v1.lease`) raises a non-NotFound error\n * (RPC / transport / decoding failure). Wrapped inside the verifier\n * closure so the failure flows through `onFailure({ reason })` before\n * the throw. Structured `ManifestMCPError`s raised by the chain client\n * are re-thrown as-is (with `onFailure` invoked first).\n */\nexport async function closeLease(\n args: CloseLeaseArgs,\n callbacks: CloseLeaseCallbacks,\n opts: CloseLeaseOptions,\n): Promise<CloseLeaseResult> {\n validateArgs(args);\n\n const block = renderConfirmationBlock(args);\n if (callbacks.onConfirm) {\n const yesNo = await callbacks.onConfirm(block);\n if (yesNo !== 'yes') {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.OPERATION_CANCELLED,\n 'User declined to proceed with close-lease.',\n );\n }\n }\n callbacks.onProgress?.({ kind: 'user_confirmed' });\n\n await stopApp(opts.clientManager, args.leaseUuid);\n\n // Direct single-lease query (Copilot review PR #60, comment 3275999624):\n // the previous `leasesByTenant` + page-1-only pagination would\n // false-`not_found` for tenants with >100 leases. `billing.v1.lease`\n // is the same query shape `troubleshoot.ts` already uses; it's\n // tenant-agnostic and bounded to a single lease.\n const spec: VerificationSpec<unknown, CloseOutcome, CloseDiag> = {\n verifier: async () => {\n // Wrap the chain call in try/catch (Copilot review PR #60,\n // comment 3276419264): if `billing.v1.lease` rejects (RPC down,\n // transport, structured `ManifestMCPError`), the error would\n // otherwise propagate OUT of `verifyAndRecover` and bypass the\n // post-verify `onFailure({ reason })` callback below. Mirror\n // the disambiguation pattern from `lookupDomain` (commit aaa5cc5)\n // and `troubleshootDeployment` (commit f1a4737): invoke\n // `onFailure` first, then re-throw `ManifestMCPError` as-is or\n // wrap plain errors as `QUERY_FAILED`.\n let result: unknown;\n try {\n const queryClient = await opts.clientManager.getQueryClient();\n result = await queryClient.liftedinit.billing.v1.lease({\n leaseUuid: args.leaseUuid,\n });\n } catch (err) {\n const reason = `Failed to query lease ${args.leaseUuid} during close-verify: ${\n err instanceof Error ? err.message : String(err)\n }`;\n if (callbacks.onFailure) {\n await callbacks.onFailure({ reason });\n }\n if (err instanceof ManifestMCPError) {\n throw err;\n }\n throw new ManifestMCPError(ManifestMCPErrorCode.QUERY_FAILED, reason);\n }\n const lease = (result as { lease?: unknown })?.lease;\n if (lease === null || lease === undefined) {\n return {\n outcome: 'not_found' as const,\n diagnostic: {\n reason: `lease ${args.leaseUuid} not visible on chain after close`,\n },\n };\n }\n const rawState = (lease as { state?: unknown }).state;\n const stateName = decodeLeaseState(\n typeof rawState === 'number' || typeof rawState === 'string'\n ? rawState\n : undefined,\n );\n if (stateName === undefined) {\n return {\n outcome: 'pending' as const,\n diagnostic: {\n reason: `lease ${args.leaseUuid} state could not be decoded (raw=${String(rawState)})`,\n },\n };\n }\n return {\n outcome: (isTerminal(stateName) ? 'terminal' : 'pending') as\n | 'terminal'\n | 'pending',\n diagnostic: { stateName },\n };\n },\n successValues: ['terminal'],\n branches: {\n pending: {\n branchId: 'pending_drift',\n journalActionTags: ['close-lease-verify-pending'],\n buildFailureEnvelope: (d) => ({\n outcome: 'failed',\n reason:\n d.reason ??\n `close_lease tx accepted but state is still ${d.stateName ?? 'unknown'}.`,\n }),\n buildRecoveryOptions: () => [],\n },\n not_found: {\n branchId: 'unclassified',\n journalActionTags: ['close-lease-verify-not-found'],\n buildFailureEnvelope: (d) => ({\n outcome: 'failed',\n reason:\n d.reason ??\n `Lease ${args.leaseUuid} not visible on chain after close.`,\n }),\n buildRecoveryOptions: () => [],\n },\n },\n };\n\n const verifyResult = await verifyAndRecover(spec, undefined);\n\n if (verifyResult.result !== 'success') {\n const reason =\n verifyResult.failure?.reason ?? 'close-lease verification failed.';\n if (callbacks.onFailure) {\n await callbacks.onFailure({ reason });\n }\n throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, reason);\n }\n\n // Invariant: when `verifyAndRecover` returns success, the matched\n // outcome was `'terminal'`, and the verifier's `terminal` branch\n // ALWAYS sets `diagnostic.stateName` (see the spec above). A missing\n // `stateName` on the success path means the verifier invariant is\n // broken — likely a future refactor regression. The previous\n // implementation fell back to `'LEASE_STATE_CLOSED'` silently, which\n // would lie to the caller (Copilot review PR #60, comment 3276719603).\n // Fail loudly with a typed error instead. `TX_FAILED` is the closest\n // available code in `ManifestMCPErrorCode` (no `INTERNAL_ERROR`\n // variant); the message names the invariant explicitly.\n if (!verifyResult.diagnostic.stateName) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.TX_FAILED,\n `close-lease verifier invariant violated: success outcome reached without diagnostic.stateName for lease ${args.leaseUuid}`,\n );\n }\n const finalState: LeaseStateName = verifyResult.diagnostic.stateName;\n const result: CloseLeaseResult = {\n leaseUuid: args.leaseUuid,\n finalState,\n };\n callbacks.onComplete?.(result);\n return result;\n}\n\n// --- Helpers --------------------------------------------------------\n\nfunction validateArgs(args: CloseLeaseArgs): void {\n if (typeof args.leaseUuid !== 'string' || !args.leaseUuid.match(UUID_RE)) {\n throw new ManifestMCPError(\n ManifestMCPErrorCode.INVALID_CONFIG,\n `closeLease: leaseUuid must be a UUID; got \"${args.leaseUuid}\".`,\n );\n }\n}\n\nfunction renderConfirmationBlock(args: CloseLeaseArgs): DeploymentPlanBlock {\n // Image is not tracked in `CloseLeaseArgs` and `stopApp` doesn't return it;\n // surface the gap explicitly so reviewers/users see the missing context\n // rather than silently omitting an image field they'd expect.\n const text = [\n `Close lease ${args.leaseUuid}.`,\n ' Image: (image not recorded)',\n ' This is permanent — the lease cannot be reopened.',\n '',\n 'Proceed?',\n ].join('\\n');\n return { text };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA2CA,MAAM,UACJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCF,eAAsB,WACpB,MACA,WACA,MAC2B;AAC3B,cAAa,KAAK;CAElB,MAAM,QAAQ,wBAAwB,KAAK;AAC3C,KAAI,UAAU;MAER,MADgB,UAAU,UAAU,MAAM,KAChC,MACZ,OAAM,IAAI,iBACR,qBAAqB,qBACrB,6CACD;;AAGL,WAAU,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAM,QAAQ,KAAK,eAAe,KAAK,UAAU;CA6FjD,MAAM,eAAe,MAAM,iBAAiB;EArF1C,UAAU,YAAY;GAUpB,IAAI;AACJ,OAAI;AAEF,aAAS,OAAM,MADW,KAAK,cAAc,gBAAgB,EAClC,WAAW,QAAQ,GAAG,MAAM,EACrD,WAAW,KAAK,WACjB,CAAC;YACK,KAAK;IACZ,MAAM,SAAS,yBAAyB,KAAK,UAAU,wBACrD,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAElD,QAAI,UAAU,UACZ,OAAM,UAAU,UAAU,EAAE,QAAQ,CAAC;AAEvC,QAAI,eAAe,iBACjB,OAAM;AAER,UAAM,IAAI,iBAAiB,qBAAqB,cAAc,OAAO;;GAEvE,MAAM,QAAS,QAAgC;AAC/C,OAAI,UAAU,QAAQ,UAAU,KAAA,EAC9B,QAAO;IACL,SAAS;IACT,YAAY,EACV,QAAQ,SAAS,KAAK,UAAU,oCACjC;IACF;GAEH,MAAM,WAAY,MAA8B;GAChD,MAAM,YAAYA,OAChB,OAAO,aAAa,YAAY,OAAO,aAAa,WAChD,WACA,KAAA,EACL;AACD,OAAI,cAAc,KAAA,EAChB,QAAO;IACL,SAAS;IACT,YAAY,EACV,QAAQ,SAAS,KAAK,UAAU,mCAAmC,OAAO,SAAS,CAAC,IACrF;IACF;AAEH,UAAO;IACL,SAAU,WAAW,UAAU,GAAG,aAAa;IAG/C,YAAY,EAAE,WAAW;IAC1B;;EAEH,eAAe,CAAC,WAAW;EAC3B,UAAU;GACR,SAAS;IACP,UAAU;IACV,mBAAmB,CAAC,6BAA6B;IACjD,uBAAuB,OAAO;KAC5B,SAAS;KACT,QACE,EAAE,UACF,8CAA8C,EAAE,aAAa,UAAU;KAC1E;IACD,4BAA4B,EAAE;IAC/B;GACD,WAAW;IACT,UAAU;IACV,mBAAmB,CAAC,+BAA+B;IACnD,uBAAuB,OAAO;KAC5B,SAAS;KACT,QACE,EAAE,UACF,SAAS,KAAK,UAAU;KAC3B;IACD,4BAA4B,EAAE;IAC/B;GACF;EAG6C,EAAE,KAAA,EAAU;AAE5D,KAAI,aAAa,WAAW,WAAW;EACrC,MAAM,SACJ,aAAa,SAAS,UAAU;AAClC,MAAI,UAAU,UACZ,OAAM,UAAU,UAAU,EAAE,QAAQ,CAAC;AAEvC,QAAM,IAAI,iBAAiB,qBAAqB,WAAW,OAAO;;AAapE,KAAI,CAAC,aAAa,WAAW,UAC3B,OAAM,IAAI,iBACR,qBAAqB,WACrB,2GAA2G,KAAK,YACjH;CAEH,MAAM,aAA6B,aAAa,WAAW;CAC3D,MAAM,SAA2B;EAC/B,WAAW,KAAK;EAChB;EACD;AACD,WAAU,aAAa,OAAO;AAC9B,QAAO;;AAKT,SAAS,aAAa,MAA4B;AAChD,KAAI,OAAO,KAAK,cAAc,YAAY,CAAC,KAAK,UAAU,MAAM,QAAQ,CACtE,OAAM,IAAI,iBACR,qBAAqB,gBACrB,8CAA8C,KAAK,UAAU,IAC9D;;AAIL,SAAS,wBAAwB,MAA2C;AAW1E,QAAO,EAAE,MAPI;EACX,eAAe,KAAK,UAAU;EAC9B;EACA;EACA;EACA;EACD,CAAC,KAAK,KACM,EAAE"}
@@ -6,8 +6,9 @@ import { DeployAppCallbacks, DeployAppOptions, DeployResult, DeploySpec } from "
6
6
  * locked composition + E-hybrid runtime-context contract.
7
7
  *
8
8
  * @throws `ManifestMCPError(INVALID_CONFIG)` for spec / wallet validation.
9
- * @throws `ManifestMCPError(INVALID_CONFIG)` when `onConfirm` returns
10
- * `'no'` or `onPlan` returns `'cancel'`.
9
+ * @throws `ManifestMCPError(OPERATION_CANCELLED)` when `onConfirm` returns
10
+ * `'no'` or `onPlan` returns `'cancel'` (deliberate user cancellation —
11
+ * ENG-272).
11
12
  *
12
13
  * Errors from fred's broadcast or core's recovery primitives surface as
13
14
  * typed `ManifestMCPError`s. Partial-success failures with applicable
@@ -1 +1 @@
1
- {"version":3,"file":"deploy-app.d.ts","names":[],"sources":["../src/deploy-app.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;iBAoHsB,SAAA,CACpB,IAAA,EAAM,UAAA,EACN,SAAA,EAAW,kBAAA,EACX,IAAA,EAAM,gBAAA,GACL,OAAA,CAAQ,YAAA"}
1
+ {"version":3,"file":"deploy-app.d.ts","names":[],"sources":["../src/deploy-app.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;iBAiIsB,SAAA,CACpB,IAAA,EAAM,UAAA,EACN,SAAA,EAAW,kBAAA,EACX,IAAA,EAAM,gBAAA,GACL,OAAA,CAAQ,YAAA"}
@@ -1,15 +1,17 @@
1
1
  import { decode } from "./internals/lease-state.js";
2
+ import { isStackSpec, summarizeSpec, validateSpec } from "./internals/spec-normalize.js";
3
+ import { buildFredDeployInput, buildManifestPreviewInput } from "./internals/build-fred-input.js";
2
4
  import { classifyDeployError } from "./internals/classify-deploy-error.js";
3
5
  import { extractRunningEndpoints, formatEndpointAsUrl, normalizeFredUrl } from "./internals/connection.js";
4
6
  import { classifyDeployResponse } from "./internals/classify-deploy-response.js";
5
- import { findSkuUuid } from "./internals/find-sku-uuid.js";
6
7
  import { EMPTY_DENOM_MAP, loadChainDenomMap } from "./internals/humanize-denom.js";
8
+ import { evaluateReadinessFromFredResponse } from "./internals/evaluate-readiness-from-fred.js";
9
+ import { findSkuUuid } from "./internals/find-sku-uuid.js";
7
10
  import { renderDeploymentPlan } from "./internals/render-deployment-plan.js";
8
- import { isStackSpec, summarizeSpec, validateSpec } from "./internals/spec-normalize.js";
9
11
  import { renderIntentRecap } from "./internals/render-intent-recap.js";
10
12
  import { renderPartialSuccessPrompt } from "./internals/render-partial-success-prompt.js";
11
13
  import { ManifestMCPError, ManifestMCPErrorCode, cosmosEstimateFee, setItemCustomDomain, stopApp } from "@manifest-network/manifest-mcp-core";
12
- import { AuthTimestampTracker, buildManifestPreview, checkDeploymentReadiness, createAuthToken, createLeaseDataSignMessage, createSignMessage, deployApp as deployApp$1 } from "@manifest-network/manifest-mcp-fred";
14
+ import { AuthTimestampTracker, buildManifestPreview, checkDeploymentReadiness, createAuthToken, createLeaseDataSignMessage, createSignMessage, deployApp as deployApp$1, fetchActiveLease, pollLeaseUntilReady, resolveProviderUrl, uploadLeaseData, waitForAppReady } from "@manifest-network/manifest-mcp-fred";
13
15
  //#region src/deploy-app.ts
14
16
  /**
15
17
  * Public entry point: orchestrate a Manifest-Network app deployment from
@@ -54,8 +56,9 @@ import { AuthTimestampTracker, buildManifestPreview, checkDeploymentReadiness, c
54
56
  * locked composition + E-hybrid runtime-context contract.
55
57
  *
56
58
  * @throws `ManifestMCPError(INVALID_CONFIG)` for spec / wallet validation.
57
- * @throws `ManifestMCPError(INVALID_CONFIG)` when `onConfirm` returns
58
- * `'no'` or `onPlan` returns `'cancel'`.
59
+ * @throws `ManifestMCPError(OPERATION_CANCELLED)` when `onConfirm` returns
60
+ * `'no'` or `onPlan` returns `'cancel'` (deliberate user cancellation —
61
+ * ENG-272).
59
62
  *
60
63
  * Errors from fred's broadcast or core's recovery primitives surface as
61
64
  * typed `ManifestMCPError`s. Partial-success failures with applicable
@@ -81,10 +84,10 @@ async function deployApp(spec, callbacks, opts) {
81
84
  const chainId = opts.clientManager.getConfig().chainId;
82
85
  const activeChain = /mainnet|main/i.test(chainId) ? "mainnet" : "testnet";
83
86
  const queryClient = await opts.clientManager.getQueryClient();
84
- let readiness = evaluateReadinessFromRaw(await checkDeploymentReadiness(queryClient, tenantAddress, {
87
+ let readiness = evaluateReadinessFromFredResponse(await checkDeploymentReadiness(queryClient, tenantAddress, {
85
88
  image: primaryImage(spec),
86
89
  size: requestedSize(spec)
87
- }), opts.clientManager.getConfig().gasPrice ?? "1umfx", denomMap);
90
+ }), opts.clientManager.getConfig().gasPrice ?? "1umfx", denomMap, tenantAddress);
88
91
  callbacks.onProgress?.({
89
92
  kind: "readiness_evaluated",
90
93
  readiness
@@ -117,7 +120,7 @@ async function deployApp(spec, callbacks, opts) {
117
120
  });
118
121
  if (callbacks.onPlan) {
119
122
  const verdict = await callbacks.onPlan(plan);
120
- if (verdict === "cancel") throw new ManifestMCPError(ManifestMCPErrorCode.INVALID_CONFIG, "User cancelled deployment at plan step.");
123
+ if (verdict === "cancel") throw new ManifestMCPError(ManifestMCPErrorCode.OPERATION_CANCELLED, "User cancelled deployment at plan step.");
121
124
  if (verdict !== "confirm") {
122
125
  confirmedSpec = applyPlanEdit(confirmedSpec, verdict);
123
126
  try {
@@ -125,10 +128,10 @@ async function deployApp(spec, callbacks, opts) {
125
128
  } catch (err) {
126
129
  throw new ManifestMCPError(ManifestMCPErrorCode.INVALID_CONFIG, err instanceof Error ? `Post-edit spec failed validation: ${err.message}` : `Post-edit spec failed validation: ${String(err)}`);
127
130
  }
128
- readiness = evaluateReadinessFromRaw(await checkDeploymentReadiness(queryClient, tenantAddress, {
131
+ readiness = evaluateReadinessFromFredResponse(await checkDeploymentReadiness(queryClient, tenantAddress, {
129
132
  image: primaryImage(confirmedSpec),
130
133
  size: requestedSize(confirmedSpec)
131
- }), opts.clientManager.getConfig().gasPrice ?? "1umfx", denomMap);
134
+ }), opts.clientManager.getConfig().gasPrice ?? "1umfx", denomMap, tenantAddress);
132
135
  callbacks.onProgress?.({
133
136
  kind: "readiness_evaluated",
134
137
  readiness
@@ -162,7 +165,7 @@ async function deployApp(spec, callbacks, opts) {
162
165
  activeChain
163
166
  }) };
164
167
  if (callbacks.onConfirm) {
165
- if (await callbacks.onConfirm(recapBlock) !== "yes") throw new ManifestMCPError(ManifestMCPErrorCode.INVALID_CONFIG, "User declined to proceed at intent-recap step.");
168
+ if (await callbacks.onConfirm(recapBlock) !== "yes") throw new ManifestMCPError(ManifestMCPErrorCode.OPERATION_CANCELLED, "User declined to proceed at intent-recap step.");
166
169
  }
167
170
  callbacks.onProgress?.({ kind: "user_confirmed" });
168
171
  const signArbitrary = opts.walletProvider.signArbitrary.bind(opts.walletProvider);
@@ -183,16 +186,72 @@ async function deployApp(spec, callbacks, opts) {
183
186
  try {
184
187
  fredResult = await deployApp$1(opts.clientManager, getAuthToken, getLeaseDataAuthToken, fredInput, opts.fetchFn);
185
188
  } catch (err) {
186
- return await handleBroadcastFailure(err, confirmedSpec, callbacks, opts);
189
+ const recoveryCtx = {
190
+ manifestJson: preview.manifest_json,
191
+ metaHash: preview.meta_hash_hex,
192
+ getAuthToken,
193
+ getLeaseDataAuthToken,
194
+ tenantAddress,
195
+ chainId,
196
+ denomMap
197
+ };
198
+ return await handleBroadcastFailure(err, confirmedSpec, callbacks, opts, recoveryCtx);
187
199
  }
188
- const classification = classifyDeployResponse(fredResult);
200
+ let liveState = fredResult.state;
201
+ let liveConnection = fredResult.connection;
202
+ let classification = classifyDeployResponse(fredResult);
189
203
  callbacks.onProgress?.({
190
204
  kind: "deploy_response_classified",
191
205
  outcome: classification.outcome
192
206
  });
193
- if (classification.outcome !== "active") {
194
- const detail = classification.errorSummary ?? (classification.stateName !== void 0 ? `Lease ${classification.leaseUuid ?? "<no-uuid>"} returned non-active state ${classification.stateName}` : `fred deployApp returned non-active outcome '${classification.outcome}'`);
195
- throw new ManifestMCPError(ManifestMCPErrorCode.INVALID_CONFIG, `${detail}. Full routing (needs_wait → wait_for_app_ready polling; failed → FailureEnvelope) deferred to ENG-185 scope item #6.`);
207
+ if (classification.outcome === "failed") {
208
+ const reason = classification.errorSummary ?? `fred deployApp returned failed outcome for lease ${classification.leaseUuid ?? "<no-uuid>"}`;
209
+ throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, reason);
210
+ }
211
+ if (classification.outcome === "needs_wait") {
212
+ if (!classification.leaseUuid) throw new ManifestMCPError(ManifestMCPErrorCode.INVALID_CONFIG, "Internal invariant: classifier returned needs_wait without leaseUuid.");
213
+ const leaseUuid = classification.leaseUuid;
214
+ const pollStartMs = Date.now();
215
+ let attempt = 0;
216
+ let pollResult;
217
+ try {
218
+ pollResult = await waitForAppReady(queryClient, tenantAddress, leaseUuid, getAuthToken, {
219
+ timeoutMs: opts.waitForReadyTimeoutMs ?? 48e4,
220
+ onProgress: (status) => {
221
+ attempt += 1;
222
+ const stateName = decode(status.state);
223
+ callbacks.onProgress?.({
224
+ kind: "polling_for_readiness",
225
+ leaseUuid,
226
+ attempt,
227
+ elapsedMs: Date.now() - pollStartMs,
228
+ ...stateName !== void 0 ? { state: stateName } : {}
229
+ });
230
+ }
231
+ }, opts.fetchFn);
232
+ } catch (err) {
233
+ const reason = err instanceof Error ? `wait_for_app_ready failed for lease ${leaseUuid}: ${err.message}` : `wait_for_app_ready failed for lease ${leaseUuid}: ${String(err)}`;
234
+ throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, reason);
235
+ }
236
+ classification = classifyDeployResponse({
237
+ lease_uuid: pollResult.lease_uuid,
238
+ provider_uuid: pollResult.provider_uuid,
239
+ provider_url: pollResult.provider_url,
240
+ state: pollResult.state,
241
+ connection: pollResult.status
242
+ });
243
+ if (classification.outcome !== "active") {
244
+ const reason = classification.errorSummary ?? `wait_for_app_ready returned for lease ${leaseUuid} but post-poll classifier outcome is ${classification.outcome}`;
245
+ throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, reason);
246
+ }
247
+ fredResult = {
248
+ ...fredResult,
249
+ lease_uuid: pollResult.lease_uuid,
250
+ provider_uuid: pollResult.provider_uuid,
251
+ provider_url: pollResult.provider_url
252
+ };
253
+ liveState = pollResult.status.state;
254
+ liveConnection = pollResult.status;
196
255
  }
197
256
  callbacks.onProgress?.({
198
257
  kind: "app_ready_confirmed",
@@ -211,13 +270,13 @@ async function deployApp(spec, callbacks, opts) {
211
270
  callbacks
212
271
  });
213
272
  let leaseStateDecoded;
214
- if (fredResult.state === void 0) leaseStateDecoded = "LEASE_STATE_ACTIVE";
273
+ if (liveState === void 0) leaseStateDecoded = "LEASE_STATE_ACTIVE";
215
274
  else {
216
- const decoded = decode(fredResult.state);
217
- if (decoded === void 0) throw new ManifestMCPError(ManifestMCPErrorCode.INVALID_CONFIG, `Unrecognized lease state from fred deployApp response: ${String(fredResult.state)}. Cannot safely classify; refusing to silently coerce to ACTIVE.`);
275
+ const decoded = decode(liveState);
276
+ if (decoded === void 0) throw new ManifestMCPError(ManifestMCPErrorCode.INVALID_CONFIG, `Unrecognized lease state from fred deployApp response: ${String(liveState)}. Cannot safely classify; refusing to silently coerce to ACTIVE.`);
218
277
  leaseStateDecoded = decoded;
219
278
  }
220
- const endpointUrls = extractRunningEndpoints(fredResult.connection).map(formatEndpointAsUrl);
279
+ const endpointUrls = extractRunningEndpoints(liveConnection).map(formatEndpointAsUrl);
221
280
  const fallbackUrl = typeof fredResult.url === "string" ? normalizeFredUrl(fredResult.url) : "";
222
281
  const result = {
223
282
  leaseUuid: fredResult.lease_uuid,
@@ -251,40 +310,16 @@ function customDomainOf(spec) {
251
310
  function customDomainServiceOf(spec) {
252
311
  if (isStackSpec(spec)) return spec.serviceName;
253
312
  }
254
- function evaluateReadinessFromRaw(raw, gasPrice, denomMap) {
255
- const rawAny = raw;
256
- return {
257
- status: "ok",
258
- reasons: [],
259
- suggestedActions: [],
260
- walletBalances: rawAny.wallet_balances ?? [],
261
- credits: rawAny.credits ?? null,
262
- sku: rawAny.sku ?? null
263
- };
264
- }
265
- function buildManifestPreviewInput(spec, size) {
266
- if (isStackSpec(spec)) return {
267
- size,
268
- services: spec.services
269
- };
270
- const single = spec;
271
- return {
272
- size,
273
- image: single.image,
274
- port: typeof single.port === "number" ? single.port : Array.isArray(single.port) ? single.port[0] : void 0,
275
- env: single.env
276
- };
277
- }
278
313
  async function estimateFees(opts, spec, metaHashHex) {
279
314
  const size = requestedSize(spec);
280
315
  const { skuUuid } = await findSkuUuid(opts.clientManager, size);
281
- const itemArg = isStackSpec(spec) && spec.serviceName ? `${skuUuid}:1:${spec.serviceName}` : `${skuUuid}:1`;
316
+ const itemArgs = isStackSpec(spec) ? Object.keys(spec.services).map((name) => `${skuUuid}:1:${name}`) : [`${skuUuid}:1`];
282
317
  let createLeaseEstimate;
283
318
  try {
284
319
  createLeaseEstimate = await cosmosEstimateFee(opts.clientManager, "billing", "create-lease", [
285
320
  "--meta-hash",
286
321
  metaHashHex,
287
- itemArg
322
+ ...itemArgs
288
323
  ]);
289
324
  } catch (err) {
290
325
  const msg = `Failed to estimate create-lease fee: ${err instanceof Error ? err.message : String(err)}`;
@@ -301,27 +336,10 @@ async function estimateFees(opts, spec, metaHashHex) {
301
336
  },
302
337
  ...typeof customDomainOf(spec) === "string" ? { setDomain: {
303
338
  notEstimated: true,
304
- reason: "set-domain fee skipped pre-broadcast lease UUID unavailable; full approach-3 fallback deferred to PR-3.x"
339
+ reason: "no representative lease for pre-broadcast simulation"
305
340
  } } : {}
306
341
  };
307
342
  }
308
- function buildFredDeployInput(spec, size) {
309
- const base = { size };
310
- if (isStackSpec(spec)) base.services = spec.services;
311
- else {
312
- const single = spec;
313
- base.image = single.image;
314
- base.port = typeof single.port === "number" ? single.port : Array.isArray(single.port) ? single.port[0] : void 0;
315
- base.env = single.env;
316
- }
317
- const customDomain = spec.customDomain;
318
- if (customDomain) {
319
- base.customDomain = customDomain;
320
- const svcName = customDomainServiceOf(spec);
321
- if (svcName) base.serviceName = svcName;
322
- }
323
- return base;
324
- }
325
343
  function applyPlanEdit(spec, edit) {
326
344
  if (edit.kind === "replace_spec") return edit.spec;
327
345
  if (edit.kind === "edit_env") {
@@ -354,7 +372,7 @@ function applyPlanEdit(spec, edit) {
354
372
  }
355
373
  return spec;
356
374
  }
357
- async function handleBroadcastFailure(err, spec, callbacks, opts) {
375
+ async function handleBroadcastFailure(err, spec, callbacks, opts, ctx) {
358
376
  const requestedCustomDomain = customDomainOf(spec);
359
377
  const classified = classifyDeployError(err, { ...requestedCustomDomain ? { expectedCustomDomain: requestedCustomDomain } : {} });
360
378
  if (classified.outcome === "partially_succeeded" && classified.leaseUuid) {
@@ -364,33 +382,34 @@ async function handleBroadcastFailure(err, spec, callbacks, opts) {
364
382
  ...requestedCustomDomain ? { requestedCustomDomain } : {},
365
383
  reason: classified.reason
366
384
  };
367
- const options = renderPartialSuccessPrompt({
385
+ const promptPayload = renderPartialSuccessPrompt({
368
386
  leaseUuid: classified.leaseUuid,
369
387
  decodedState: "LEASE_STATE_PENDING",
370
388
  reason: classified.reason,
371
389
  ...requestedCustomDomain ? { requestedCustomDomain } : {}
372
- }).options.map((id) => ({
390
+ });
391
+ const options = promptPayload.options.map((id) => ({
373
392
  id,
374
393
  label: recoveryOptionLabel(id),
375
394
  description: recoveryOptionDescription(id)
376
395
  }));
377
- if (options.length > 0 && callbacks.onFailure !== void 0) return await dispatchRecovery(await callbacks.onFailure(envelope, options), envelope, spec, opts);
396
+ if (options.length > 0 && callbacks.onFailure !== void 0) {
397
+ callbacks.onProgress?.({
398
+ kind: "partial_success_prompt_rendered",
399
+ prompt: promptPayload.prompt,
400
+ leaseUuid: envelope.leaseUuid
401
+ });
402
+ return await dispatchRecovery(await callbacks.onFailure(envelope, options), envelope, spec, opts, callbacks, ctx);
403
+ }
378
404
  throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, classified.reason);
379
405
  }
380
406
  classified.reason;
381
407
  throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, classified.reason);
382
408
  }
383
- async function dispatchRecovery(choice, envelope, spec, opts) {
409
+ async function dispatchRecovery(choice, envelope, spec, opts, callbacks, ctx) {
384
410
  const leaseUuid = envelope.outcome === "partially_succeeded" ? envelope.leaseUuid : "";
385
411
  switch (choice.id) {
386
- case "retry_set_domain": {
387
- const domain = customDomainOf(spec);
388
- if (!domain) throw new ManifestMCPError(ManifestMCPErrorCode.INVALID_CONFIG, "retry_set_domain requires a customDomain in spec.");
389
- const serviceName = customDomainServiceOf(spec);
390
- const setItemOpts = serviceName ? { serviceName } : void 0;
391
- await setItemCustomDomain(opts.clientManager, leaseUuid, domain, setItemOpts);
392
- throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, `retry_set_domain completed for ${leaseUuid}; caller should re-run troubleshootDeployment to confirm app readiness.`);
393
- }
412
+ case "retry_set_domain": return await retrySetDomainAndComplete(leaseUuid, spec, opts, callbacks, ctx);
394
413
  case "salvage_without_domain": throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, `salvage_without_domain: lease ${leaseUuid} retained without domain; caller should re-run troubleshootDeployment.`);
395
414
  case "cancel_lease":
396
415
  case "close_lease":
@@ -399,6 +418,155 @@ async function dispatchRecovery(choice, envelope, spec, opts) {
399
418
  }
400
419
  throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, `Unknown recovery option: ${choice.id}`);
401
420
  }
421
+ /**
422
+ * `retry_set_domain` recovery: decompose the deploy after the partial-
423
+ * success failure. ENG-185 sub-PR E.
424
+ *
425
+ * Steps (mirrors fred's atomic `deployApp` minus the create-lease tx,
426
+ * which already succeeded):
427
+ * 1. `setItemCustomDomain` — broadcast the domain claim against the
428
+ * pre-existing lease. Stack specs thread `serviceName` so the
429
+ * tx targets the named lease item.
430
+ * 2. `fetchActiveLease` + `resolveProviderUrl` — look up the provider
431
+ * URL from the on-chain lease record (the partial-success error
432
+ * envelope only carries `leaseUuid`).
433
+ * 3. `uploadLeaseData` — push the manifest payload to the provider.
434
+ * Uses the ADR-036 lease-data auth token (signed against the
435
+ * manifest's meta-hash).
436
+ * 4. `pollLeaseUntilReady` — poll until the provider reports ACTIVE +
437
+ * running. Uses the LOWER-LEVEL primitive (not `waitForAppReady`)
438
+ * so the already-resolved `providerApiUrl` and auth-token closure
439
+ * pass through directly — no redundant on-chain queries (Copilot
440
+ * fix-1, PR #71). Reuses D's canonical polling-emission pattern:
441
+ * `onProgress` closure translates each `FredLeaseStatus` sample
442
+ * into a typed `polling_for_readiness` ProgressEvent, default
443
+ * 480_000ms timeout overridable via `opts.waitForReadyTimeoutMs`.
444
+ * 5. Defense #2 parity (post-poll re-classify) — guard the
445
+ * ACTIVE-with-no-instances race per D's pattern.
446
+ * 6. Persist manifest (best-effort) + build typed `DeployResult` +
447
+ * emit `app_ready_confirmed` + `success_rendered` + onComplete.
448
+ *
449
+ * Failure paths (sibling-parity wraps — every catch site surfaces
450
+ * `retry_set_domain <primitive-name> failed for lease ${leaseUuid}:
451
+ * ${err.message}` in the thrown message, matching D's L548-550 style).
452
+ * Error-code policy: typed `ManifestMCPError`s flow through with their
453
+ * original code preserved (precedent at `estimateFees` — see the
454
+ * `cosmosEstimateFee` catch block); untyped errors default to
455
+ * `TX_FAILED`. The post-poll re-classify path likewise prefixes BOTH
456
+ * the errorSummary-set and the no-errorSummary branches with
457
+ * `retry_set_domain` + leaseUuid (Copilot fix-4, PR #71):
458
+ * - `setItemCustomDomain` throws → wrap with prefix + leaseUuid +
459
+ * code preservation. Most likely cause: chain rejected the
460
+ * set-item-custom-domain tx (FQDN validation, reserved-suffix
461
+ * match, lease not active, etc.).
462
+ * - `fetchActiveLease` / `resolveProviderUrl` throw → wrap with
463
+ * prefix + leaseUuid.
464
+ * - `uploadLeaseData` throws → wrap with prefix + leaseUuid.
465
+ * - `pollLeaseUntilReady` throws → wrap with prefix + leaseUuid.
466
+ * - Post-poll re-classify outcome !== 'active' → wrap both
467
+ * branches: errorSummary-set (terminal-state response) AND
468
+ * no-errorSummary fallback (ACTIVE-with-no-instances Defense #2
469
+ * race) carry prefix + leaseUuid.
470
+ */
471
+ async function retrySetDomainAndComplete(leaseUuid, spec, opts, callbacks, ctx) {
472
+ const domain = customDomainOf(spec);
473
+ if (!domain) throw new ManifestMCPError(ManifestMCPErrorCode.INVALID_CONFIG, "retry_set_domain requires a customDomain in spec.");
474
+ const serviceName = customDomainServiceOf(spec);
475
+ const setItemOpts = serviceName ? { serviceName } : void 0;
476
+ try {
477
+ await setItemCustomDomain(opts.clientManager, leaseUuid, domain, setItemOpts);
478
+ } catch (err) {
479
+ const reason = err instanceof Error ? `retry_set_domain set-item-custom-domain failed for lease ${leaseUuid}: ${err.message}` : `retry_set_domain set-item-custom-domain failed for lease ${leaseUuid}: ${String(err)}`;
480
+ throw new ManifestMCPError(err instanceof ManifestMCPError ? err.code : ManifestMCPErrorCode.TX_FAILED, reason);
481
+ }
482
+ const queryClient = await opts.clientManager.getQueryClient();
483
+ let lease;
484
+ let providerApiUrl;
485
+ try {
486
+ lease = await fetchActiveLease(queryClient, leaseUuid, "cannot complete retry_set_domain");
487
+ providerApiUrl = await resolveProviderUrl(queryClient, lease.providerUuid);
488
+ } catch (err) {
489
+ const reason = err instanceof Error ? `retry_set_domain failed to resolve provider for lease ${leaseUuid}: ${err.message}` : `retry_set_domain failed to resolve provider for lease ${leaseUuid}: ${String(err)}`;
490
+ throw new ManifestMCPError(err instanceof ManifestMCPError ? err.code : ManifestMCPErrorCode.TX_FAILED, reason);
491
+ }
492
+ const manifestBytes = new TextEncoder().encode(ctx.manifestJson);
493
+ try {
494
+ const leaseDataAuthToken = await ctx.getLeaseDataAuthToken(ctx.tenantAddress, leaseUuid, ctx.metaHash);
495
+ await uploadLeaseData(providerApiUrl, leaseUuid, manifestBytes, leaseDataAuthToken, opts.fetchFn);
496
+ } catch (err) {
497
+ const reason = err instanceof Error ? `retry_set_domain manifest upload failed for lease ${leaseUuid}: ${err.message}` : `retry_set_domain manifest upload failed for lease ${leaseUuid}: ${String(err)}`;
498
+ throw new ManifestMCPError(err instanceof ManifestMCPError ? err.code : ManifestMCPErrorCode.TX_FAILED, reason);
499
+ }
500
+ const pollStartMs = Date.now();
501
+ let attempt = 0;
502
+ let pollResult;
503
+ try {
504
+ pollResult = await pollLeaseUntilReady(providerApiUrl, leaseUuid, () => ctx.getAuthToken(ctx.tenantAddress, leaseUuid), {
505
+ timeoutMs: opts.waitForReadyTimeoutMs ?? 48e4,
506
+ onProgress: (status) => {
507
+ attempt += 1;
508
+ const stateName = decode(status.state);
509
+ callbacks.onProgress?.({
510
+ kind: "polling_for_readiness",
511
+ leaseUuid,
512
+ attempt,
513
+ elapsedMs: Date.now() - pollStartMs,
514
+ ...stateName !== void 0 ? { state: stateName } : {}
515
+ });
516
+ }
517
+ }, opts.fetchFn);
518
+ } catch (err) {
519
+ const reason = err instanceof Error ? `retry_set_domain pollLeaseUntilReady failed for lease ${leaseUuid}: ${err.message}` : `retry_set_domain pollLeaseUntilReady failed for lease ${leaseUuid}: ${String(err)}`;
520
+ throw new ManifestMCPError(err instanceof ManifestMCPError ? err.code : ManifestMCPErrorCode.TX_FAILED, reason);
521
+ }
522
+ const classification = classifyDeployResponse({
523
+ lease_uuid: leaseUuid,
524
+ provider_uuid: lease.providerUuid,
525
+ provider_url: providerApiUrl,
526
+ state: pollResult.state,
527
+ connection: pollResult
528
+ });
529
+ if (classification.outcome !== "active") {
530
+ const reason = classification.errorSummary !== void 0 ? `retry_set_domain post-poll re-classification failed for lease ${leaseUuid}: ${classification.errorSummary}` : `retry_set_domain: pollLeaseUntilReady returned for lease ${leaseUuid} but post-poll classifier outcome is ${classification.outcome}`;
531
+ throw new ManifestMCPError(ManifestMCPErrorCode.TX_FAILED, reason);
532
+ }
533
+ callbacks.onProgress?.({
534
+ kind: "app_ready_confirmed",
535
+ leaseUuid
536
+ });
537
+ const persistedPath = await tryPersistManifest({
538
+ leaseUuid,
539
+ image: primaryImage(spec),
540
+ size: requestedSize(spec),
541
+ metaHash: ctx.metaHash,
542
+ chainId: ctx.chainId,
543
+ manifestJson: ctx.manifestJson,
544
+ customDomain: domain,
545
+ customDomainService: serviceName,
546
+ dataDir: opts.dataDir,
547
+ callbacks
548
+ });
549
+ const liveState = pollResult.state;
550
+ let leaseStateDecoded;
551
+ const decoded = decode(liveState);
552
+ if (decoded === void 0) throw new ManifestMCPError(ManifestMCPErrorCode.INVALID_CONFIG, `Unrecognized lease state from pollLeaseUntilReady response: ${String(liveState)}. Cannot safely classify; refusing to silently coerce to ACTIVE.`);
553
+ leaseStateDecoded = decoded;
554
+ const endpointUrls = extractRunningEndpoints(pollResult).map(formatEndpointAsUrl);
555
+ const result = {
556
+ leaseUuid,
557
+ providerUuid: lease.providerUuid,
558
+ leaseState: leaseStateDecoded,
559
+ urls: endpointUrls,
560
+ customDomain: domain,
561
+ manifestPath: persistedPath ?? ""
562
+ };
563
+ callbacks.onProgress?.({
564
+ kind: "success_rendered",
565
+ result
566
+ });
567
+ callbacks.onComplete?.(result);
568
+ return result;
569
+ }
402
570
  function recoveryOptionLabel(id) {
403
571
  switch (id) {
404
572
  case "retry_set_domain": return "Retry set-domain + upload";